[ Index ] |
PHP Cross Reference of phpBB-3.3.14-deutsch |
[Summary view] [Print] [Text view]
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\HttpFoundation\Request; 19 use Symfony\Component\HttpFoundation\Response; 20 use Symfony\Component\HttpKernel\HttpKernelInterface; 21 use Symfony\Component\HttpKernel\TerminableInterface; 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 $surrogate; 34 private $surrogateCacheStrategy; 35 private $options = []; 36 private $traces = []; 37 38 /** 39 * Constructor. 40 * 41 * The available options are: 42 * 43 * * debug: If true, the traces are added as a HTTP header to ease debugging 44 * 45 * * default_ttl The number of seconds that a cache entry should be considered 46 * fresh when no explicit freshness information is provided in 47 * a response. Explicit Cache-Control or Expires headers 48 * override this value. (default: 0) 49 * 50 * * private_headers Set of request headers that trigger "private" cache-control behavior 51 * on responses that don't explicitly state whether the response is 52 * public or private via a Cache-Control directive. (default: Authorization and Cookie) 53 * 54 * * allow_reload Specifies whether the client can force a cache reload by including a 55 * Cache-Control "no-cache" directive in the request. Set it to ``true`` 56 * for compliance with RFC 2616. (default: false) 57 * 58 * * allow_revalidate Specifies whether the client can force a cache revalidate by including 59 * a Cache-Control "max-age=0" directive in the request. Set it to ``true`` 60 * for compliance with RFC 2616. (default: false) 61 * 62 * * stale_while_revalidate Specifies the default number of seconds (the granularity is the second as the 63 * Response TTL precision is a second) during which the cache can immediately return 64 * a stale response while it revalidates it in the background (default: 2). 65 * This setting is overridden by the stale-while-revalidate HTTP Cache-Control 66 * extension (see RFC 5861). 67 * 68 * * stale_if_error Specifies the default number of seconds (the granularity is the second) during which 69 * the cache can serve a stale response when an error is encountered (default: 60). 70 * This setting is overridden by the stale-if-error HTTP Cache-Control extension 71 * (see RFC 5861). 72 */ 73 public function __construct(HttpKernelInterface $kernel, StoreInterface $store, SurrogateInterface $surrogate = null, array $options = []) 74 { 75 $this->store = $store; 76 $this->kernel = $kernel; 77 $this->surrogate = $surrogate; 78 79 // needed in case there is a fatal error because the backend is too slow to respond 80 register_shutdown_function([$this->store, 'cleanup']); 81 82 $this->options = array_merge([ 83 'debug' => false, 84 'default_ttl' => 0, 85 'private_headers' => ['Authorization', 'Cookie'], 86 'allow_reload' => false, 87 'allow_revalidate' => false, 88 'stale_while_revalidate' => 2, 89 'stale_if_error' => 60, 90 ], $options); 91 } 92 93 /** 94 * Gets the current store. 95 * 96 * @return StoreInterface A StoreInterface instance 97 */ 98 public function getStore() 99 { 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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Mon Nov 25 19:05:08 2024 | Cross-referenced by PHPXref 0.7.1 |