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