[ Index ]

PHP Cross Reference of phpBB-3.1.12-deutsch

title

Body

[close]

/vendor/symfony/http-kernel/Symfony/Component/HttpKernel/HttpCache/ -> HttpCache.php (source)

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


Generated: Thu Jan 11 00:25:41 2018 Cross-referenced by PHPXref 0.7.1