comparison vendor/symfony/http-kernel/HttpCache/HttpCache.php @ 14:1fec387a4317

Update Drupal core to 8.5.2 via Composer
author Chris Cannam
date Mon, 23 Apr 2018 09:46:53 +0100
parents 4c8ae668cc8c
children c2387f117808
comparison
equal deleted inserted replaced
13:5fb285c0d0e3 14:1fec387a4317
67 * 67 *
68 * * stale_if_error Specifies the default number of seconds (the granularity is the second) during which 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). 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 70 * This setting is overridden by the stale-if-error HTTP Cache-Control extension
71 * (see RFC 5861). 71 * (see RFC 5861).
72 *
73 * @param HttpKernelInterface $kernel An HttpKernelInterface instance
74 * @param StoreInterface $store A Store instance
75 * @param SurrogateInterface $surrogate A SurrogateInterface instance
76 * @param array $options An array of options
77 */ 72 */
78 public function __construct(HttpKernelInterface $kernel, StoreInterface $store, SurrogateInterface $surrogate = null, array $options = array()) 73 public function __construct(HttpKernelInterface $kernel, StoreInterface $store, SurrogateInterface $surrogate = null, array $options = array())
79 { 74 {
80 $this->store = $store; 75 $this->store = $store;
81 $this->kernel = $kernel; 76 $this->kernel = $kernel;
174 if (null !== $this->surrogate) { 169 if (null !== $this->surrogate) {
175 $this->surrogateCacheStrategy = $this->surrogate->createCacheStrategy(); 170 $this->surrogateCacheStrategy = $this->surrogate->createCacheStrategy();
176 } 171 }
177 } 172 }
178 173
179 $path = $request->getPathInfo(); 174 $this->traces[$this->getTraceKey($request)] = array();
180 if ($qs = $request->getQueryString()) {
181 $path .= '?'.$qs;
182 }
183 $this->traces[$request->getMethod().' '.$path] = array();
184 175
185 if (!$request->isMethodSafe(false)) { 176 if (!$request->isMethodSafe(false)) {
186 $response = $this->invalidate($request, $catch); 177 $response = $this->invalidate($request, $catch);
187 } elseif ($request->headers->has('expect') || !$request->isMethodCacheable()) { 178 } elseif ($request->headers->has('expect') || !$request->isMethodCacheable()) {
188 $response = $this->pass($request, $catch); 179 $response = $this->pass($request, $catch);
180 } elseif ($this->options['allow_reload'] && $request->isNoCache()) {
181 /*
182 If allow_reload is configured and the client requests "Cache-Control: no-cache",
183 reload the cache by fetching a fresh response and caching it (if possible).
184 */
185 $this->record($request, 'reload');
186 $response = $this->fetch($request, $catch);
189 } else { 187 } else {
190 $response = $this->lookup($request, $catch); 188 $response = $this->lookup($request, $catch);
191 } 189 }
192 190
193 $this->restoreResponseBody($request, $response); 191 $this->restoreResponseBody($request, $response);
194
195 $response->setDate(\DateTime::createFromFormat('U', time(), new \DateTimeZone('UTC')));
196 192
197 if (HttpKernelInterface::MASTER_REQUEST === $type && $this->options['debug']) { 193 if (HttpKernelInterface::MASTER_REQUEST === $type && $this->options['debug']) {
198 $response->headers->set('X-Symfony-Cache', $this->getLog()); 194 $response->headers->set('X-Symfony-Cache', $this->getLog());
199 } 195 }
200 196
289 * cache entry is found but is stale, it attempts to "validate" the entry with 285 * 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, 286 * the backend using conditional GET. When no matching cache entry is found,
291 * it triggers "miss" processing. 287 * it triggers "miss" processing.
292 * 288 *
293 * @param Request $request A Request instance 289 * @param Request $request A Request instance
294 * @param bool $catch whether to process exceptions 290 * @param bool $catch Whether to process exceptions
295 * 291 *
296 * @return Response A Response instance 292 * @return Response A Response instance
297 * 293 *
298 * @throws \Exception 294 * @throws \Exception
299 */ 295 */
300 protected function lookup(Request $request, $catch = false) 296 protected function lookup(Request $request, $catch = false)
301 { 297 {
302 // if allow_reload and no-cache Cache-Control, allow a cache reload
303 if ($this->options['allow_reload'] && $request->isNoCache()) {
304 $this->record($request, 'reload');
305
306 return $this->fetch($request, $catch);
307 }
308
309 try { 298 try {
310 $entry = $this->store->lookup($request); 299 $entry = $this->store->lookup($request);
311 } catch (\Exception $e) { 300 } catch (\Exception $e) {
312 $this->record($request, 'lookup-failed'); 301 $this->record($request, 'lookup-failed');
313 302
401 390
402 return $response; 391 return $response;
403 } 392 }
404 393
405 /** 394 /**
406 * Forwards the Request to the backend and determines whether the response should be stored. 395 * Unconditionally fetches a fresh response from the backend and
407 * 396 * stores it in the cache if is cacheable.
408 * This methods is triggered when the cache missed or a reload is required.
409 * 397 *
410 * @param Request $request A Request instance 398 * @param Request $request A Request instance
411 * @param bool $catch whether to process exceptions 399 * @param bool $catch Whether to process exceptions
412 * 400 *
413 * @return Response A Response instance 401 * @return Response A Response instance
414 */ 402 */
415 protected function fetch(Request $request, $catch = false) 403 protected function fetch(Request $request, $catch = false)
416 { 404 {
434 return $response; 422 return $response;
435 } 423 }
436 424
437 /** 425 /**
438 * Forwards the Request to the backend and returns the Response. 426 * Forwards the Request to the backend and returns the Response.
427 *
428 * All backend requests (cache passes, fetches, cache validations)
429 * run through this method.
439 * 430 *
440 * @param Request $request A Request instance 431 * @param Request $request A Request instance
441 * @param bool $catch Whether to catch exceptions or not 432 * @param bool $catch Whether to catch exceptions or not
442 * @param Response $entry A Response instance (the stale entry if present, null otherwise) 433 * @param Response $entry A Response instance (the stale entry if present, null otherwise)
443 * 434 *
462 $request->server->set('REMOTE_ADDR', '127.0.0.1'); 453 $request->server->set('REMOTE_ADDR', '127.0.0.1');
463 454
464 // make sure HttpCache is a trusted proxy 455 // make sure HttpCache is a trusted proxy
465 if (!in_array('127.0.0.1', $trustedProxies = Request::getTrustedProxies())) { 456 if (!in_array('127.0.0.1', $trustedProxies = Request::getTrustedProxies())) {
466 $trustedProxies[] = '127.0.0.1'; 457 $trustedProxies[] = '127.0.0.1';
467 Request::setTrustedProxies($trustedProxies, method_exists('Request', 'getTrustedHeaderSet') ? Request::getTrustedHeaderSet() : -1); 458 Request::setTrustedProxies($trustedProxies, Request::HEADER_X_FORWARDED_ALL);
468 } 459 }
469 460
470 // always a "master" request (as the real master request can be in cache) 461 // always a "master" request (as the real master request can be in cache)
471 $response = $this->kernel->handle($request, HttpKernelInterface::MASTER_REQUEST, $catch); 462 $response = $this->kernel->handle($request, HttpKernelInterface::MASTER_REQUEST, $catch);
472 // FIXME: we probably need to also catch exceptions if raw === true 463 // FIXME: we probably need to also catch exceptions if raw === true
482 473
483 return $entry; 474 return $entry;
484 } 475 }
485 } 476 }
486 477
478 /*
479 RFC 7231 Sect. 7.1.1.2 says that a server that does not have a reasonably accurate
480 clock MUST NOT send a "Date" header, although it MUST send one in most other cases
481 except for 1xx or 5xx responses where it MAY do so.
482
483 Anyway, a client that received a message without a "Date" header MUST add it.
484 */
485 if (!$response->headers->has('Date')) {
486 $response->setDate(\DateTime::createFromFormat('U', time()));
487 }
488
487 $this->processResponseBody($request, $response); 489 $this->processResponseBody($request, $response);
488 490
489 if ($this->isPrivateRequest($request) && !$response->headers->hasCacheControlDirective('public')) { 491 if ($this->isPrivateRequest($request) && !$response->headers->hasCacheControlDirective('public')) {
490 $response->setPrivate(); 492 $response->setPrivate();
491 } elseif ($this->options['default_ttl'] > 0 && null === $response->getTtl() && !$response->headers->getCacheControlDirective('must-revalidate')) { 493 } elseif ($this->options['default_ttl'] > 0 && null === $response->getTtl() && !$response->headers->getCacheControlDirective('must-revalidate')) {
496 } 498 }
497 499
498 /** 500 /**
499 * Checks whether the cache entry is "fresh enough" to satisfy the Request. 501 * Checks whether the cache entry is "fresh enough" to satisfy the Request.
500 * 502 *
501 * @param Request $request A Request instance
502 * @param Response $entry A Response instance
503 *
504 * @return bool true if the cache entry if fresh enough, false otherwise 503 * @return bool true if the cache entry if fresh enough, false otherwise
505 */ 504 */
506 protected function isFreshEnough(Request $request, Response $entry) 505 protected function isFreshEnough(Request $request, Response $entry)
507 { 506 {
508 if (!$entry->isFresh()) { 507 if (!$entry->isFresh()) {
517 } 516 }
518 517
519 /** 518 /**
520 * Locks a Request during the call to the backend. 519 * Locks a Request during the call to the backend.
521 * 520 *
522 * @param Request $request A Request instance
523 * @param Response $entry A Response instance
524 *
525 * @return bool true if the cache entry can be returned even if it is staled, false otherwise 521 * @return bool true if the cache entry can be returned even if it is staled, false otherwise
526 */ 522 */
527 protected function lock(Request $request, Response $entry) 523 protected function lock(Request $request, Response $entry)
528 { 524 {
529 // try to acquire a lock to call the backend 525 // try to acquire a lock to call the backend
530 $lock = $this->store->lock($request); 526 $lock = $this->store->lock($request);
531 527
528 if (true === $lock) {
529 // we have the lock, call the backend
530 return false;
531 }
532
532 // there is already another process calling the backend 533 // there is already another process calling the backend
533 if (true !== $lock) { 534
534 // check if we can serve the stale entry 535 // May we serve a stale response?
535 if (null === $age = $entry->headers->getCacheControlDirective('stale-while-revalidate')) { 536 if ($this->mayServeStaleWhileRevalidate($entry)) {
536 $age = $this->options['stale_while_revalidate']; 537 $this->record($request, 'stale-while-revalidate');
537 }
538
539 if (abs($entry->getTtl()) < $age) {
540 $this->record($request, 'stale-while-revalidate');
541
542 // server the stale response while there is a revalidation
543 return true;
544 }
545
546 // wait for the lock to be released
547 $wait = 0;
548 while ($this->store->isLocked($request) && $wait < 5000000) {
549 usleep(50000);
550 $wait += 50000;
551 }
552
553 if ($wait < 5000000) {
554 // replace the current entry with the fresh one
555 $new = $this->lookup($request);
556 $entry->headers = $new->headers;
557 $entry->setContent($new->getContent());
558 $entry->setStatusCode($new->getStatusCode());
559 $entry->setProtocolVersion($new->getProtocolVersion());
560 foreach ($new->headers->getCookies() as $cookie) {
561 $entry->headers->setCookie($cookie);
562 }
563 } else {
564 // backend is slow as hell, send a 503 response (to avoid the dog pile effect)
565 $entry->setStatusCode(503);
566 $entry->setContent('503 Service Unavailable');
567 $entry->headers->set('Retry-After', 10);
568 }
569 538
570 return true; 539 return true;
571 } 540 }
572 541
573 // we have the lock, call the backend 542 // wait for the lock to be released
574 return false; 543 if ($this->waitForLock($request)) {
544 // replace the current entry with the fresh one
545 $new = $this->lookup($request);
546 $entry->headers = $new->headers;
547 $entry->setContent($new->getContent());
548 $entry->setStatusCode($new->getStatusCode());
549 $entry->setProtocolVersion($new->getProtocolVersion());
550 foreach ($new->headers->getCookies() as $cookie) {
551 $entry->headers->setCookie($cookie);
552 }
553 } else {
554 // backend is slow as hell, send a 503 response (to avoid the dog pile effect)
555 $entry->setStatusCode(503);
556 $entry->setContent('503 Service Unavailable');
557 $entry->headers->set('Retry-After', 10);
558 }
559
560 return true;
575 } 561 }
576 562
577 /** 563 /**
578 * Writes the Response to the cache. 564 * Writes the Response to the cache.
579 * 565 *
580 * @param Request $request A Request instance
581 * @param Response $response A Response instance
582 *
583 * @throws \Exception 566 * @throws \Exception
584 */ 567 */
585 protected function store(Request $request, Response $response) 568 protected function store(Request $request, Response $response)
586 { 569 {
587 if (!$response->headers->has('Date')) {
588 $response->setDate(\DateTime::createFromFormat('U', time()));
589 }
590 try { 570 try {
591 $this->store->write($request, $response); 571 $this->store->write($request, $response);
592 572
593 $this->record($request, 'store'); 573 $this->record($request, 'store');
594 574
605 $this->store->unlock($request); 585 $this->store->unlock($request);
606 } 586 }
607 587
608 /** 588 /**
609 * Restores the Response body. 589 * Restores the Response body.
610 *
611 * @param Request $request A Request instance
612 * @param Response $response A Response instance
613 */ 590 */
614 private function restoreResponseBody(Request $request, Response $response) 591 private function restoreResponseBody(Request $request, Response $response)
615 { 592 {
616 if ($request->isMethod('HEAD') || 304 === $response->getStatusCode()) {
617 $response->setContent(null);
618 $response->headers->remove('X-Body-Eval');
619 $response->headers->remove('X-Body-File');
620
621 return;
622 }
623
624 if ($response->headers->has('X-Body-Eval')) { 593 if ($response->headers->has('X-Body-Eval')) {
625 ob_start(); 594 ob_start();
626 595
627 if ($response->headers->has('X-Body-File')) { 596 if ($response->headers->has('X-Body-File')) {
628 include $response->headers->get('X-Body-File'); 597 include $response->headers->get('X-Body-File');
634 $response->headers->remove('X-Body-Eval'); 603 $response->headers->remove('X-Body-Eval');
635 if (!$response->headers->has('Transfer-Encoding')) { 604 if (!$response->headers->has('Transfer-Encoding')) {
636 $response->headers->set('Content-Length', strlen($response->getContent())); 605 $response->headers->set('Content-Length', strlen($response->getContent()));
637 } 606 }
638 } elseif ($response->headers->has('X-Body-File')) { 607 } elseif ($response->headers->has('X-Body-File')) {
639 $response->setContent(file_get_contents($response->headers->get('X-Body-File'))); 608 // Response does not include possibly dynamic content (ESI, SSI), so we need
609 // not handle the content for HEAD requests
610 if (!$request->isMethod('HEAD')) {
611 $response->setContent(file_get_contents($response->headers->get('X-Body-File')));
612 }
640 } else { 613 } else {
641 return; 614 return;
642 } 615 }
643 616
644 $response->headers->remove('X-Body-File'); 617 $response->headers->remove('X-Body-File');
652 } 625 }
653 626
654 /** 627 /**
655 * Checks if the Request includes authorization or other sensitive information 628 * Checks if the Request includes authorization or other sensitive information
656 * that should cause the Response to be considered private by default. 629 * that should cause the Response to be considered private by default.
657 *
658 * @param Request $request A Request instance
659 * 630 *
660 * @return bool true if the Request is private, false otherwise 631 * @return bool true if the Request is private, false otherwise
661 */ 632 */
662 private function isPrivateRequest(Request $request) 633 private function isPrivateRequest(Request $request)
663 { 634 {
682 * @param Request $request A Request instance 653 * @param Request $request A Request instance
683 * @param string $event The event name 654 * @param string $event The event name
684 */ 655 */
685 private function record(Request $request, $event) 656 private function record(Request $request, $event)
686 { 657 {
658 $this->traces[$this->getTraceKey($request)][] = $event;
659 }
660
661 /**
662 * Calculates the key we use in the "trace" array for a given request.
663 *
664 * @param Request $request
665 *
666 * @return string
667 */
668 private function getTraceKey(Request $request)
669 {
687 $path = $request->getPathInfo(); 670 $path = $request->getPathInfo();
688 if ($qs = $request->getQueryString()) { 671 if ($qs = $request->getQueryString()) {
689 $path .= '?'.$qs; 672 $path .= '?'.$qs;
690 } 673 }
691 $this->traces[$request->getMethod().' '.$path][] = $event; 674
675 return $request->getMethod().' '.$path;
676 }
677
678 /**
679 * Checks whether the given (cached) response may be served as "stale" when a revalidation
680 * is currently in progress.
681 *
682 * @param Response $entry
683 *
684 * @return bool true when the stale response may be served, false otherwise
685 */
686 private function mayServeStaleWhileRevalidate(Response $entry)
687 {
688 $timeout = $entry->headers->getCacheControlDirective('stale-while-revalidate');
689
690 if (null === $timeout) {
691 $timeout = $this->options['stale_while_revalidate'];
692 }
693
694 return abs($entry->getTtl()) < $timeout;
695 }
696
697 /**
698 * Waits for the store to release a locked entry.
699 *
700 * @param Request $request The request to wait for
701 *
702 * @return bool true if the lock was released before the internal timeout was hit; false if the wait timeout was exceeded
703 */
704 private function waitForLock(Request $request)
705 {
706 $wait = 0;
707 while ($this->store->isLocked($request) && $wait < 100) {
708 usleep(50000);
709 ++$wait;
710 }
711
712 return $wait < 100;
692 } 713 }
693 } 714 }