vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php line 420

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * This code is partially based on the Rack-Cache library by Ryan Tomayko,
  8.  * which is released under the MIT license.
  9.  * (based on commit 02d2b48d75bcb63cf1c0c7149c077ad256542801)
  10.  *
  11.  * For the full copyright and license information, please view the LICENSE
  12.  * file that was distributed with this source code.
  13.  */
  14. namespace Symfony\Component\HttpKernel\HttpCache;
  15. use Symfony\Component\HttpKernel\HttpKernelInterface;
  16. use Symfony\Component\HttpKernel\TerminableInterface;
  17. use Symfony\Component\HttpFoundation\Request;
  18. use Symfony\Component\HttpFoundation\Response;
  19. /**
  20.  * Cache provides HTTP caching.
  21.  *
  22.  * @author Fabien Potencier <fabien@symfony.com>
  23.  */
  24. class HttpCache implements HttpKernelInterfaceTerminableInterface
  25. {
  26.     private $kernel;
  27.     private $store;
  28.     private $request;
  29.     private $surrogate;
  30.     private $surrogateCacheStrategy;
  31.     private $options = array();
  32.     private $traces = array();
  33.     /**
  34.      * Constructor.
  35.      *
  36.      * The available options are:
  37.      *
  38.      *   * debug:                 If true, the traces are added as a HTTP header to ease debugging
  39.      *
  40.      *   * default_ttl            The number of seconds that a cache entry should be considered
  41.      *                            fresh when no explicit freshness information is provided in
  42.      *                            a response. Explicit Cache-Control or Expires headers
  43.      *                            override this value. (default: 0)
  44.      *
  45.      *   * private_headers        Set of request headers that trigger "private" cache-control behavior
  46.      *                            on responses that don't explicitly state whether the response is
  47.      *                            public or private via a Cache-Control directive. (default: Authorization and Cookie)
  48.      *
  49.      *   * allow_reload           Specifies whether the client can force a cache reload by including a
  50.      *                            Cache-Control "no-cache" directive in the request. Set it to ``true``
  51.      *                            for compliance with RFC 2616. (default: false)
  52.      *
  53.      *   * allow_revalidate       Specifies whether the client can force a cache revalidate by including
  54.      *                            a Cache-Control "max-age=0" directive in the request. Set it to ``true``
  55.      *                            for compliance with RFC 2616. (default: false)
  56.      *
  57.      *   * stale_while_revalidate Specifies the default number of seconds (the granularity is the second as the
  58.      *                            Response TTL precision is a second) during which the cache can immediately return
  59.      *                            a stale response while it revalidates it in the background (default: 2).
  60.      *                            This setting is overridden by the stale-while-revalidate HTTP Cache-Control
  61.      *                            extension (see RFC 5861).
  62.      *
  63.      *   * stale_if_error         Specifies the default number of seconds (the granularity is the second) during which
  64.      *                            the cache can serve a stale response when an error is encountered (default: 60).
  65.      *                            This setting is overridden by the stale-if-error HTTP Cache-Control extension
  66.      *                            (see RFC 5861).
  67.      */
  68.     public function __construct(HttpKernelInterface $kernelStoreInterface $storeSurrogateInterface $surrogate null, array $options = array())
  69.     {
  70.         $this->store $store;
  71.         $this->kernel $kernel;
  72.         $this->surrogate $surrogate;
  73.         // needed in case there is a fatal error because the backend is too slow to respond
  74.         register_shutdown_function(array($this->store'cleanup'));
  75.         $this->options array_merge(array(
  76.             'debug' => false,
  77.             'default_ttl' => 0,
  78.             'private_headers' => array('Authorization''Cookie'),
  79.             'allow_reload' => false,
  80.             'allow_revalidate' => false,
  81.             'stale_while_revalidate' => 2,
  82.             'stale_if_error' => 60,
  83.         ), $options);
  84.     }
  85.     /**
  86.      * Gets the current store.
  87.      *
  88.      * @return StoreInterface $store A StoreInterface instance
  89.      */
  90.     public function getStore()
  91.     {
  92.         return $this->store;
  93.     }
  94.     /**
  95.      * Returns an array of events that took place during processing of the last request.
  96.      *
  97.      * @return array An array of events
  98.      */
  99.     public function getTraces()
  100.     {
  101.         return $this->traces;
  102.     }
  103.     /**
  104.      * Returns a log message for the events of the last request processing.
  105.      *
  106.      * @return string A log message
  107.      */
  108.     public function getLog()
  109.     {
  110.         $log = array();
  111.         foreach ($this->traces as $request => $traces) {
  112.             $log[] = sprintf('%s: %s'$requestimplode(', '$traces));
  113.         }
  114.         return implode('; '$log);
  115.     }
  116.     /**
  117.      * Gets the Request instance associated with the master request.
  118.      *
  119.      * @return Request A Request instance
  120.      */
  121.     public function getRequest()
  122.     {
  123.         return $this->request;
  124.     }
  125.     /**
  126.      * Gets the Kernel instance.
  127.      *
  128.      * @return HttpKernelInterface An HttpKernelInterface instance
  129.      */
  130.     public function getKernel()
  131.     {
  132.         return $this->kernel;
  133.     }
  134.     /**
  135.      * Gets the Surrogate instance.
  136.      *
  137.      * @return SurrogateInterface A Surrogate instance
  138.      *
  139.      * @throws \LogicException
  140.      */
  141.     public function getSurrogate()
  142.     {
  143.         return $this->surrogate;
  144.     }
  145.     /**
  146.      * {@inheritdoc}
  147.      */
  148.     public function handle(Request $request$type HttpKernelInterface::MASTER_REQUEST$catch true)
  149.     {
  150.         // FIXME: catch exceptions and implement a 500 error page here? -> in Varnish, there is a built-in error page mechanism
  151.         if (HttpKernelInterface::MASTER_REQUEST === $type) {
  152.             $this->traces = array();
  153.             // Keep a clone of the original request for surrogates so they can access it.
  154.             // We must clone here to get a separate instance because the application will modify the request during
  155.             // the application flow (we know it always does because we do ourselves by setting REMOTE_ADDR to 127.0.0.1
  156.             // and adding the X-Forwarded-For header, see HttpCache::forward()).
  157.             $this->request = clone $request;
  158.             if (null !== $this->surrogate) {
  159.                 $this->surrogateCacheStrategy $this->surrogate->createCacheStrategy();
  160.             }
  161.         }
  162.         $this->traces[$this->getTraceKey($request)] = array();
  163.         if (!$request->isMethodSafe(false)) {
  164.             $response $this->invalidate($request$catch);
  165.         } elseif ($request->headers->has('expect') || !$request->isMethodCacheable()) {
  166.             $response $this->pass($request$catch);
  167.         } elseif ($this->options['allow_reload'] && $request->isNoCache()) {
  168.             /*
  169.                 If allow_reload is configured and the client requests "Cache-Control: no-cache",
  170.                 reload the cache by fetching a fresh response and caching it (if possible).
  171.             */
  172.             $this->record($request'reload');
  173.             $response $this->fetch($request$catch);
  174.         } else {
  175.             $response $this->lookup($request$catch);
  176.         }
  177.         $this->restoreResponseBody($request$response);
  178.         if (HttpKernelInterface::MASTER_REQUEST === $type && $this->options['debug']) {
  179.             $response->headers->set('X-Symfony-Cache'$this->getLog());
  180.         }
  181.         if (null !== $this->surrogate) {
  182.             if (HttpKernelInterface::MASTER_REQUEST === $type) {
  183.                 $this->surrogateCacheStrategy->update($response);
  184.             } else {
  185.                 $this->surrogateCacheStrategy->add($response);
  186.             }
  187.         }
  188.         $response->prepare($request);
  189.         $response->isNotModified($request);
  190.         return $response;
  191.     }
  192.     /**
  193.      * {@inheritdoc}
  194.      */
  195.     public function terminate(Request $requestResponse $response)
  196.     {
  197.         if ($this->getKernel() instanceof TerminableInterface) {
  198.             $this->getKernel()->terminate($request$response);
  199.         }
  200.     }
  201.     /**
  202.      * Forwards the Request to the backend without storing the Response in the cache.
  203.      *
  204.      * @param Request $request A Request instance
  205.      * @param bool    $catch   Whether to process exceptions
  206.      *
  207.      * @return Response A Response instance
  208.      */
  209.     protected function pass(Request $request$catch false)
  210.     {
  211.         $this->record($request'pass');
  212.         return $this->forward($request$catch);
  213.     }
  214.     /**
  215.      * Invalidates non-safe methods (like POST, PUT, and DELETE).
  216.      *
  217.      * @param Request $request A Request instance
  218.      * @param bool    $catch   Whether to process exceptions
  219.      *
  220.      * @return Response A Response instance
  221.      *
  222.      * @throws \Exception
  223.      *
  224.      * @see RFC2616 13.10
  225.      */
  226.     protected function invalidate(Request $request$catch false)
  227.     {
  228.         $response $this->pass($request$catch);
  229.         // invalidate only when the response is successful
  230.         if ($response->isSuccessful() || $response->isRedirect()) {
  231.             try {
  232.                 $this->store->invalidate($request);
  233.                 // As per the RFC, invalidate Location and Content-Location URLs if present
  234.                 foreach (array('Location''Content-Location') as $header) {
  235.                     if ($uri $response->headers->get($header)) {
  236.                         $subRequest Request::create($uri'get', array(), array(), array(), $request->server->all());
  237.                         $this->store->invalidate($subRequest);
  238.                     }
  239.                 }
  240.                 $this->record($request'invalidate');
  241.             } catch (\Exception $e) {
  242.                 $this->record($request'invalidate-failed');
  243.                 if ($this->options['debug']) {
  244.                     throw $e;
  245.                 }
  246.             }
  247.         }
  248.         return $response;
  249.     }
  250.     /**
  251.      * Lookups a Response from the cache for the given Request.
  252.      *
  253.      * When a matching cache entry is found and is fresh, it uses it as the
  254.      * response without forwarding any request to the backend. When a matching
  255.      * cache entry is found but is stale, it attempts to "validate" the entry with
  256.      * the backend using conditional GET. When no matching cache entry is found,
  257.      * it triggers "miss" processing.
  258.      *
  259.      * @param Request $request A Request instance
  260.      * @param bool    $catch   Whether to process exceptions
  261.      *
  262.      * @return Response A Response instance
  263.      *
  264.      * @throws \Exception
  265.      */
  266.     protected function lookup(Request $request$catch false)
  267.     {
  268.         try {
  269.             $entry $this->store->lookup($request);
  270.         } catch (\Exception $e) {
  271.             $this->record($request'lookup-failed');
  272.             if ($this->options['debug']) {
  273.                 throw $e;
  274.             }
  275.             return $this->pass($request$catch);
  276.         }
  277.         if (null === $entry) {
  278.             $this->record($request'miss');
  279.             return $this->fetch($request$catch);
  280.         }
  281.         if (!$this->isFreshEnough($request$entry)) {
  282.             $this->record($request'stale');
  283.             return $this->validate($request$entry$catch);
  284.         }
  285.         $this->record($request'fresh');
  286.         $entry->headers->set('Age'$entry->getAge());
  287.         return $entry;
  288.     }
  289.     /**
  290.      * Validates that a cache entry is fresh.
  291.      *
  292.      * The original request is used as a template for a conditional
  293.      * GET request with the backend.
  294.      *
  295.      * @param Request  $request A Request instance
  296.      * @param Response $entry   A Response instance to validate
  297.      * @param bool     $catch   Whether to process exceptions
  298.      *
  299.      * @return Response A Response instance
  300.      */
  301.     protected function validate(Request $requestResponse $entry$catch false)
  302.     {
  303.         $subRequest = clone $request;
  304.         // send no head requests because we want content
  305.         if ('HEAD' === $request->getMethod()) {
  306.             $subRequest->setMethod('GET');
  307.         }
  308.         // add our cached last-modified validator
  309.         $subRequest->headers->set('if_modified_since'$entry->headers->get('Last-Modified'));
  310.         // Add our cached etag validator to the environment.
  311.         // We keep the etags from the client to handle the case when the client
  312.         // has a different private valid entry which is not cached here.
  313.         $cachedEtags $entry->getEtag() ? array($entry->getEtag()) : array();
  314.         $requestEtags $request->getETags();
  315.         if ($etags array_unique(array_merge($cachedEtags$requestEtags))) {
  316.             $subRequest->headers->set('if_none_match'implode(', '$etags));
  317.         }
  318.         $response $this->forward($subRequest$catch$entry);
  319.         if (304 == $response->getStatusCode()) {
  320.             $this->record($request'valid');
  321.             // return the response and not the cache entry if the response is valid but not cached
  322.             $etag $response->getEtag();
  323.             if ($etag && in_array($etag$requestEtags) && !in_array($etag$cachedEtags)) {
  324.                 return $response;
  325.             }
  326.             $entry = clone $entry;
  327.             $entry->headers->remove('Date');
  328.             foreach (array('Date''Expires''Cache-Control''ETag''Last-Modified') as $name) {
  329.                 if ($response->headers->has($name)) {
  330.                     $entry->headers->set($name$response->headers->get($name));
  331.                 }
  332.             }
  333.             $response $entry;
  334.         } else {
  335.             $this->record($request'invalid');
  336.         }
  337.         if ($response->isCacheable()) {
  338.             $this->store($request$response);
  339.         }
  340.         return $response;
  341.     }
  342.     /**
  343.      * Unconditionally fetches a fresh response from the backend and
  344.      * stores it in the cache if is cacheable.
  345.      *
  346.      * @param Request $request A Request instance
  347.      * @param bool    $catch   Whether to process exceptions
  348.      *
  349.      * @return Response A Response instance
  350.      */
  351.     protected function fetch(Request $request$catch false)
  352.     {
  353.         $subRequest = clone $request;
  354.         // send no head requests because we want content
  355.         if ('HEAD' === $request->getMethod()) {
  356.             $subRequest->setMethod('GET');
  357.         }
  358.         // avoid that the backend sends no content
  359.         $subRequest->headers->remove('if_modified_since');
  360.         $subRequest->headers->remove('if_none_match');
  361.         $response $this->forward($subRequest$catch);
  362.         if ($response->isCacheable()) {
  363.             $this->store($request$response);
  364.         }
  365.         return $response;
  366.     }
  367.     /**
  368.      * Forwards the Request to the backend and returns the Response.
  369.      *
  370.      * All backend requests (cache passes, fetches, cache validations)
  371.      * run through this method.
  372.      *
  373.      * @param Request  $request A Request instance
  374.      * @param bool     $catch   Whether to catch exceptions or not
  375.      * @param Response $entry   A Response instance (the stale entry if present, null otherwise)
  376.      *
  377.      * @return Response A Response instance
  378.      */
  379.     protected function forward(Request $request$catch falseResponse $entry null)
  380.     {
  381.         if ($this->surrogate) {
  382.             $this->surrogate->addSurrogateCapability($request);
  383.         }
  384.         // modify the X-Forwarded-For header if needed
  385.         $forwardedFor $request->headers->get('X-Forwarded-For');
  386.         if ($forwardedFor) {
  387.             $request->headers->set('X-Forwarded-For'$forwardedFor.', '.$request->server->get('REMOTE_ADDR'));
  388.         } else {
  389.             $request->headers->set('X-Forwarded-For'$request->server->get('REMOTE_ADDR'));
  390.         }
  391.         // fix the client IP address by setting it to 127.0.0.1 as HttpCache
  392.         // is always called from the same process as the backend.
  393.         $request->server->set('REMOTE_ADDR''127.0.0.1');
  394.         // make sure HttpCache is a trusted proxy
  395.         if (!in_array('127.0.0.1'$trustedProxies Request::getTrustedProxies())) {
  396.             $trustedProxies[] = '127.0.0.1';
  397.             Request::setTrustedProxies($trustedProxiesRequest::HEADER_X_FORWARDED_ALL);
  398.         }
  399.         // always a "master" request (as the real master request can be in cache)
  400.         $response $this->kernel->handle($requestHttpKernelInterface::MASTER_REQUEST$catch);
  401.         // FIXME: we probably need to also catch exceptions if raw === true
  402.         // we don't implement the stale-if-error on Requests, which is nonetheless part of the RFC
  403.         if (null !== $entry && in_array($response->getStatusCode(), array(500502503504))) {
  404.             if (null === $age $entry->headers->getCacheControlDirective('stale-if-error')) {
  405.                 $age $this->options['stale_if_error'];
  406.             }
  407.             if (abs($entry->getTtl()) < $age) {
  408.                 $this->record($request'stale-if-error');
  409.                 return $entry;
  410.             }
  411.         }
  412.         /*
  413.             RFC 7231 Sect. 7.1.1.2 says that a server that does not have a reasonably accurate
  414.             clock MUST NOT send a "Date" header, although it MUST send one in most other cases
  415.             except for 1xx or 5xx responses where it MAY do so.
  416.             Anyway, a client that received a message without a "Date" header MUST add it.
  417.         */
  418.         if (!$response->headers->has('Date')) {
  419.             $response->setDate(\DateTime::createFromFormat('U'time()));
  420.         }
  421.         $this->processResponseBody($request$response);
  422.         if ($this->isPrivateRequest($request) && !$response->headers->hasCacheControlDirective('public')) {
  423.             $response->setPrivate();
  424.         } elseif ($this->options['default_ttl'] > && null === $response->getTtl() && !$response->headers->getCacheControlDirective('must-revalidate')) {
  425.             $response->setTtl($this->options['default_ttl']);
  426.         }
  427.         return $response;
  428.     }
  429.     /**
  430.      * Checks whether the cache entry is "fresh enough" to satisfy the Request.
  431.      *
  432.      * @return bool true if the cache entry if fresh enough, false otherwise
  433.      */
  434.     protected function isFreshEnough(Request $requestResponse $entry)
  435.     {
  436.         if (!$entry->isFresh()) {
  437.             return $this->lock($request$entry);
  438.         }
  439.         if ($this->options['allow_revalidate'] && null !== $maxAge $request->headers->getCacheControlDirective('max-age')) {
  440.             return $maxAge && $maxAge >= $entry->getAge();
  441.         }
  442.         return true;
  443.     }
  444.     /**
  445.      * Locks a Request during the call to the backend.
  446.      *
  447.      * @return bool true if the cache entry can be returned even if it is staled, false otherwise
  448.      */
  449.     protected function lock(Request $requestResponse $entry)
  450.     {
  451.         // try to acquire a lock to call the backend
  452.         $lock $this->store->lock($request);
  453.         if (true === $lock) {
  454.             // we have the lock, call the backend
  455.             return false;
  456.         }
  457.         // there is already another process calling the backend
  458.         // May we serve a stale response?
  459.         if ($this->mayServeStaleWhileRevalidate($entry)) {
  460.             $this->record($request'stale-while-revalidate');
  461.             return true;
  462.         }
  463.         // wait for the lock to be released
  464.         if ($this->waitForLock($request)) {
  465.             // replace the current entry with the fresh one
  466.             $new $this->lookup($request);
  467.             $entry->headers $new->headers;
  468.             $entry->setContent($new->getContent());
  469.             $entry->setStatusCode($new->getStatusCode());
  470.             $entry->setProtocolVersion($new->getProtocolVersion());
  471.             foreach ($new->headers->getCookies() as $cookie) {
  472.                 $entry->headers->setCookie($cookie);
  473.             }
  474.         } else {
  475.             // backend is slow as hell, send a 503 response (to avoid the dog pile effect)
  476.             $entry->setStatusCode(503);
  477.             $entry->setContent('503 Service Unavailable');
  478.             $entry->headers->set('Retry-After'10);
  479.         }
  480.         return true;
  481.     }
  482.     /**
  483.      * Writes the Response to the cache.
  484.      *
  485.      * @throws \Exception
  486.      */
  487.     protected function store(Request $requestResponse $response)
  488.     {
  489.         try {
  490.             $this->store->write($request$response);
  491.             $this->record($request'store');
  492.             $response->headers->set('Age'$response->getAge());
  493.         } catch (\Exception $e) {
  494.             $this->record($request'store-failed');
  495.             if ($this->options['debug']) {
  496.                 throw $e;
  497.             }
  498.         }
  499.         // now that the response is cached, release the lock
  500.         $this->store->unlock($request);
  501.     }
  502.     /**
  503.      * Restores the Response body.
  504.      */
  505.     private function restoreResponseBody(Request $requestResponse $response)
  506.     {
  507.         if ($response->headers->has('X-Body-Eval')) {
  508.             ob_start();
  509.             if ($response->headers->has('X-Body-File')) {
  510.                 include $response->headers->get('X-Body-File');
  511.             } else {
  512.                 eval('; ?>'.$response->getContent().'<?php ;');
  513.             }
  514.             $response->setContent(ob_get_clean());
  515.             $response->headers->remove('X-Body-Eval');
  516.             if (!$response->headers->has('Transfer-Encoding')) {
  517.                 $response->headers->set('Content-Length'strlen($response->getContent()));
  518.             }
  519.         } elseif ($response->headers->has('X-Body-File')) {
  520.             // Response does not include possibly dynamic content (ESI, SSI), so we need
  521.             // not handle the content for HEAD requests
  522.             if (!$request->isMethod('HEAD')) {
  523.                 $response->setContent(file_get_contents($response->headers->get('X-Body-File')));
  524.             }
  525.         } else {
  526.             return;
  527.         }
  528.         $response->headers->remove('X-Body-File');
  529.     }
  530.     protected function processResponseBody(Request $requestResponse $response)
  531.     {
  532.         if (null !== $this->surrogate && $this->surrogate->needsParsing($response)) {
  533.             $this->surrogate->process($request$response);
  534.         }
  535.     }
  536.     /**
  537.      * Checks if the Request includes authorization or other sensitive information
  538.      * that should cause the Response to be considered private by default.
  539.      *
  540.      * @return bool true if the Request is private, false otherwise
  541.      */
  542.     private function isPrivateRequest(Request $request)
  543.     {
  544.         foreach ($this->options['private_headers'] as $key) {
  545.             $key strtolower(str_replace('HTTP_'''$key));
  546.             if ('cookie' === $key) {
  547.                 if (count($request->cookies->all())) {
  548.                     return true;
  549.                 }
  550.             } elseif ($request->headers->has($key)) {
  551.                 return true;
  552.             }
  553.         }
  554.         return false;
  555.     }
  556.     /**
  557.      * Records that an event took place.
  558.      *
  559.      * @param Request $request A Request instance
  560.      * @param string  $event   The event name
  561.      */
  562.     private function record(Request $request$event)
  563.     {
  564.         $this->traces[$this->getTraceKey($request)][] = $event;
  565.     }
  566.     /**
  567.      * Calculates the key we use in the "trace" array for a given request.
  568.      *
  569.      * @param Request $request
  570.      *
  571.      * @return string
  572.      */
  573.     private function getTraceKey(Request $request)
  574.     {
  575.         $path $request->getPathInfo();
  576.         if ($qs $request->getQueryString()) {
  577.             $path .= '?'.$qs;
  578.         }
  579.         return $request->getMethod().' '.$path;
  580.     }
  581.     /**
  582.      * Checks whether the given (cached) response may be served as "stale" when a revalidation
  583.      * is currently in progress.
  584.      *
  585.      * @param Response $entry
  586.      *
  587.      * @return bool true when the stale response may be served, false otherwise
  588.      */
  589.     private function mayServeStaleWhileRevalidate(Response $entry)
  590.     {
  591.         $timeout $entry->headers->getCacheControlDirective('stale-while-revalidate');
  592.         if (null === $timeout) {
  593.             $timeout $this->options['stale_while_revalidate'];
  594.         }
  595.         return abs($entry->getTtl()) < $timeout;
  596.     }
  597.     /**
  598.      * Waits for the store to release a locked entry.
  599.      *
  600.      * @param Request $request The request to wait for
  601.      *
  602.      * @return bool true if the lock was released before the internal timeout was hit; false if the wait timeout was exceeded
  603.      */
  604.     private function waitForLock(Request $request)
  605.     {
  606.         $wait 0;
  607.         while ($this->store->isLocked($request) && $wait 100) {
  608.             usleep(50000);
  609.             ++$wait;
  610.         }
  611.         return $wait 100;
  612.     }
  613. }