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