[ Index ] |
PHP Cross Reference of phpBB-3.1.12-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\HttpKernel\HttpKernelInterface; 19 use Symfony\Component\HttpKernel\TerminableInterface; 20 use Symfony\Component\HttpFoundation\Request; 21 use Symfony\Component\HttpFoundation\Response; 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 $esi; 34 private $esiCacheStrategy; 35 private $traces; 36 37 /** 38 * Constructor. 39 * 40 * The available options are: 41 * 42 * * debug: If true, the traces are added as a HTTP header to ease debugging 43 * 44 * * default_ttl The number of seconds that a cache entry should be considered 45 * fresh when no explicit freshness information is provided in 46 * a response. Explicit Cache-Control or Expires headers 47 * override this value. (default: 0) 48 * 49 * * private_headers Set of request headers that trigger "private" cache-control behavior 50 * on responses that don't explicitly state whether the response is 51 * public or private via a Cache-Control directive. (default: Authorization and Cookie) 52 * 53 * * allow_reload Specifies whether the client can force a cache reload by including a 54 * Cache-Control "no-cache" directive in the request. Set it to ``true`` 55 * for compliance with RFC 2616. (default: false) 56 * 57 * * allow_revalidate Specifies whether the client can force a cache revalidate by including 58 * a Cache-Control "max-age=0" directive in the request. Set it to ``true`` 59 * for compliance with RFC 2616. (default: false) 60 * 61 * * stale_while_revalidate Specifies the default number of seconds (the granularity is the second as the 62 * Response TTL precision is a second) during which the cache can immediately return 63 * a stale response while it revalidates it in the background (default: 2). 64 * This setting is overridden by the stale-while-revalidate HTTP Cache-Control 65 * extension (see RFC 5861). 66 * 67 * * stale_if_error Specifies the default number of seconds (the granularity is the second) during which 68 * the cache can serve a stale response when an error is encountered (default: 60). 69 * This setting is overridden by the stale-if-error HTTP Cache-Control extension 70 * (see RFC 5861). 71 * 72 * @param HttpKernelInterface $kernel An HttpKernelInterface instance 73 * @param StoreInterface $store A Store instance 74 * @param Esi $esi An Esi instance 75 * @param array $options An array of options 76 */ 77 public function __construct(HttpKernelInterface $kernel, StoreInterface $store, Esi $esi = null, array $options = array()) 78 { 79 $this->store = $store; 80 $this->kernel = $kernel; 81 82 // needed in case there is a fatal error because the backend is too slow to respond 83 register_shutdown_function(array($this->store, 'cleanup')); 84 85 $this->options = array_merge(array( 86 'debug' => false, 87 'default_ttl' => 0, 88 'private_headers' => array('Authorization', 'Cookie'), 89 'allow_reload' => false, 90 'allow_revalidate' => false, 91 'stale_while_revalidate' => 2, 92 'stale_if_error' => 60, 93 ), $options); 94 $this->esi = $esi; 95 $this->traces = array(); 96 } 97 98 /** 99 * Gets the current store. 100 * 101 * @return StoreInterface $store A StoreInterface instance 102 */ 103 public function getStore() 104 { 105 return $this->store; 106 } 107 108 /** 109 * Returns an array of events that took place during processing of the last request. 110 * 111 * @return array An array of events 112 */ 113 public function getTraces() 114 { 115 return $this->traces; 116 } 117 118 /** 119 * Returns a log message for the events of the last request processing. 120 * 121 * @return string A log message 122 */ 123 public function getLog() 124 { 125 $log = array(); 126 foreach ($this->traces as $request => $traces) { 127 $log[] = sprintf('%s: %s', $request, implode(', ', $traces)); 128 } 129 130 return implode('; ', $log); 131 } 132 133 /** 134 * Gets the Request instance associated with the master request. 135 * 136 * @return Request A Request instance 137 */ 138 public function getRequest() 139 { 140 return $this->request; 141 } 142 143 /** 144 * Gets the Kernel instance. 145 * 146 * @return HttpKernelInterface An HttpKernelInterface instance 147 */ 148 public function getKernel() 149 { 150 return $this->kernel; 151 } 152 153 /** 154 * Gets the Esi instance. 155 * 156 * @return Esi An Esi instance 157 */ 158 public function getEsi() 159 { 160 return $this->esi; 161 } 162 163 /** 164 * {@inheritdoc} 165 */ 166 public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true) 167 { 168 // FIXME: catch exceptions and implement a 500 error page here? -> in Varnish, there is a built-in error page mechanism 169 if (HttpKernelInterface::MASTER_REQUEST === $type) { 170 $this->traces = array(); 171 $this->request = $request; 172 if (null !== $this->esi) { 173 $this->esiCacheStrategy = $this->esi->createCacheStrategy(); 174 } 175 } 176 177 $path = $request->getPathInfo(); 178 if ($qs = $request->getQueryString()) { 179 $path .= '?'.$qs; 180 } 181 $this->traces[$request->getMethod().' '.$path] = array(); 182 183 if (!$request->isMethodSafe()) { 184 $response = $this->invalidate($request, $catch); 185 } elseif ($request->headers->has('expect')) { 186 $response = $this->pass($request, $catch); 187 } else { 188 $response = $this->lookup($request, $catch); 189 } 190 191 $this->restoreResponseBody($request, $response); 192 193 $response->setDate(\DateTime::createFromFormat('U', time(), new \DateTimeZone('UTC'))); 194 195 if (HttpKernelInterface::MASTER_REQUEST === $type && $this->options['debug']) { 196 $response->headers->set('X-Symfony-Cache', $this->getLog()); 197 } 198 199 if (null !== $this->esi) { 200 if (HttpKernelInterface::MASTER_REQUEST === $type) { 201 $this->esiCacheStrategy->update($response); 202 } else { 203 $this->esiCacheStrategy->add($response); 204 } 205 } 206 207 $response->prepare($request); 208 209 $response->isNotModified($request); 210 211 return $response; 212 } 213 214 /** 215 * {@inheritdoc} 216 */ 217 public function terminate(Request $request, Response $response) 218 { 219 if ($this->getKernel() instanceof TerminableInterface) { 220 $this->getKernel()->terminate($request, $response); 221 } 222 } 223 224 /** 225 * Forwards the Request to the backend without storing the Response in the cache. 226 * 227 * @param Request $request A Request instance 228 * @param bool $catch Whether to process exceptions 229 * 230 * @return Response A Response instance 231 */ 232 protected function pass(Request $request, $catch = false) 233 { 234 $this->record($request, 'pass'); 235 236 return $this->forward($request, $catch); 237 } 238 239 /** 240 * Invalidates non-safe methods (like POST, PUT, and DELETE). 241 * 242 * @param Request $request A Request instance 243 * @param bool $catch Whether to process exceptions 244 * 245 * @return Response A Response instance 246 * 247 * @throws \Exception 248 * 249 * @see RFC2616 13.10 250 */ 251 protected function invalidate(Request $request, $catch = false) 252 { 253 $response = $this->pass($request, $catch); 254 255 // invalidate only when the response is successful 256 if ($response->isSuccessful() || $response->isRedirect()) { 257 try { 258 $this->store->invalidate($request); 259 260 // As per the RFC, invalidate Location and Content-Location URLs if present 261 foreach (array('Location', 'Content-Location') as $header) { 262 if ($uri = $response->headers->get($header)) { 263 $subRequest = $request::create($uri, 'get', array(), array(), array(), $request->server->all()); 264 265 $this->store->invalidate($subRequest); 266 } 267 } 268 269 $this->record($request, 'invalidate'); 270 } catch (\Exception $e) { 271 $this->record($request, 'invalidate-failed'); 272 273 if ($this->options['debug']) { 274 throw $e; 275 } 276 } 277 } 278 279 return $response; 280 } 281 282 /** 283 * Lookups a Response from the cache for the given Request. 284 * 285 * When a matching cache entry is found and is fresh, it uses it as the 286 * response without forwarding any request to the backend. When a matching 287 * cache entry is found but is stale, it attempts to "validate" the entry with 288 * the backend using conditional GET. When no matching cache entry is found, 289 * it triggers "miss" processing. 290 * 291 * @param Request $request A Request instance 292 * @param bool $catch whether to process exceptions 293 * 294 * @return Response A Response instance 295 * 296 * @throws \Exception 297 */ 298 protected function lookup(Request $request, $catch = false) 299 { 300 // if allow_reload and no-cache Cache-Control, allow a cache reload 301 if ($this->options['allow_reload'] && $request->isNoCache()) { 302 $this->record($request, 'reload'); 303 304 return $this->fetch($request, $catch); 305 } 306 307 try { 308 $entry = $this->store->lookup($request); 309 } catch (\Exception $e) { 310 $this->record($request, 'lookup-failed'); 311 312 if ($this->options['debug']) { 313 throw $e; 314 } 315 316 return $this->pass($request, $catch); 317 } 318 319 if (null === $entry) { 320 $this->record($request, 'miss'); 321 322 return $this->fetch($request, $catch); 323 } 324 325 if (!$this->isFreshEnough($request, $entry)) { 326 $this->record($request, 'stale'); 327 328 return $this->validate($request, $entry, $catch); 329 } 330 331 $this->record($request, 'fresh'); 332 333 $entry->headers->set('Age', $entry->getAge()); 334 335 return $entry; 336 } 337 338 /** 339 * Validates that a cache entry is fresh. 340 * 341 * The original request is used as a template for a conditional 342 * GET request with the backend. 343 * 344 * @param Request $request A Request instance 345 * @param Response $entry A Response instance to validate 346 * @param bool $catch Whether to process exceptions 347 * 348 * @return Response A Response instance 349 */ 350 protected function validate(Request $request, Response $entry, $catch = false) 351 { 352 $subRequest = clone $request; 353 354 // send no head requests because we want content 355 $subRequest->setMethod('GET'); 356 357 // add our cached last-modified validator 358 $subRequest->headers->set('if_modified_since', $entry->headers->get('Last-Modified')); 359 360 // Add our cached etag validator to the environment. 361 // We keep the etags from the client to handle the case when the client 362 // has a different private valid entry which is not cached here. 363 $cachedEtags = $entry->getEtag() ? array($entry->getEtag()) : array(); 364 $requestEtags = $request->getETags(); 365 if ($etags = array_unique(array_merge($cachedEtags, $requestEtags))) { 366 $subRequest->headers->set('if_none_match', implode(', ', $etags)); 367 } 368 369 $response = $this->forward($subRequest, $catch, $entry); 370 371 if (304 == $response->getStatusCode()) { 372 $this->record($request, 'valid'); 373 374 // return the response and not the cache entry if the response is valid but not cached 375 $etag = $response->getEtag(); 376 if ($etag && in_array($etag, $requestEtags) && !in_array($etag, $cachedEtags)) { 377 return $response; 378 } 379 380 $entry = clone $entry; 381 $entry->headers->remove('Date'); 382 383 foreach (array('Date', 'Expires', 'Cache-Control', 'ETag', 'Last-Modified') as $name) { 384 if ($response->headers->has($name)) { 385 $entry->headers->set($name, $response->headers->get($name)); 386 } 387 } 388 389 $response = $entry; 390 } else { 391 $this->record($request, 'invalid'); 392 } 393 394 if ($response->isCacheable()) { 395 $this->store($request, $response); 396 } 397 398 return $response; 399 } 400 401 /** 402 * Forwards the Request to the backend and determines whether the response should be stored. 403 * 404 * This methods is triggered when the cache missed or a reload is required. 405 * 406 * @param Request $request A Request instance 407 * @param bool $catch whether to process exceptions 408 * 409 * @return Response A Response instance 410 */ 411 protected function fetch(Request $request, $catch = false) 412 { 413 $subRequest = clone $request; 414 415 // send no head requests because we want content 416 $subRequest->setMethod('GET'); 417 418 // avoid that the backend sends no content 419 $subRequest->headers->remove('if_modified_since'); 420 $subRequest->headers->remove('if_none_match'); 421 422 $response = $this->forward($subRequest, $catch); 423 424 if ($response->isCacheable()) { 425 $this->store($request, $response); 426 } 427 428 return $response; 429 } 430 431 /** 432 * Forwards the Request to the backend and returns the Response. 433 * 434 * @param Request $request A Request instance 435 * @param bool $catch Whether to catch exceptions or not 436 * @param Response $entry A Response instance (the stale entry if present, null otherwise) 437 * 438 * @return Response A Response instance 439 */ 440 protected function forward(Request $request, $catch = false, Response $entry = null) 441 { 442 if ($this->esi) { 443 $this->esi->addSurrogateEsiCapability($request); 444 } 445 446 // modify the X-Forwarded-For header if needed 447 $forwardedFor = $request->headers->get('X-Forwarded-For'); 448 if ($forwardedFor) { 449 $request->headers->set('X-Forwarded-For', $forwardedFor.', '.$request->server->get('REMOTE_ADDR')); 450 } else { 451 $request->headers->set('X-Forwarded-For', $request->server->get('REMOTE_ADDR')); 452 } 453 454 // fix the client IP address by setting it to 127.0.0.1 as HttpCache 455 // is always called from the same process as the backend. 456 $request->server->set('REMOTE_ADDR', '127.0.0.1'); 457 458 // make sure HttpCache is a trusted proxy 459 if (!in_array('127.0.0.1', $trustedProxies = Request::getTrustedProxies())) { 460 $trustedProxies[] = '127.0.0.1'; 461 Request::setTrustedProxies($trustedProxies); 462 } 463 464 // always a "master" request (as the real master request can be in cache) 465 $response = $this->kernel->handle($request, HttpKernelInterface::MASTER_REQUEST, $catch); 466 // FIXME: we probably need to also catch exceptions if raw === true 467 468 // we don't implement the stale-if-error on Requests, which is nonetheless part of the RFC 469 if (null !== $entry && in_array($response->getStatusCode(), array(500, 502, 503, 504))) { 470 if (null === $age = $entry->headers->getCacheControlDirective('stale-if-error')) { 471 $age = $this->options['stale_if_error']; 472 } 473 474 if (abs($entry->getTtl()) < $age) { 475 $this->record($request, 'stale-if-error'); 476 477 return $entry; 478 } 479 } 480 481 $this->processResponseBody($request, $response); 482 483 if ($this->isPrivateRequest($request) && !$response->headers->hasCacheControlDirective('public')) { 484 $response->setPrivate(); 485 } elseif ($this->options['default_ttl'] > 0 && null === $response->getTtl() && !$response->headers->getCacheControlDirective('must-revalidate')) { 486 $response->setTtl($this->options['default_ttl']); 487 } 488 489 return $response; 490 } 491 492 /** 493 * Checks whether the cache entry is "fresh enough" to satisfy the Request. 494 * 495 * @param Request $request A Request instance 496 * @param Response $entry A Response instance 497 * 498 * @return bool true if the cache entry if fresh enough, false otherwise 499 */ 500 protected function isFreshEnough(Request $request, Response $entry) 501 { 502 if (!$entry->isFresh()) { 503 return $this->lock($request, $entry); 504 } 505 506 if ($this->options['allow_revalidate'] && null !== $maxAge = $request->headers->getCacheControlDirective('max-age')) { 507 return $maxAge > 0 && $maxAge >= $entry->getAge(); 508 } 509 510 return true; 511 } 512 513 /** 514 * Locks a Request during the call to the backend. 515 * 516 * @param Request $request A Request instance 517 * @param Response $entry A Response instance 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 * @param Request $request A Request instance 575 * @param Response $response A Response instance 576 * 577 * @throws \Exception 578 */ 579 protected function store(Request $request, Response $response) 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 * @param Request $request A Request instance 603 * @param Response $response A Response instance 604 */ 605 private function restoreResponseBody(Request $request, Response $response) 606 { 607 if ($request->isMethod('HEAD') || 304 === $response->getStatusCode()) { 608 $response->setContent(null); 609 $response->headers->remove('X-Body-Eval'); 610 $response->headers->remove('X-Body-File'); 611 612 return; 613 } 614 615 if ($response->headers->has('X-Body-Eval')) { 616 ob_start(); 617 618 if ($response->headers->has('X-Body-File')) { 619 include $response->headers->get('X-Body-File'); 620 } else { 621 eval('; ?>'.$response->getContent().'<?php ;'); 622 } 623 624 $response->setContent(ob_get_clean()); 625 $response->headers->remove('X-Body-Eval'); 626 if (!$response->headers->has('Transfer-Encoding')) { 627 $response->headers->set('Content-Length', strlen($response->getContent())); 628 } 629 } elseif ($response->headers->has('X-Body-File')) { 630 $response->setContent(file_get_contents($response->headers->get('X-Body-File'))); 631 } else { 632 return; 633 } 634 635 $response->headers->remove('X-Body-File'); 636 } 637 638 protected function processResponseBody(Request $request, Response $response) 639 { 640 if (null !== $this->esi && $this->esi->needsEsiParsing($response)) { 641 $this->esi->process($request, $response); 642 } 643 } 644 645 /** 646 * Checks if the Request includes authorization or other sensitive information 647 * that should cause the Response to be considered private by default. 648 * 649 * @param Request $request A Request instance 650 * 651 * @return bool true if the Request is private, false otherwise 652 */ 653 private function isPrivateRequest(Request $request) 654 { 655 foreach ($this->options['private_headers'] as $key) { 656 $key = strtolower(str_replace('HTTP_', '', $key)); 657 658 if ('cookie' === $key) { 659 if (count($request->cookies->all())) { 660 return true; 661 } 662 } elseif ($request->headers->has($key)) { 663 return true; 664 } 665 } 666 667 return false; 668 } 669 670 /** 671 * Records that an event took place. 672 * 673 * @param Request $request A Request instance 674 * @param string $event The event name 675 */ 676 private function record(Request $request, $event) 677 { 678 $path = $request->getPathInfo(); 679 if ($qs = $request->getQueryString()) { 680 $path .= '?'.$qs; 681 } 682 $this->traces[$request->getMethod().' '.$path][] = $event; 683 } 684 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Jan 11 00:25:41 2018 | Cross-referenced by PHPXref 0.7.1 |