[ Index ] |
PHP Cross Reference of phpBB-3.2.11-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 = array(); 36 private $traces = array(); 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 = array()) 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(array($this->store, 'cleanup')); 81 82 $this->options = array_merge(array( 83 'debug' => false, 84 'default_ttl' => 0, 85 'private_headers' => array('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 = array(); 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 if (!$this->surrogate instanceof Esi) { 158 throw new \LogicException('This instance of HttpCache was not set up to use ESI as surrogate handler. You must overwrite and use createSurrogate'); 159 } 160 161 return $this->surrogate; 162 } 163 164 /** 165 * Gets the Esi instance. 166 * 167 * @return Esi An Esi instance 168 * 169 * @throws \LogicException 170 * 171 * @deprecated since version 2.6, to be removed in 3.0. Use getSurrogate() instead 172 */ 173 public function getEsi() 174 { 175 @trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.6 and will be removed in 3.0. Use the getSurrogate() method instead.', E_USER_DEPRECATED); 176 177 return $this->getSurrogate(); 178 } 179 180 /** 181 * {@inheritdoc} 182 */ 183 public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true) 184 { 185 // FIXME: catch exceptions and implement a 500 error page here? -> in Varnish, there is a built-in error page mechanism 186 if (HttpKernelInterface::MASTER_REQUEST === $type) { 187 $this->traces = array(); 188 // Keep a clone of the original request for surrogates so they can access it. 189 // We must clone here to get a separate instance because the application will modify the request during 190 // the application flow (we know it always does because we do ourselves by setting REMOTE_ADDR to 127.0.0.1 191 // and adding the X-Forwarded-For header, see HttpCache::forward()). 192 $this->request = clone $request; 193 if (null !== $this->surrogate) { 194 $this->surrogateCacheStrategy = $this->surrogate->createCacheStrategy(); 195 } 196 } 197 198 $path = $request->getPathInfo(); 199 if ($qs = $request->getQueryString()) { 200 $path .= '?'.$qs; 201 } 202 $this->traces[$request->getMethod().' '.$path] = array(); 203 204 if (!$request->isMethodSafe(false)) { 205 $response = $this->invalidate($request, $catch); 206 } elseif ($request->headers->has('expect') || !$request->isMethodCacheable()) { 207 $response = $this->pass($request, $catch); 208 } else { 209 $response = $this->lookup($request, $catch); 210 } 211 212 $this->restoreResponseBody($request, $response); 213 214 $response->setDate(\DateTime::createFromFormat('U', time(), new \DateTimeZone('UTC'))); 215 216 if (HttpKernelInterface::MASTER_REQUEST === $type && $this->options['debug']) { 217 $response->headers->set('X-Symfony-Cache', $this->getLog()); 218 } 219 220 if (null !== $this->surrogate) { 221 if (HttpKernelInterface::MASTER_REQUEST === $type) { 222 $this->surrogateCacheStrategy->update($response); 223 } else { 224 $this->surrogateCacheStrategy->add($response); 225 } 226 } 227 228 $response->prepare($request); 229 230 $response->isNotModified($request); 231 232 return $response; 233 } 234 235 /** 236 * {@inheritdoc} 237 */ 238 public function terminate(Request $request, Response $response) 239 { 240 if ($this->getKernel() instanceof TerminableInterface) { 241 $this->getKernel()->terminate($request, $response); 242 } 243 } 244 245 /** 246 * Forwards the Request to the backend without storing the Response in the cache. 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 protected function pass(Request $request, $catch = false) 254 { 255 $this->record($request, 'pass'); 256 257 return $this->forward($request, $catch); 258 } 259 260 /** 261 * Invalidates non-safe methods (like POST, PUT, and DELETE). 262 * 263 * @param Request $request A Request instance 264 * @param bool $catch Whether to process exceptions 265 * 266 * @return Response A Response instance 267 * 268 * @throws \Exception 269 * 270 * @see RFC2616 13.10 271 */ 272 protected function invalidate(Request $request, $catch = false) 273 { 274 $response = $this->pass($request, $catch); 275 276 // invalidate only when the response is successful 277 if ($response->isSuccessful() || $response->isRedirect()) { 278 try { 279 $this->store->invalidate($request); 280 281 // As per the RFC, invalidate Location and Content-Location URLs if present 282 foreach (array('Location', 'Content-Location') as $header) { 283 if ($uri = $response->headers->get($header)) { 284 $subRequest = Request::create($uri, 'get', array(), array(), array(), $request->server->all()); 285 286 $this->store->invalidate($subRequest); 287 } 288 } 289 290 $this->record($request, 'invalidate'); 291 } catch (\Exception $e) { 292 $this->record($request, 'invalidate-failed'); 293 294 if ($this->options['debug']) { 295 throw $e; 296 } 297 } 298 } 299 300 return $response; 301 } 302 303 /** 304 * Lookups a Response from the cache for the given Request. 305 * 306 * When a matching cache entry is found and is fresh, it uses it as the 307 * response without forwarding any request to the backend. When a matching 308 * cache entry is found but is stale, it attempts to "validate" the entry with 309 * the backend using conditional GET. When no matching cache entry is found, 310 * it triggers "miss" processing. 311 * 312 * @param Request $request A Request instance 313 * @param bool $catch Whether to process exceptions 314 * 315 * @return Response A Response instance 316 * 317 * @throws \Exception 318 */ 319 protected function lookup(Request $request, $catch = false) 320 { 321 // if allow_reload and no-cache Cache-Control, allow a cache reload 322 if ($this->options['allow_reload'] && $request->isNoCache()) { 323 $this->record($request, 'reload'); 324 325 return $this->fetch($request, $catch); 326 } 327 328 try { 329 $entry = $this->store->lookup($request); 330 } catch (\Exception $e) { 331 $this->record($request, 'lookup-failed'); 332 333 if ($this->options['debug']) { 334 throw $e; 335 } 336 337 return $this->pass($request, $catch); 338 } 339 340 if (null === $entry) { 341 $this->record($request, 'miss'); 342 343 return $this->fetch($request, $catch); 344 } 345 346 if (!$this->isFreshEnough($request, $entry)) { 347 $this->record($request, 'stale'); 348 349 return $this->validate($request, $entry, $catch); 350 } 351 352 $this->record($request, 'fresh'); 353 354 $entry->headers->set('Age', $entry->getAge()); 355 356 return $entry; 357 } 358 359 /** 360 * Validates that a cache entry is fresh. 361 * 362 * The original request is used as a template for a conditional 363 * GET request with the backend. 364 * 365 * @param Request $request A Request instance 366 * @param Response $entry A Response instance to validate 367 * @param bool $catch Whether to process exceptions 368 * 369 * @return Response A Response instance 370 */ 371 protected function validate(Request $request, Response $entry, $catch = false) 372 { 373 $subRequest = clone $request; 374 375 // send no head requests because we want content 376 if ('HEAD' === $request->getMethod()) { 377 $subRequest->setMethod('GET'); 378 } 379 380 // add our cached last-modified validator 381 $subRequest->headers->set('if_modified_since', $entry->headers->get('Last-Modified')); 382 383 // Add our cached etag validator to the environment. 384 // We keep the etags from the client to handle the case when the client 385 // has a different private valid entry which is not cached here. 386 $cachedEtags = $entry->getEtag() ? array($entry->getEtag()) : array(); 387 $requestEtags = $request->getETags(); 388 if ($etags = array_unique(array_merge($cachedEtags, $requestEtags))) { 389 $subRequest->headers->set('if_none_match', implode(', ', $etags)); 390 } 391 392 $response = $this->forward($subRequest, $catch, $entry); 393 394 if (304 == $response->getStatusCode()) { 395 $this->record($request, 'valid'); 396 397 // return the response and not the cache entry if the response is valid but not cached 398 $etag = $response->getEtag(); 399 if ($etag && \in_array($etag, $requestEtags) && !\in_array($etag, $cachedEtags)) { 400 return $response; 401 } 402 403 $entry = clone $entry; 404 $entry->headers->remove('Date'); 405 406 foreach (array('Date', 'Expires', 'Cache-Control', 'ETag', 'Last-Modified') as $name) { 407 if ($response->headers->has($name)) { 408 $entry->headers->set($name, $response->headers->get($name)); 409 } 410 } 411 412 $response = $entry; 413 } else { 414 $this->record($request, 'invalid'); 415 } 416 417 if ($response->isCacheable()) { 418 $this->store($request, $response); 419 } 420 421 return $response; 422 } 423 424 /** 425 * Forwards the Request to the backend and determines whether the response should be stored. 426 * 427 * This methods is triggered when the cache missed or a reload is required. 428 * 429 * @param Request $request A Request instance 430 * @param bool $catch Whether to process exceptions 431 * 432 * @return Response A Response instance 433 */ 434 protected function fetch(Request $request, $catch = false) 435 { 436 $subRequest = clone $request; 437 438 // send no head requests because we want content 439 if ('HEAD' === $request->getMethod()) { 440 $subRequest->setMethod('GET'); 441 } 442 443 // avoid that the backend sends no content 444 $subRequest->headers->remove('if_modified_since'); 445 $subRequest->headers->remove('if_none_match'); 446 447 $response = $this->forward($subRequest, $catch); 448 449 if ($response->isCacheable()) { 450 $this->store($request, $response); 451 } 452 453 return $response; 454 } 455 456 /** 457 * Forwards the Request to the backend and returns the Response. 458 * 459 * @param Request $request A Request instance 460 * @param bool $catch Whether to catch exceptions or not 461 * @param Response $entry A Response instance (the stale entry if present, null otherwise) 462 * 463 * @return Response A Response instance 464 */ 465 protected function forward(Request $request, $catch = false, Response $entry = null) 466 { 467 if ($this->surrogate) { 468 $this->surrogate->addSurrogateCapability($request); 469 } 470 471 // always a "master" request (as the real master request can be in cache) 472 $response = SubRequestHandler::handle($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $catch); 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(); 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 * @return bool true if the cache entry if fresh enough, false otherwise 502 */ 503 protected function isFreshEnough(Request $request, Response $entry) 504 { 505 if (!$entry->isFresh()) { 506 return $this->lock($request, $entry); 507 } 508 509 if ($this->options['allow_revalidate'] && null !== $maxAge = $request->headers->getCacheControlDirective('max-age')) { 510 return $maxAge > 0 && $maxAge >= $entry->getAge(); 511 } 512 513 return true; 514 } 515 516 /** 517 * Locks a Request during the call to the backend. 518 * 519 * @return bool true if the cache entry can be returned even if it is staled, false otherwise 520 */ 521 protected function lock(Request $request, Response $entry) 522 { 523 // try to acquire a lock to call the backend 524 $lock = $this->store->lock($request); 525 526 // there is already another process calling the backend 527 if (true !== $lock) { 528 // check if we can serve the stale entry 529 if (null === $age = $entry->headers->getCacheControlDirective('stale-while-revalidate')) { 530 $age = $this->options['stale_while_revalidate']; 531 } 532 533 if (abs($entry->getTtl()) < $age) { 534 $this->record($request, 'stale-while-revalidate'); 535 536 // server the stale response while there is a revalidation 537 return true; 538 } 539 540 // wait for the lock to be released 541 $wait = 0; 542 while ($this->store->isLocked($request) && $wait < 5000000) { 543 usleep(50000); 544 $wait += 50000; 545 } 546 547 if ($wait < 5000000) { 548 // replace the current entry with the fresh one 549 $new = $this->lookup($request); 550 $entry->headers = $new->headers; 551 $entry->setContent($new->getContent()); 552 $entry->setStatusCode($new->getStatusCode()); 553 $entry->setProtocolVersion($new->getProtocolVersion()); 554 foreach ($new->headers->getCookies() as $cookie) { 555 $entry->headers->setCookie($cookie); 556 } 557 } else { 558 // backend is slow as hell, send a 503 response (to avoid the dog pile effect) 559 $entry->setStatusCode(503); 560 $entry->setContent('503 Service Unavailable'); 561 $entry->headers->set('Retry-After', 10); 562 } 563 564 return true; 565 } 566 567 // we have the lock, call the backend 568 return false; 569 } 570 571 /** 572 * Writes the Response to the cache. 573 * 574 * @throws \Exception 575 */ 576 protected function store(Request $request, Response $response) 577 { 578 if (!$response->headers->has('Date')) { 579 $response->setDate(\DateTime::createFromFormat('U', time())); 580 } 581 try { 582 $this->store->write($request, $response); 583 584 $this->record($request, 'store'); 585 586 $response->headers->set('Age', $response->getAge()); 587 } catch (\Exception $e) { 588 $this->record($request, 'store-failed'); 589 590 if ($this->options['debug']) { 591 throw $e; 592 } 593 } 594 595 // now that the response is cached, release the lock 596 $this->store->unlock($request); 597 } 598 599 /** 600 * Restores the Response body. 601 */ 602 private function restoreResponseBody(Request $request, Response $response) 603 { 604 if ($response->headers->has('X-Body-Eval')) { 605 ob_start(); 606 607 if ($response->headers->has('X-Body-File')) { 608 include $response->headers->get('X-Body-File'); 609 } else { 610 eval('; ?>'.$response->getContent().'<?php ;'); 611 } 612 613 $response->setContent(ob_get_clean()); 614 $response->headers->remove('X-Body-Eval'); 615 if (!$response->headers->has('Transfer-Encoding')) { 616 $response->headers->set('Content-Length', \strlen($response->getContent())); 617 } 618 } elseif ($response->headers->has('X-Body-File')) { 619 // Response does not include possibly dynamic content (ESI, SSI), so we need 620 // not handle the content for HEAD requests 621 if (!$request->isMethod('HEAD')) { 622 $response->setContent(file_get_contents($response->headers->get('X-Body-File'))); 623 } 624 } else { 625 return; 626 } 627 628 $response->headers->remove('X-Body-File'); 629 } 630 631 protected function processResponseBody(Request $request, Response $response) 632 { 633 if (null !== $this->surrogate && $this->surrogate->needsParsing($response)) { 634 $this->surrogate->process($request, $response); 635 } 636 } 637 638 /** 639 * Checks if the Request includes authorization or other sensitive information 640 * that should cause the Response to be considered private by default. 641 * 642 * @return bool true if the Request is private, false otherwise 643 */ 644 private function isPrivateRequest(Request $request) 645 { 646 foreach ($this->options['private_headers'] as $key) { 647 $key = strtolower(str_replace('HTTP_', '', $key)); 648 649 if ('cookie' === $key) { 650 if (\count($request->cookies->all())) { 651 return true; 652 } 653 } elseif ($request->headers->has($key)) { 654 return true; 655 } 656 } 657 658 return false; 659 } 660 661 /** 662 * Records that an event took place. 663 * 664 * @param Request $request A Request instance 665 * @param string $event The event name 666 */ 667 private function record(Request $request, $event) 668 { 669 $path = $request->getPathInfo(); 670 if ($qs = $request->getQueryString()) { 671 $path .= '?'.$qs; 672 } 673 $this->traces[$request->getMethod().' '.$path][] = $event; 674 } 675 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Nov 11 20:33:01 2020 | Cross-referenced by PHPXref 0.7.1 |