Verzeichnisstruktur phpBB-3.1.0


Veröffentlicht
27.10.2014

So funktioniert es


Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück

Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis.
Auf den Verzeichnisnamen klicken, dies zeigt nur das Verzeichnis mit Inhalt an

      (Beispiel Datei-Icons)

Auf das Icon klicken um den Quellcode anzuzeigen

HttpCache.php

Zuletzt modifiziert: 09.10.2024, 12:58 - Dateigröße: 23.16 KiB


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