Verzeichnisstruktur phpBB-3.3.15


Veröffentlicht
28.08.2024

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