mirror of
https://gitlab.com/signalytic/client-external/streamline/streamline-emr.git
synced 2026-09-12 03:01:32 +00:00
Import latest app updates from streamline
An updated set of source files and initialization was provided by streamline to address issues observed during initial testing. These files have been updated in order to generate a new set of app images.
This commit is contained in:
-142
@@ -1,142 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpKernel\HttpCache;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
/**
|
||||
* Abstract class implementing Surrogate capabilities to Request and Response instances.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
abstract class AbstractSurrogate implements SurrogateInterface
|
||||
{
|
||||
protected $contentTypes;
|
||||
|
||||
/**
|
||||
* @deprecated since Symfony 6.3
|
||||
*/
|
||||
protected $phpEscapeMap = [
|
||||
['<?', '<%', '<s', '<S'],
|
||||
['<?php echo "<?"; ?>', '<?php echo "<%"; ?>', '<?php echo "<s"; ?>', '<?php echo "<S"; ?>'],
|
||||
];
|
||||
|
||||
/**
|
||||
* @param array $contentTypes An array of content-type that should be parsed for Surrogate information
|
||||
* (default: text/html, text/xml, application/xhtml+xml, and application/xml)
|
||||
*/
|
||||
public function __construct(array $contentTypes = ['text/html', 'text/xml', 'application/xhtml+xml', 'application/xml'])
|
||||
{
|
||||
$this->contentTypes = $contentTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new cache strategy instance.
|
||||
*/
|
||||
public function createCacheStrategy(): ResponseCacheStrategyInterface
|
||||
{
|
||||
return new ResponseCacheStrategy();
|
||||
}
|
||||
|
||||
public function hasSurrogateCapability(Request $request): bool
|
||||
{
|
||||
if (null === $value = $request->headers->get('Surrogate-Capability')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return str_contains($value, sprintf('%s/1.0', strtoupper($this->getName())));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function addSurrogateCapability(Request $request)
|
||||
{
|
||||
$current = $request->headers->get('Surrogate-Capability');
|
||||
$new = sprintf('symfony="%s/1.0"', strtoupper($this->getName()));
|
||||
|
||||
$request->headers->set('Surrogate-Capability', $current ? $current.', '.$new : $new);
|
||||
}
|
||||
|
||||
public function needsParsing(Response $response): bool
|
||||
{
|
||||
if (!$control = $response->headers->get('Surrogate-Control')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pattern = sprintf('#content="[^"]*%s/1.0[^"]*"#', strtoupper($this->getName()));
|
||||
|
||||
return (bool) preg_match($pattern, $control);
|
||||
}
|
||||
|
||||
public function handle(HttpCache $cache, string $uri, string $alt, bool $ignoreErrors): string
|
||||
{
|
||||
$subRequest = Request::create($uri, Request::METHOD_GET, [], $cache->getRequest()->cookies->all(), [], $cache->getRequest()->server->all());
|
||||
|
||||
try {
|
||||
$response = $cache->handle($subRequest, HttpKernelInterface::SUB_REQUEST, true);
|
||||
|
||||
if (!$response->isSuccessful() && Response::HTTP_NOT_MODIFIED !== $response->getStatusCode()) {
|
||||
throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %d).', $subRequest->getUri(), $response->getStatusCode()));
|
||||
}
|
||||
|
||||
return $response->getContent();
|
||||
} catch (\Exception $e) {
|
||||
if ($alt) {
|
||||
return $this->handle($cache, $alt, '', $ignoreErrors);
|
||||
}
|
||||
|
||||
if (!$ignoreErrors) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the Surrogate from the Surrogate-Control header.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function removeFromControl(Response $response)
|
||||
{
|
||||
if (!$response->headers->has('Surrogate-Control')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$value = $response->headers->get('Surrogate-Control');
|
||||
$upperName = strtoupper($this->getName());
|
||||
|
||||
if (sprintf('content="%s/1.0"', $upperName) == $value) {
|
||||
$response->headers->remove('Surrogate-Control');
|
||||
} elseif (preg_match(sprintf('#,\s*content="%s/1.0"#', $upperName), $value)) {
|
||||
$response->headers->set('Surrogate-Control', preg_replace(sprintf('#,\s*content="%s/1.0"#', $upperName), '', $value));
|
||||
} elseif (preg_match(sprintf('#content="%s/1.0",\s*#', $upperName), $value)) {
|
||||
$response->headers->set('Surrogate-Control', preg_replace(sprintf('#content="%s/1.0",\s*#', $upperName), '', $value));
|
||||
}
|
||||
}
|
||||
|
||||
protected static function generateBodyEvalBoundary(): string
|
||||
{
|
||||
static $cookie;
|
||||
$cookie = hash('xxh128', $cookie ?? $cookie = random_bytes(16), true);
|
||||
$boundary = base64_encode($cookie);
|
||||
|
||||
\assert(HttpCache::BODY_EVAL_BOUNDARY_LENGTH === \strlen($boundary));
|
||||
|
||||
return $boundary;
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpKernel\HttpCache;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Esi implements the ESI capabilities to Request and Response instances.
|
||||
*
|
||||
* For more information, read the following W3C notes:
|
||||
*
|
||||
* * ESI Language Specification 1.0 (http://www.w3.org/TR/esi-lang)
|
||||
*
|
||||
* * Edge Architecture Specification (http://www.w3.org/TR/edge-arch)
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class Esi extends AbstractSurrogate
|
||||
{
|
||||
public function getName(): string
|
||||
{
|
||||
return 'esi';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function addSurrogateControl(Response $response)
|
||||
{
|
||||
if (str_contains($response->getContent(), '<esi:include')) {
|
||||
$response->headers->set('Surrogate-Control', 'content="ESI/1.0"');
|
||||
}
|
||||
}
|
||||
|
||||
public function renderIncludeTag(string $uri, ?string $alt = null, bool $ignoreErrors = true, string $comment = ''): string
|
||||
{
|
||||
$html = sprintf('<esi:include src="%s"%s%s />',
|
||||
$uri,
|
||||
$ignoreErrors ? ' onerror="continue"' : '',
|
||||
$alt ? sprintf(' alt="%s"', $alt) : ''
|
||||
);
|
||||
|
||||
if (!empty($comment)) {
|
||||
return sprintf("<esi:comment text=\"%s\" />\n%s", $comment, $html);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function process(Request $request, Response $response): Response
|
||||
{
|
||||
$type = $response->headers->get('Content-Type');
|
||||
if (empty($type)) {
|
||||
$type = 'text/html';
|
||||
}
|
||||
|
||||
$parts = explode(';', $type);
|
||||
if (!\in_array($parts[0], $this->contentTypes)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
// we don't use a proper XML parser here as we can have ESI tags in a plain text response
|
||||
$content = $response->getContent();
|
||||
$content = preg_replace('#<esi\:remove>.*?</esi\:remove>#s', '', $content);
|
||||
$content = preg_replace('#<esi\:comment[^>]+>#s', '', $content);
|
||||
|
||||
$boundary = self::generateBodyEvalBoundary();
|
||||
$chunks = preg_split('#<esi\:include\s+(.*?)\s*(?:/|</esi\:include)>#', $content, -1, \PREG_SPLIT_DELIM_CAPTURE);
|
||||
|
||||
$i = 1;
|
||||
while (isset($chunks[$i])) {
|
||||
$options = [];
|
||||
preg_match_all('/(src|onerror|alt)="([^"]*?)"/', $chunks[$i], $matches, \PREG_SET_ORDER);
|
||||
foreach ($matches as $set) {
|
||||
$options[$set[1]] = $set[2];
|
||||
}
|
||||
|
||||
if (!isset($options['src'])) {
|
||||
throw new \RuntimeException('Unable to process an ESI tag without a "src" attribute.');
|
||||
}
|
||||
|
||||
$chunks[$i] = $boundary.$options['src']."\n".($options['alt'] ?? '')."\n".('continue' === ($options['onerror'] ?? ''))."\n";
|
||||
$i += 2;
|
||||
}
|
||||
$content = $boundary.implode('', $chunks).$boundary;
|
||||
|
||||
$response->setContent($content);
|
||||
$response->headers->set('X-Body-Eval', 'ESI');
|
||||
|
||||
// remove ESI/1.0 from the Surrogate-Control header
|
||||
$this->removeFromControl($response);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -1,758 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This code is partially based on the Rack-Cache library by Ryan Tomayko,
|
||||
* which is released under the MIT license.
|
||||
* (based on commit 02d2b48d75bcb63cf1c0c7149c077ad256542801)
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpKernel\HttpCache;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
use Symfony\Component\HttpKernel\TerminableInterface;
|
||||
|
||||
/**
|
||||
* Cache provides HTTP caching.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class HttpCache implements HttpKernelInterface, TerminableInterface
|
||||
{
|
||||
public const BODY_EVAL_BOUNDARY_LENGTH = 24;
|
||||
|
||||
private HttpKernelInterface $kernel;
|
||||
private StoreInterface $store;
|
||||
private Request $request;
|
||||
private ?SurrogateInterface $surrogate;
|
||||
private ?ResponseCacheStrategyInterface $surrogateCacheStrategy = null;
|
||||
private array $options = [];
|
||||
private array $traces = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* The available options are:
|
||||
*
|
||||
* * debug If true, exceptions are thrown when things go wrong. Otherwise, the cache
|
||||
* will try to carry on and deliver a meaningful response.
|
||||
*
|
||||
* * trace_level May be one of 'none', 'short' and 'full'. For 'short', a concise trace of the
|
||||
* main request will be added as an HTTP header. 'full' will add traces for all
|
||||
* requests (including ESI subrequests). (default: 'full' if in debug; 'none' otherwise)
|
||||
*
|
||||
* * trace_header Header name to use for traces. (default: X-Symfony-Cache)
|
||||
*
|
||||
* * default_ttl The number of seconds that a cache entry should be considered
|
||||
* fresh when no explicit freshness information is provided in
|
||||
* a response. Explicit Cache-Control or Expires headers
|
||||
* override this value. (default: 0)
|
||||
*
|
||||
* * private_headers Set of request headers that trigger "private" cache-control behavior
|
||||
* on responses that don't explicitly state whether the response is
|
||||
* public or private via a Cache-Control directive. (default: Authorization and Cookie)
|
||||
*
|
||||
* * skip_response_headers Set of response headers that are never cached even if a response is cacheable (public).
|
||||
* (default: Set-Cookie)
|
||||
*
|
||||
* * allow_reload Specifies whether the client can force a cache reload by including a
|
||||
* Cache-Control "no-cache" directive in the request. Set it to ``true``
|
||||
* for compliance with RFC 2616. (default: false)
|
||||
*
|
||||
* * allow_revalidate Specifies whether the client can force a cache revalidate by including
|
||||
* a Cache-Control "max-age=0" directive in the request. Set it to ``true``
|
||||
* for compliance with RFC 2616. (default: false)
|
||||
*
|
||||
* * stale_while_revalidate Specifies the default number of seconds (the granularity is the second as the
|
||||
* Response TTL precision is a second) during which the cache can immediately return
|
||||
* a stale response while it revalidates it in the background (default: 2).
|
||||
* This setting is overridden by the stale-while-revalidate HTTP Cache-Control
|
||||
* extension (see RFC 5861).
|
||||
*
|
||||
* * stale_if_error Specifies the default number of seconds (the granularity is the second) during which
|
||||
* the cache can serve a stale response when an error is encountered (default: 60).
|
||||
* This setting is overridden by the stale-if-error HTTP Cache-Control extension
|
||||
* (see RFC 5861).
|
||||
*
|
||||
* * terminate_on_cache_hit Specifies if the kernel.terminate event should be dispatched even when the cache
|
||||
* was hit (default: true).
|
||||
* Unless your application needs to process events on cache hits, it is recommended
|
||||
* to set this to false to avoid having to bootstrap the Symfony framework on a cache hit.
|
||||
*/
|
||||
public function __construct(HttpKernelInterface $kernel, StoreInterface $store, ?SurrogateInterface $surrogate = null, array $options = [])
|
||||
{
|
||||
$this->store = $store;
|
||||
$this->kernel = $kernel;
|
||||
$this->surrogate = $surrogate;
|
||||
|
||||
// needed in case there is a fatal error because the backend is too slow to respond
|
||||
register_shutdown_function($this->store->cleanup(...));
|
||||
|
||||
$this->options = array_merge([
|
||||
'debug' => false,
|
||||
'default_ttl' => 0,
|
||||
'private_headers' => ['Authorization', 'Cookie'],
|
||||
'skip_response_headers' => ['Set-Cookie'],
|
||||
'allow_reload' => false,
|
||||
'allow_revalidate' => false,
|
||||
'stale_while_revalidate' => 2,
|
||||
'stale_if_error' => 60,
|
||||
'trace_level' => 'none',
|
||||
'trace_header' => 'X-Symfony-Cache',
|
||||
'terminate_on_cache_hit' => true,
|
||||
], $options);
|
||||
|
||||
if (!isset($options['trace_level'])) {
|
||||
$this->options['trace_level'] = $this->options['debug'] ? 'full' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current store.
|
||||
*/
|
||||
public function getStore(): StoreInterface
|
||||
{
|
||||
return $this->store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of events that took place during processing of the last request.
|
||||
*/
|
||||
public function getTraces(): array
|
||||
{
|
||||
return $this->traces;
|
||||
}
|
||||
|
||||
private function addTraces(Response $response): void
|
||||
{
|
||||
$traceString = null;
|
||||
|
||||
if ('full' === $this->options['trace_level']) {
|
||||
$traceString = $this->getLog();
|
||||
}
|
||||
|
||||
if ('short' === $this->options['trace_level'] && $masterId = array_key_first($this->traces)) {
|
||||
$traceString = implode('/', $this->traces[$masterId]);
|
||||
}
|
||||
|
||||
if (null !== $traceString) {
|
||||
$response->headers->add([$this->options['trace_header'] => $traceString]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a log message for the events of the last request processing.
|
||||
*/
|
||||
public function getLog(): string
|
||||
{
|
||||
$log = [];
|
||||
foreach ($this->traces as $request => $traces) {
|
||||
$log[] = sprintf('%s: %s', $request, implode(', ', $traces));
|
||||
}
|
||||
|
||||
return implode('; ', $log);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Request instance associated with the main request.
|
||||
*/
|
||||
public function getRequest(): Request
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Kernel instance.
|
||||
*/
|
||||
public function getKernel(): HttpKernelInterface
|
||||
{
|
||||
return $this->kernel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Surrogate instance.
|
||||
*
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function getSurrogate(): SurrogateInterface
|
||||
{
|
||||
return $this->surrogate;
|
||||
}
|
||||
|
||||
public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response
|
||||
{
|
||||
// FIXME: catch exceptions and implement a 500 error page here? -> in Varnish, there is a built-in error page mechanism
|
||||
if (HttpKernelInterface::MAIN_REQUEST === $type) {
|
||||
$this->traces = [];
|
||||
// Keep a clone of the original request for surrogates so they can access it.
|
||||
// We must clone here to get a separate instance because the application will modify the request during
|
||||
// the application flow (we know it always does because we do ourselves by setting REMOTE_ADDR to 127.0.0.1
|
||||
// and adding the X-Forwarded-For header, see HttpCache::forward()).
|
||||
$this->request = clone $request;
|
||||
if (null !== $this->surrogate) {
|
||||
$this->surrogateCacheStrategy = $this->surrogate->createCacheStrategy();
|
||||
}
|
||||
}
|
||||
|
||||
$this->traces[$this->getTraceKey($request)] = [];
|
||||
|
||||
if (!$request->isMethodSafe()) {
|
||||
$response = $this->invalidate($request, $catch);
|
||||
} elseif ($request->headers->has('expect') || !$request->isMethodCacheable()) {
|
||||
$response = $this->pass($request, $catch);
|
||||
} elseif ($this->options['allow_reload'] && $request->isNoCache()) {
|
||||
/*
|
||||
If allow_reload is configured and the client requests "Cache-Control: no-cache",
|
||||
reload the cache by fetching a fresh response and caching it (if possible).
|
||||
*/
|
||||
$this->record($request, 'reload');
|
||||
$response = $this->fetch($request, $catch);
|
||||
} else {
|
||||
$response = $this->lookup($request, $catch);
|
||||
}
|
||||
|
||||
$this->restoreResponseBody($request, $response);
|
||||
|
||||
if (HttpKernelInterface::MAIN_REQUEST === $type) {
|
||||
$this->addTraces($response);
|
||||
}
|
||||
|
||||
if (null !== $this->surrogate) {
|
||||
if (HttpKernelInterface::MAIN_REQUEST === $type) {
|
||||
$this->surrogateCacheStrategy->update($response);
|
||||
} else {
|
||||
$this->surrogateCacheStrategy->add($response);
|
||||
}
|
||||
}
|
||||
|
||||
$response->prepare($request);
|
||||
|
||||
$response->isNotModified($request);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function terminate(Request $request, Response $response)
|
||||
{
|
||||
// Do not call any listeners in case of a cache hit.
|
||||
// This ensures identical behavior as if you had a separate
|
||||
// reverse caching proxy such as Varnish and the like.
|
||||
if ($this->options['terminate_on_cache_hit']) {
|
||||
trigger_deprecation('symfony/http-kernel', '6.2', 'Setting "terminate_on_cache_hit" to "true" is deprecated and will be changed to "false" in Symfony 7.0.');
|
||||
} elseif (\in_array('fresh', $this->traces[$this->getTraceKey($request)] ?? [], true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->getKernel() instanceof TerminableInterface) {
|
||||
$this->getKernel()->terminate($request, $response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwards the Request to the backend without storing the Response in the cache.
|
||||
*
|
||||
* @param bool $catch Whether to process exceptions
|
||||
*/
|
||||
protected function pass(Request $request, bool $catch = false): Response
|
||||
{
|
||||
$this->record($request, 'pass');
|
||||
|
||||
return $this->forward($request, $catch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates non-safe methods (like POST, PUT, and DELETE).
|
||||
*
|
||||
* @param bool $catch Whether to process exceptions
|
||||
*
|
||||
* @throws \Exception
|
||||
*
|
||||
* @see RFC2616 13.10
|
||||
*/
|
||||
protected function invalidate(Request $request, bool $catch = false): Response
|
||||
{
|
||||
$response = $this->pass($request, $catch);
|
||||
|
||||
// invalidate only when the response is successful
|
||||
if ($response->isSuccessful() || $response->isRedirect()) {
|
||||
try {
|
||||
$this->store->invalidate($request);
|
||||
|
||||
// As per the RFC, invalidate Location and Content-Location URLs if present
|
||||
foreach (['Location', 'Content-Location'] as $header) {
|
||||
if ($uri = $response->headers->get($header)) {
|
||||
$subRequest = Request::create($uri, 'get', [], [], [], $request->server->all());
|
||||
|
||||
$this->store->invalidate($subRequest);
|
||||
}
|
||||
}
|
||||
|
||||
$this->record($request, 'invalidate');
|
||||
} catch (\Exception $e) {
|
||||
$this->record($request, 'invalidate-failed');
|
||||
|
||||
if ($this->options['debug']) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookups a Response from the cache for the given Request.
|
||||
*
|
||||
* When a matching cache entry is found and is fresh, it uses it as the
|
||||
* response without forwarding any request to the backend. When a matching
|
||||
* cache entry is found but is stale, it attempts to "validate" the entry with
|
||||
* the backend using conditional GET. When no matching cache entry is found,
|
||||
* it triggers "miss" processing.
|
||||
*
|
||||
* @param bool $catch Whether to process exceptions
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function lookup(Request $request, bool $catch = false): Response
|
||||
{
|
||||
try {
|
||||
$entry = $this->store->lookup($request);
|
||||
} catch (\Exception $e) {
|
||||
$this->record($request, 'lookup-failed');
|
||||
|
||||
if ($this->options['debug']) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $this->pass($request, $catch);
|
||||
}
|
||||
|
||||
if (null === $entry) {
|
||||
$this->record($request, 'miss');
|
||||
|
||||
return $this->fetch($request, $catch);
|
||||
}
|
||||
|
||||
if (!$this->isFreshEnough($request, $entry)) {
|
||||
$this->record($request, 'stale');
|
||||
|
||||
return $this->validate($request, $entry, $catch);
|
||||
}
|
||||
|
||||
if ($entry->headers->hasCacheControlDirective('no-cache')) {
|
||||
return $this->validate($request, $entry, $catch);
|
||||
}
|
||||
|
||||
$this->record($request, 'fresh');
|
||||
|
||||
$entry->headers->set('Age', $entry->getAge());
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a cache entry is fresh.
|
||||
*
|
||||
* The original request is used as a template for a conditional
|
||||
* GET request with the backend.
|
||||
*
|
||||
* @param bool $catch Whether to process exceptions
|
||||
*/
|
||||
protected function validate(Request $request, Response $entry, bool $catch = false): Response
|
||||
{
|
||||
$subRequest = clone $request;
|
||||
|
||||
// send no head requests because we want content
|
||||
if ('HEAD' === $request->getMethod()) {
|
||||
$subRequest->setMethod('GET');
|
||||
}
|
||||
|
||||
// add our cached last-modified validator
|
||||
if ($entry->headers->has('Last-Modified')) {
|
||||
$subRequest->headers->set('If-Modified-Since', $entry->headers->get('Last-Modified'));
|
||||
}
|
||||
|
||||
// Add our cached etag validator to the environment.
|
||||
// We keep the etags from the client to handle the case when the client
|
||||
// has a different private valid entry which is not cached here.
|
||||
$cachedEtags = $entry->getEtag() ? [$entry->getEtag()] : [];
|
||||
$requestEtags = $request->getETags();
|
||||
if ($etags = array_unique(array_merge($cachedEtags, $requestEtags))) {
|
||||
$subRequest->headers->set('If-None-Match', implode(', ', $etags));
|
||||
}
|
||||
|
||||
$response = $this->forward($subRequest, $catch, $entry);
|
||||
|
||||
if (304 == $response->getStatusCode()) {
|
||||
$this->record($request, 'valid');
|
||||
|
||||
// return the response and not the cache entry if the response is valid but not cached
|
||||
$etag = $response->getEtag();
|
||||
if ($etag && \in_array($etag, $requestEtags) && !\in_array($etag, $cachedEtags)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$entry = clone $entry;
|
||||
$entry->headers->remove('Date');
|
||||
|
||||
foreach (['Date', 'Expires', 'Cache-Control', 'ETag', 'Last-Modified'] as $name) {
|
||||
if ($response->headers->has($name)) {
|
||||
$entry->headers->set($name, $response->headers->get($name));
|
||||
}
|
||||
}
|
||||
|
||||
$response = $entry;
|
||||
} else {
|
||||
$this->record($request, 'invalid');
|
||||
}
|
||||
|
||||
if ($response->isCacheable()) {
|
||||
$this->store($request, $response);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unconditionally fetches a fresh response from the backend and
|
||||
* stores it in the cache if is cacheable.
|
||||
*
|
||||
* @param bool $catch Whether to process exceptions
|
||||
*/
|
||||
protected function fetch(Request $request, bool $catch = false): Response
|
||||
{
|
||||
$subRequest = clone $request;
|
||||
|
||||
// send no head requests because we want content
|
||||
if ('HEAD' === $request->getMethod()) {
|
||||
$subRequest->setMethod('GET');
|
||||
}
|
||||
|
||||
// avoid that the backend sends no content
|
||||
$subRequest->headers->remove('If-Modified-Since');
|
||||
$subRequest->headers->remove('If-None-Match');
|
||||
|
||||
$response = $this->forward($subRequest, $catch);
|
||||
|
||||
if ($response->isCacheable()) {
|
||||
$this->store($request, $response);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwards the Request to the backend and returns the Response.
|
||||
*
|
||||
* All backend requests (cache passes, fetches, cache validations)
|
||||
* run through this method.
|
||||
*
|
||||
* @param bool $catch Whether to catch exceptions or not
|
||||
* @param Response|null $entry A Response instance (the stale entry if present, null otherwise)
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
protected function forward(Request $request, bool $catch = false, ?Response $entry = null)
|
||||
{
|
||||
$this->surrogate?->addSurrogateCapability($request);
|
||||
|
||||
// always a "master" request (as the real master request can be in cache)
|
||||
$response = SubRequestHandler::handle($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST, $catch);
|
||||
|
||||
/*
|
||||
* Support stale-if-error given on Responses or as a config option.
|
||||
* RFC 7234 summarizes in Section 4.2.4 (but also mentions with the individual
|
||||
* Cache-Control directives) that
|
||||
*
|
||||
* A cache MUST NOT generate a stale response if it is prohibited by an
|
||||
* explicit in-protocol directive (e.g., by a "no-store" or "no-cache"
|
||||
* cache directive, a "must-revalidate" cache-response-directive, or an
|
||||
* applicable "s-maxage" or "proxy-revalidate" cache-response-directive;
|
||||
* see Section 5.2.2).
|
||||
*
|
||||
* https://tools.ietf.org/html/rfc7234#section-4.2.4
|
||||
*
|
||||
* We deviate from this in one detail, namely that we *do* serve entries in the
|
||||
* stale-if-error case even if they have a `s-maxage` Cache-Control directive.
|
||||
*/
|
||||
if (null !== $entry
|
||||
&& \in_array($response->getStatusCode(), [500, 502, 503, 504])
|
||||
&& !$entry->headers->hasCacheControlDirective('no-cache')
|
||||
&& !$entry->mustRevalidate()
|
||||
) {
|
||||
if (null === $age = $entry->headers->getCacheControlDirective('stale-if-error')) {
|
||||
$age = $this->options['stale_if_error'];
|
||||
}
|
||||
|
||||
/*
|
||||
* stale-if-error gives the (extra) time that the Response may be used *after* it has become stale.
|
||||
* So we compare the time the $entry has been sitting in the cache already with the
|
||||
* time it was fresh plus the allowed grace period.
|
||||
*/
|
||||
if ($entry->getAge() <= $entry->getMaxAge() + $age) {
|
||||
$this->record($request, 'stale-if-error');
|
||||
|
||||
return $entry;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
RFC 7231 Sect. 7.1.1.2 says that a server that does not have a reasonably accurate
|
||||
clock MUST NOT send a "Date" header, although it MUST send one in most other cases
|
||||
except for 1xx or 5xx responses where it MAY do so.
|
||||
|
||||
Anyway, a client that received a message without a "Date" header MUST add it.
|
||||
*/
|
||||
if (!$response->headers->has('Date')) {
|
||||
$response->setDate(\DateTimeImmutable::createFromFormat('U', time()));
|
||||
}
|
||||
|
||||
$this->processResponseBody($request, $response);
|
||||
|
||||
if ($this->isPrivateRequest($request) && !$response->headers->hasCacheControlDirective('public')) {
|
||||
$response->setPrivate();
|
||||
} elseif ($this->options['default_ttl'] > 0 && null === $response->getTtl() && !$response->headers->getCacheControlDirective('must-revalidate')) {
|
||||
$response->setTtl($this->options['default_ttl']);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the cache entry is "fresh enough" to satisfy the Request.
|
||||
*/
|
||||
protected function isFreshEnough(Request $request, Response $entry): bool
|
||||
{
|
||||
if (!$entry->isFresh()) {
|
||||
return $this->lock($request, $entry);
|
||||
}
|
||||
|
||||
if ($this->options['allow_revalidate'] && null !== $maxAge = $request->headers->getCacheControlDirective('max-age')) {
|
||||
return $maxAge > 0 && $maxAge >= $entry->getAge();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks a Request during the call to the backend.
|
||||
*
|
||||
* @return bool true if the cache entry can be returned even if it is staled, false otherwise
|
||||
*/
|
||||
protected function lock(Request $request, Response $entry): bool
|
||||
{
|
||||
// try to acquire a lock to call the backend
|
||||
$lock = $this->store->lock($request);
|
||||
|
||||
if (true === $lock) {
|
||||
// we have the lock, call the backend
|
||||
return false;
|
||||
}
|
||||
|
||||
// there is already another process calling the backend
|
||||
|
||||
// May we serve a stale response?
|
||||
if ($this->mayServeStaleWhileRevalidate($entry)) {
|
||||
$this->record($request, 'stale-while-revalidate');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// wait for the lock to be released
|
||||
if ($this->waitForLock($request)) {
|
||||
// replace the current entry with the fresh one
|
||||
$new = $this->lookup($request);
|
||||
$entry->headers = $new->headers;
|
||||
$entry->setContent($new->getContent());
|
||||
$entry->setStatusCode($new->getStatusCode());
|
||||
$entry->setProtocolVersion($new->getProtocolVersion());
|
||||
foreach ($new->headers->getCookies() as $cookie) {
|
||||
$entry->headers->setCookie($cookie);
|
||||
}
|
||||
} else {
|
||||
// backend is slow as hell, send a 503 response (to avoid the dog pile effect)
|
||||
$entry->setStatusCode(503);
|
||||
$entry->setContent('503 Service Unavailable');
|
||||
$entry->headers->set('Retry-After', 10);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the Response to the cache.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function store(Request $request, Response $response)
|
||||
{
|
||||
try {
|
||||
$restoreHeaders = [];
|
||||
foreach ($this->options['skip_response_headers'] as $header) {
|
||||
if (!$response->headers->has($header)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$restoreHeaders[$header] = $response->headers->all($header);
|
||||
$response->headers->remove($header);
|
||||
}
|
||||
|
||||
$this->store->write($request, $response);
|
||||
$this->record($request, 'store');
|
||||
|
||||
$response->headers->set('Age', $response->getAge());
|
||||
} catch (\Exception $e) {
|
||||
$this->record($request, 'store-failed');
|
||||
|
||||
if ($this->options['debug']) {
|
||||
throw $e;
|
||||
}
|
||||
} finally {
|
||||
foreach ($restoreHeaders as $header => $values) {
|
||||
$response->headers->set($header, $values);
|
||||
}
|
||||
}
|
||||
|
||||
// now that the response is cached, release the lock
|
||||
$this->store->unlock($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the Response body.
|
||||
*/
|
||||
private function restoreResponseBody(Request $request, Response $response): void
|
||||
{
|
||||
if ($response->headers->has('X-Body-Eval')) {
|
||||
\assert(self::BODY_EVAL_BOUNDARY_LENGTH === 24);
|
||||
|
||||
ob_start();
|
||||
|
||||
$content = $response->getContent();
|
||||
$boundary = substr($content, 0, 24);
|
||||
$j = strpos($content, $boundary, 24);
|
||||
echo substr($content, 24, $j - 24);
|
||||
$i = $j + 24;
|
||||
|
||||
while (false !== $j = strpos($content, $boundary, $i)) {
|
||||
[$uri, $alt, $ignoreErrors, $part] = explode("\n", substr($content, $i, $j - $i), 4);
|
||||
$i = $j + 24;
|
||||
|
||||
echo $this->surrogate->handle($this, $uri, $alt, $ignoreErrors);
|
||||
echo $part;
|
||||
}
|
||||
|
||||
$response->setContent(ob_get_clean());
|
||||
$response->headers->remove('X-Body-Eval');
|
||||
if (!$response->headers->has('Transfer-Encoding')) {
|
||||
$response->headers->set('Content-Length', \strlen($response->getContent()));
|
||||
}
|
||||
} elseif ($response->headers->has('X-Body-File')) {
|
||||
// Response does not include possibly dynamic content (ESI, SSI), so we need
|
||||
// not handle the content for HEAD requests
|
||||
if (!$request->isMethod('HEAD')) {
|
||||
$response->setContent(file_get_contents($response->headers->get('X-Body-File')));
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
$response->headers->remove('X-Body-File');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function processResponseBody(Request $request, Response $response)
|
||||
{
|
||||
if ($this->surrogate?->needsParsing($response)) {
|
||||
$this->surrogate->process($request, $response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the Request includes authorization or other sensitive information
|
||||
* that should cause the Response to be considered private by default.
|
||||
*/
|
||||
private function isPrivateRequest(Request $request): bool
|
||||
{
|
||||
foreach ($this->options['private_headers'] as $key) {
|
||||
$key = strtolower(str_replace('HTTP_', '', $key));
|
||||
|
||||
if ('cookie' === $key) {
|
||||
if (\count($request->cookies->all())) {
|
||||
return true;
|
||||
}
|
||||
} elseif ($request->headers->has($key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records that an event took place.
|
||||
*/
|
||||
private function record(Request $request, string $event): void
|
||||
{
|
||||
$this->traces[$this->getTraceKey($request)][] = $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the key we use in the "trace" array for a given request.
|
||||
*/
|
||||
private function getTraceKey(Request $request): string
|
||||
{
|
||||
$path = $request->getPathInfo();
|
||||
if ($qs = $request->getQueryString()) {
|
||||
$path .= '?'.$qs;
|
||||
}
|
||||
|
||||
return $request->getMethod().' '.$path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given (cached) response may be served as "stale" when a revalidation
|
||||
* is currently in progress.
|
||||
*/
|
||||
private function mayServeStaleWhileRevalidate(Response $entry): bool
|
||||
{
|
||||
$timeout = $entry->headers->getCacheControlDirective('stale-while-revalidate');
|
||||
$timeout ??= $this->options['stale_while_revalidate'];
|
||||
|
||||
$age = $entry->getAge();
|
||||
$maxAge = $entry->getMaxAge() ?? 0;
|
||||
$ttl = $maxAge - $age;
|
||||
|
||||
return abs($ttl) < $timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the store to release a locked entry.
|
||||
*/
|
||||
private function waitForLock(Request $request): bool
|
||||
{
|
||||
$wait = 0;
|
||||
while ($this->store->isLocked($request) && $wait < 100) {
|
||||
usleep(50000);
|
||||
++$wait;
|
||||
}
|
||||
|
||||
return $wait < 100;
|
||||
}
|
||||
}
|
||||
-238
@@ -1,238 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpKernel\HttpCache;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* ResponseCacheStrategy knows how to compute the Response cache HTTP header
|
||||
* based on the different response cache headers.
|
||||
*
|
||||
* This implementation changes the main response TTL to the smallest TTL received
|
||||
* or force validation if one of the surrogates has validation cache strategy.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class ResponseCacheStrategy implements ResponseCacheStrategyInterface
|
||||
{
|
||||
/**
|
||||
* Cache-Control headers that are sent to the final response if they appear in ANY of the responses.
|
||||
*/
|
||||
private const OVERRIDE_DIRECTIVES = ['private', 'no-cache', 'no-store', 'no-transform', 'must-revalidate', 'proxy-revalidate'];
|
||||
|
||||
/**
|
||||
* Cache-Control headers that are sent to the final response if they appear in ALL of the responses.
|
||||
*/
|
||||
private const INHERIT_DIRECTIVES = ['public', 'immutable'];
|
||||
|
||||
private int $embeddedResponses = 0;
|
||||
private bool $isNotCacheableResponseEmbedded = false;
|
||||
private int $age = 0;
|
||||
private \DateTimeInterface|null|false $lastModified = null;
|
||||
private array $flagDirectives = [
|
||||
'no-cache' => null,
|
||||
'no-store' => null,
|
||||
'no-transform' => null,
|
||||
'must-revalidate' => null,
|
||||
'proxy-revalidate' => null,
|
||||
'public' => null,
|
||||
'private' => null,
|
||||
'immutable' => null,
|
||||
];
|
||||
private array $ageDirectives = [
|
||||
'max-age' => null,
|
||||
's-maxage' => null,
|
||||
'expires' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function add(Response $response)
|
||||
{
|
||||
++$this->embeddedResponses;
|
||||
|
||||
foreach (self::OVERRIDE_DIRECTIVES as $directive) {
|
||||
if ($response->headers->hasCacheControlDirective($directive)) {
|
||||
$this->flagDirectives[$directive] = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (self::INHERIT_DIRECTIVES as $directive) {
|
||||
if (false !== $this->flagDirectives[$directive]) {
|
||||
$this->flagDirectives[$directive] = $response->headers->hasCacheControlDirective($directive);
|
||||
}
|
||||
}
|
||||
|
||||
$age = $response->getAge();
|
||||
$this->age = max($this->age, $age);
|
||||
|
||||
if ($this->willMakeFinalResponseUncacheable($response)) {
|
||||
$this->isNotCacheableResponseEmbedded = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$isHeuristicallyCacheable = $response->headers->hasCacheControlDirective('public');
|
||||
$maxAge = $response->headers->hasCacheControlDirective('max-age') ? (int) $response->headers->getCacheControlDirective('max-age') : null;
|
||||
$this->storeRelativeAgeDirective('max-age', $maxAge, $age, $isHeuristicallyCacheable);
|
||||
$sharedMaxAge = $response->headers->hasCacheControlDirective('s-maxage') ? (int) $response->headers->getCacheControlDirective('s-maxage') : $maxAge;
|
||||
$this->storeRelativeAgeDirective('s-maxage', $sharedMaxAge, $age, $isHeuristicallyCacheable);
|
||||
|
||||
$expires = $response->getExpires();
|
||||
$expires = null !== $expires ? (int) $expires->format('U') - (int) $response->getDate()->format('U') : null;
|
||||
$this->storeRelativeAgeDirective('expires', $expires >= 0 ? $expires : null, 0, $isHeuristicallyCacheable);
|
||||
|
||||
if (false !== $this->lastModified) {
|
||||
$lastModified = $response->getLastModified();
|
||||
$this->lastModified = $lastModified ? max($this->lastModified, $lastModified) : false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function update(Response $response)
|
||||
{
|
||||
// if we have no embedded Response, do nothing
|
||||
if (0 === $this->embeddedResponses) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove Etag since it cannot be merged from embedded responses.
|
||||
$response->setEtag(null);
|
||||
|
||||
$this->add($response);
|
||||
|
||||
$response->headers->set('Age', $this->age);
|
||||
|
||||
if ($this->isNotCacheableResponseEmbedded) {
|
||||
$response->setLastModified(null);
|
||||
|
||||
if ($this->flagDirectives['no-store']) {
|
||||
$response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||
} else {
|
||||
$response->headers->set('Cache-Control', 'no-cache, must-revalidate');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$response->setLastModified($this->lastModified ?: null);
|
||||
|
||||
$flags = array_filter($this->flagDirectives);
|
||||
|
||||
if (isset($flags['must-revalidate'])) {
|
||||
$flags['no-cache'] = true;
|
||||
}
|
||||
|
||||
$response->headers->set('Cache-Control', implode(', ', array_keys($flags)));
|
||||
|
||||
$maxAge = null;
|
||||
|
||||
if (is_numeric($this->ageDirectives['max-age'])) {
|
||||
$maxAge = $this->ageDirectives['max-age'] + $this->age;
|
||||
$response->headers->addCacheControlDirective('max-age', $maxAge);
|
||||
}
|
||||
|
||||
if (is_numeric($this->ageDirectives['s-maxage'])) {
|
||||
$sMaxage = $this->ageDirectives['s-maxage'] + $this->age;
|
||||
|
||||
if ($maxAge !== $sMaxage) {
|
||||
$response->headers->addCacheControlDirective('s-maxage', $sMaxage);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_numeric($this->ageDirectives['expires'])) {
|
||||
$date = clone $response->getDate();
|
||||
$date = $date->modify('+'.($this->ageDirectives['expires'] + $this->age).' seconds');
|
||||
$response->setExpires($date);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC2616, Section 13.4.
|
||||
*
|
||||
* @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
|
||||
*/
|
||||
private function willMakeFinalResponseUncacheable(Response $response): bool
|
||||
{
|
||||
// RFC2616: A response received with a status code of 200, 203, 300, 301 or 410
|
||||
// MAY be stored by a cache […] unless a cache-control directive prohibits caching.
|
||||
if ($response->headers->hasCacheControlDirective('no-cache')
|
||||
|| $response->headers->hasCacheControlDirective('no-store')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Etag headers cannot be merged, they render the response uncacheable
|
||||
// by default (except if the response also has max-age etc.).
|
||||
if (null === $response->getEtag() && \in_array($response->getStatusCode(), [200, 203, 300, 301, 410])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// RFC2616: A response received with any other status code (e.g. status codes 302 and 307)
|
||||
// MUST NOT be returned in a reply to a subsequent request unless there are
|
||||
// cache-control directives or another header(s) that explicitly allow it.
|
||||
$cacheControl = ['max-age', 's-maxage', 'must-revalidate', 'proxy-revalidate', 'public', 'private'];
|
||||
foreach ($cacheControl as $key) {
|
||||
if ($response->headers->hasCacheControlDirective($key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($response->headers->has('Expires')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store lowest max-age/s-maxage/expires for the final response.
|
||||
*
|
||||
* The response might have been stored in cache a while ago. To keep things comparable,
|
||||
* we have to subtract the age so that the value is normalized for an age of 0.
|
||||
*
|
||||
* If the value is lower than the currently stored value, we update the value, to keep a rolling
|
||||
* minimal value of each instruction.
|
||||
*
|
||||
* If the value is NULL and the isHeuristicallyCacheable parameter is false, the directive will
|
||||
* not be set on the final response. In this case, not all responses had the directive set and no
|
||||
* value can be found that satisfies the requirements of all responses. The directive will be dropped
|
||||
* from the final response.
|
||||
*
|
||||
* If the isHeuristicallyCacheable parameter is true, however, the current response has been marked
|
||||
* as cacheable in a public (shared) cache, but did not provide an explicit lifetime that would serve
|
||||
* as an upper bound. In this case, we can proceed and possibly keep the directive on the final response.
|
||||
*/
|
||||
private function storeRelativeAgeDirective(string $directive, ?int $value, int $age, bool $isHeuristicallyCacheable): void
|
||||
{
|
||||
if (null === $value) {
|
||||
if ($isHeuristicallyCacheable) {
|
||||
/*
|
||||
* See https://datatracker.ietf.org/doc/html/rfc7234#section-4.2.2
|
||||
* This particular response does not require maximum lifetime; heuristics might be applied.
|
||||
* Other responses, however, might have more stringent requirements on maximum lifetime.
|
||||
* So, return early here so that the final response can have the more limiting value set.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
$this->ageDirectives[$directive] = false;
|
||||
}
|
||||
|
||||
if (false !== $this->ageDirectives[$directive]) {
|
||||
$value -= $age;
|
||||
$this->ageDirectives[$directive] = null !== $this->ageDirectives[$directive] ? min($this->ageDirectives[$directive], $value) : $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
-41
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* This code is partially based on the Rack-Cache library by Ryan Tomayko,
|
||||
* which is released under the MIT license.
|
||||
* (based on commit 02d2b48d75bcb63cf1c0c7149c077ad256542801)
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpKernel\HttpCache;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* ResponseCacheStrategyInterface implementations know how to compute the
|
||||
* Response cache HTTP header based on the different response cache headers.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
interface ResponseCacheStrategyInterface
|
||||
{
|
||||
/**
|
||||
* Adds a Response.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add(Response $response);
|
||||
|
||||
/**
|
||||
* Updates the Response HTTP headers based on the embedded Responses.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function update(Response $response);
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpKernel\HttpCache;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Ssi implements the SSI capabilities to Request and Response instances.
|
||||
*
|
||||
* @author Sebastian Krebs <krebs.seb@gmail.com>
|
||||
*/
|
||||
class Ssi extends AbstractSurrogate
|
||||
{
|
||||
public function getName(): string
|
||||
{
|
||||
return 'ssi';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function addSurrogateControl(Response $response)
|
||||
{
|
||||
if (str_contains($response->getContent(), '<!--#include')) {
|
||||
$response->headers->set('Surrogate-Control', 'content="SSI/1.0"');
|
||||
}
|
||||
}
|
||||
|
||||
public function renderIncludeTag(string $uri, ?string $alt = null, bool $ignoreErrors = true, string $comment = ''): string
|
||||
{
|
||||
return sprintf('<!--#include virtual="%s" -->', $uri);
|
||||
}
|
||||
|
||||
public function process(Request $request, Response $response): Response
|
||||
{
|
||||
$type = $response->headers->get('Content-Type');
|
||||
if (empty($type)) {
|
||||
$type = 'text/html';
|
||||
}
|
||||
|
||||
$parts = explode(';', $type);
|
||||
if (!\in_array($parts[0], $this->contentTypes)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
// we don't use a proper XML parser here as we can have SSI tags in a plain text response
|
||||
$content = $response->getContent();
|
||||
$boundary = self::generateBodyEvalBoundary();
|
||||
$chunks = preg_split('#<!--\#include\s+(.*?)\s*-->#', $content, -1, \PREG_SPLIT_DELIM_CAPTURE);
|
||||
|
||||
$i = 1;
|
||||
while (isset($chunks[$i])) {
|
||||
$options = [];
|
||||
preg_match_all('/(virtual)="([^"]*?)"/', $chunks[$i], $matches, \PREG_SET_ORDER);
|
||||
foreach ($matches as $set) {
|
||||
$options[$set[1]] = $set[2];
|
||||
}
|
||||
|
||||
if (!isset($options['virtual'])) {
|
||||
throw new \RuntimeException('Unable to process an SSI tag without a "virtual" attribute.');
|
||||
}
|
||||
|
||||
$chunks[$i] = $boundary.$options['virtual']."\n\n\n";
|
||||
$i += 2;
|
||||
}
|
||||
$content = $boundary.implode('', $chunks).$boundary;
|
||||
|
||||
$response->setContent($content);
|
||||
$response->headers->set('X-Body-Eval', 'SSI');
|
||||
|
||||
// remove SSI/1.0 from the Surrogate-Control header
|
||||
$this->removeFromControl($response);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -1,498 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* This code is partially based on the Rack-Cache library by Ryan Tomayko,
|
||||
* which is released under the MIT license.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpKernel\HttpCache;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Store implements all the logic for storing cache metadata (Request and Response headers).
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class Store implements StoreInterface
|
||||
{
|
||||
protected $root;
|
||||
/** @var \SplObjectStorage<Request, string> */
|
||||
private \SplObjectStorage $keyCache;
|
||||
/** @var array<string, resource> */
|
||||
private array $locks = [];
|
||||
private array $options;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* The available options are:
|
||||
*
|
||||
* * private_headers Set of response headers that should not be stored
|
||||
* when a response is cached. (default: Set-Cookie)
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function __construct(string $root, array $options = [])
|
||||
{
|
||||
$this->root = $root;
|
||||
if (!is_dir($this->root) && !@mkdir($this->root, 0777, true) && !is_dir($this->root)) {
|
||||
throw new \RuntimeException(sprintf('Unable to create the store directory (%s).', $this->root));
|
||||
}
|
||||
$this->keyCache = new \SplObjectStorage();
|
||||
$this->options = array_merge([
|
||||
'private_headers' => ['Set-Cookie'],
|
||||
], $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanups storage.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function cleanup()
|
||||
{
|
||||
// unlock everything
|
||||
foreach ($this->locks as $lock) {
|
||||
flock($lock, \LOCK_UN);
|
||||
fclose($lock);
|
||||
}
|
||||
|
||||
$this->locks = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to lock the cache for a given Request, without blocking.
|
||||
*
|
||||
* @return bool|string true if the lock is acquired, the path to the current lock otherwise
|
||||
*/
|
||||
public function lock(Request $request): bool|string
|
||||
{
|
||||
$key = $this->getCacheKey($request);
|
||||
|
||||
if (!isset($this->locks[$key])) {
|
||||
$path = $this->getPath($key);
|
||||
if (!is_dir(\dirname($path)) && false === @mkdir(\dirname($path), 0777, true) && !is_dir(\dirname($path))) {
|
||||
return $path;
|
||||
}
|
||||
$h = fopen($path, 'c');
|
||||
if (!flock($h, \LOCK_EX | \LOCK_NB)) {
|
||||
fclose($h);
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
$this->locks[$key] = $h;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the lock for the given Request.
|
||||
*
|
||||
* @return bool False if the lock file does not exist or cannot be unlocked, true otherwise
|
||||
*/
|
||||
public function unlock(Request $request): bool
|
||||
{
|
||||
$key = $this->getCacheKey($request);
|
||||
|
||||
if (isset($this->locks[$key])) {
|
||||
flock($this->locks[$key], \LOCK_UN);
|
||||
fclose($this->locks[$key]);
|
||||
unset($this->locks[$key]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isLocked(Request $request): bool
|
||||
{
|
||||
$key = $this->getCacheKey($request);
|
||||
|
||||
if (isset($this->locks[$key])) {
|
||||
return true; // shortcut if lock held by this process
|
||||
}
|
||||
|
||||
if (!is_file($path = $this->getPath($key))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$h = fopen($path, 'r');
|
||||
flock($h, \LOCK_EX | \LOCK_NB, $wouldBlock);
|
||||
flock($h, \LOCK_UN); // release the lock we just acquired
|
||||
fclose($h);
|
||||
|
||||
return (bool) $wouldBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates a cached Response for the Request provided.
|
||||
*/
|
||||
public function lookup(Request $request): ?Response
|
||||
{
|
||||
$key = $this->getCacheKey($request);
|
||||
|
||||
if (!$entries = $this->getMetadata($key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// find a cached entry that matches the request.
|
||||
$match = null;
|
||||
foreach ($entries as $entry) {
|
||||
if ($this->requestsMatch(isset($entry[1]['vary'][0]) ? implode(', ', $entry[1]['vary']) : '', $request->headers->all(), $entry[0])) {
|
||||
$match = $entry;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$headers = $match[1];
|
||||
if (file_exists($path = $this->getPath($headers['x-content-digest'][0]))) {
|
||||
return $this->restoreResponse($headers, $path);
|
||||
}
|
||||
|
||||
// TODO the metaStore referenced an entity that doesn't exist in
|
||||
// the entityStore. We definitely want to return nil but we should
|
||||
// also purge the entry from the meta-store when this is detected.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a cache entry to the store for the given Request and Response.
|
||||
*
|
||||
* Existing entries are read and any that match the response are removed. This
|
||||
* method calls write with the new list of cache entries.
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function write(Request $request, Response $response): string
|
||||
{
|
||||
$key = $this->getCacheKey($request);
|
||||
$storedEnv = $this->persistRequest($request);
|
||||
|
||||
if ($response->headers->has('X-Body-File')) {
|
||||
// Assume the response came from disk, but at least perform some safeguard checks
|
||||
if (!$response->headers->has('X-Content-Digest')) {
|
||||
throw new \RuntimeException('A restored response must have the X-Content-Digest header.');
|
||||
}
|
||||
|
||||
$digest = $response->headers->get('X-Content-Digest');
|
||||
if ($this->getPath($digest) !== $response->headers->get('X-Body-File')) {
|
||||
throw new \RuntimeException('X-Body-File and X-Content-Digest do not match.');
|
||||
}
|
||||
// Everything seems ok, omit writing content to disk
|
||||
} else {
|
||||
$digest = $this->generateContentDigest($response);
|
||||
$response->headers->set('X-Content-Digest', $digest);
|
||||
|
||||
if (!$this->save($digest, $response->getContent(), false)) {
|
||||
throw new \RuntimeException('Unable to store the entity.');
|
||||
}
|
||||
|
||||
if (!$response->headers->has('Transfer-Encoding')) {
|
||||
$response->headers->set('Content-Length', \strlen($response->getContent()));
|
||||
}
|
||||
}
|
||||
|
||||
// read existing cache entries, remove non-varying, and add this one to the list
|
||||
$entries = [];
|
||||
$vary = $response->headers->get('vary');
|
||||
foreach ($this->getMetadata($key) as $entry) {
|
||||
if (!isset($entry[1]['vary'][0])) {
|
||||
$entry[1]['vary'] = [''];
|
||||
}
|
||||
|
||||
if ($entry[1]['vary'][0] != $vary || !$this->requestsMatch($vary ?? '', $entry[0], $storedEnv)) {
|
||||
$entries[] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
$headers = $this->persistResponse($response);
|
||||
unset($headers['age']);
|
||||
|
||||
foreach ($this->options['private_headers'] as $h) {
|
||||
unset($headers[strtolower($h)]);
|
||||
}
|
||||
|
||||
array_unshift($entries, [$storedEnv, $headers]);
|
||||
|
||||
if (!$this->save($key, serialize($entries))) {
|
||||
throw new \RuntimeException('Unable to store the metadata.');
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns content digest for $response.
|
||||
*/
|
||||
protected function generateContentDigest(Response $response): string
|
||||
{
|
||||
return 'en'.hash('xxh128', $response->getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates all cache entries that match the request.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function invalidate(Request $request)
|
||||
{
|
||||
$modified = false;
|
||||
$key = $this->getCacheKey($request);
|
||||
|
||||
$entries = [];
|
||||
foreach ($this->getMetadata($key) as $entry) {
|
||||
$response = $this->restoreResponse($entry[1]);
|
||||
if ($response->isFresh()) {
|
||||
$response->expire();
|
||||
$modified = true;
|
||||
$entries[] = [$entry[0], $this->persistResponse($response)];
|
||||
} else {
|
||||
$entries[] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
if ($modified && !$this->save($key, serialize($entries))) {
|
||||
throw new \RuntimeException('Unable to store the metadata.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether two Request HTTP header sets are non-varying based on
|
||||
* the vary response header value provided.
|
||||
*
|
||||
* @param string|null $vary A Response vary header
|
||||
* @param array $env1 A Request HTTP header array
|
||||
* @param array $env2 A Request HTTP header array
|
||||
*/
|
||||
private function requestsMatch(?string $vary, array $env1, array $env2): bool
|
||||
{
|
||||
if (empty($vary)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (preg_split('/[\s,]+/', $vary) as $header) {
|
||||
$key = str_replace('_', '-', strtolower($header));
|
||||
$v1 = $env1[$key] ?? null;
|
||||
$v2 = $env2[$key] ?? null;
|
||||
if ($v1 !== $v2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all data associated with the given key.
|
||||
*
|
||||
* Use this method only if you know what you are doing.
|
||||
*/
|
||||
private function getMetadata(string $key): array
|
||||
{
|
||||
if (!$entries = $this->load($key)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return unserialize($entries) ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Purges data for the given URL.
|
||||
*
|
||||
* This method purges both the HTTP and the HTTPS version of the cache entry.
|
||||
*
|
||||
* @return bool true if the URL exists with either HTTP or HTTPS scheme and has been purged, false otherwise
|
||||
*/
|
||||
public function purge(string $url): bool
|
||||
{
|
||||
$http = preg_replace('#^https:#', 'http:', $url);
|
||||
$https = preg_replace('#^http:#', 'https:', $url);
|
||||
|
||||
$purgedHttp = $this->doPurge($http);
|
||||
$purgedHttps = $this->doPurge($https);
|
||||
|
||||
return $purgedHttp || $purgedHttps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Purges data for the given URL.
|
||||
*/
|
||||
private function doPurge(string $url): bool
|
||||
{
|
||||
$key = $this->getCacheKey(Request::create($url));
|
||||
if (isset($this->locks[$key])) {
|
||||
flock($this->locks[$key], \LOCK_UN);
|
||||
fclose($this->locks[$key]);
|
||||
unset($this->locks[$key]);
|
||||
}
|
||||
|
||||
if (is_file($path = $this->getPath($key))) {
|
||||
unlink($path);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads data for the given key.
|
||||
*/
|
||||
private function load(string $key): ?string
|
||||
{
|
||||
$path = $this->getPath($key);
|
||||
|
||||
return is_file($path) && false !== ($contents = @file_get_contents($path)) ? $contents : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save data for the given key.
|
||||
*/
|
||||
private function save(string $key, string $data, bool $overwrite = true): bool
|
||||
{
|
||||
$path = $this->getPath($key);
|
||||
|
||||
if (!$overwrite && file_exists($path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isset($this->locks[$key])) {
|
||||
$fp = $this->locks[$key];
|
||||
@ftruncate($fp, 0);
|
||||
@fseek($fp, 0);
|
||||
$len = @fwrite($fp, $data);
|
||||
if (\strlen($data) !== $len) {
|
||||
@ftruncate($fp, 0);
|
||||
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!is_dir(\dirname($path)) && false === @mkdir(\dirname($path), 0777, true) && !is_dir(\dirname($path))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tmpFile = tempnam(\dirname($path), basename($path));
|
||||
if (false === $fp = @fopen($tmpFile, 'w')) {
|
||||
@unlink($tmpFile);
|
||||
|
||||
return false;
|
||||
}
|
||||
@fwrite($fp, $data);
|
||||
@fclose($fp);
|
||||
|
||||
if ($data != file_get_contents($tmpFile)) {
|
||||
@unlink($tmpFile);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (false === @rename($tmpFile, $path)) {
|
||||
@unlink($tmpFile);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@chmod($path, 0666 & ~umask());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPath(string $key)
|
||||
{
|
||||
return $this->root.\DIRECTORY_SEPARATOR.substr($key, 0, 2).\DIRECTORY_SEPARATOR.substr($key, 2, 2).\DIRECTORY_SEPARATOR.substr($key, 4, 2).\DIRECTORY_SEPARATOR.substr($key, 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a cache key for the given Request.
|
||||
*
|
||||
* This method should return a key that must only depend on a
|
||||
* normalized version of the request URI.
|
||||
*
|
||||
* If the same URI can have more than one representation, based on some
|
||||
* headers, use a Vary header to indicate them, and each representation will
|
||||
* be stored independently under the same cache key.
|
||||
*/
|
||||
protected function generateCacheKey(Request $request): string
|
||||
{
|
||||
return 'md'.hash('sha256', $request->getUri());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a cache key for the given Request.
|
||||
*/
|
||||
private function getCacheKey(Request $request): string
|
||||
{
|
||||
if (isset($this->keyCache[$request])) {
|
||||
return $this->keyCache[$request];
|
||||
}
|
||||
|
||||
return $this->keyCache[$request] = $this->generateCacheKey($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the Request HTTP headers.
|
||||
*/
|
||||
private function persistRequest(Request $request): array
|
||||
{
|
||||
return $request->headers->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the Response HTTP headers.
|
||||
*/
|
||||
private function persistResponse(Response $response): array
|
||||
{
|
||||
$headers = $response->headers->all();
|
||||
$headers['X-Status'] = [$response->getStatusCode()];
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a Response from the HTTP headers and body.
|
||||
*/
|
||||
private function restoreResponse(array $headers, ?string $path = null): ?Response
|
||||
{
|
||||
$status = $headers['X-Status'][0];
|
||||
unset($headers['X-Status']);
|
||||
$content = null;
|
||||
|
||||
if (null !== $path) {
|
||||
$headers['X-Body-File'] = [$path];
|
||||
unset($headers['x-body-file']);
|
||||
|
||||
if ($headers['X-Body-Eval'] ?? $headers['x-body-eval'] ?? false) {
|
||||
$content = file_get_contents($path);
|
||||
\assert(HttpCache::BODY_EVAL_BOUNDARY_LENGTH === 24);
|
||||
if (48 > \strlen($content) || substr($content, -24) !== substr($content, 0, 24)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Response($content, $status, $headers);
|
||||
}
|
||||
}
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* This code is partially based on the Rack-Cache library by Ryan Tomayko,
|
||||
* which is released under the MIT license.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpKernel\HttpCache;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Interface implemented by HTTP cache stores.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
interface StoreInterface
|
||||
{
|
||||
/**
|
||||
* Locates a cached Response for the Request provided.
|
||||
*/
|
||||
public function lookup(Request $request): ?Response;
|
||||
|
||||
/**
|
||||
* Writes a cache entry to the store for the given Request and Response.
|
||||
*
|
||||
* Existing entries are read and any that match the response are removed. This
|
||||
* method calls write with the new list of cache entries.
|
||||
*
|
||||
* @return string The key under which the response is stored
|
||||
*/
|
||||
public function write(Request $request, Response $response): string;
|
||||
|
||||
/**
|
||||
* Invalidates all cache entries that match the request.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function invalidate(Request $request);
|
||||
|
||||
/**
|
||||
* Locks the cache for a given Request.
|
||||
*
|
||||
* @return bool|string true if the lock is acquired, the path to the current lock otherwise
|
||||
*/
|
||||
public function lock(Request $request): bool|string;
|
||||
|
||||
/**
|
||||
* Releases the lock for the given Request.
|
||||
*
|
||||
* @return bool False if the lock file does not exist or cannot be unlocked, true otherwise
|
||||
*/
|
||||
public function unlock(Request $request): bool;
|
||||
|
||||
/**
|
||||
* Returns whether or not a lock exists.
|
||||
*
|
||||
* @return bool true if lock exists, false otherwise
|
||||
*/
|
||||
public function isLocked(Request $request): bool;
|
||||
|
||||
/**
|
||||
* Purges data for the given URL.
|
||||
*
|
||||
* @return bool true if the URL exists and has been purged, false otherwise
|
||||
*/
|
||||
public function purge(string $url): bool;
|
||||
|
||||
/**
|
||||
* Cleanups storage.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function cleanup();
|
||||
}
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpKernel\HttpCache;
|
||||
|
||||
use Symfony\Component\HttpFoundation\IpUtils;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class SubRequestHandler
|
||||
{
|
||||
public static function handle(HttpKernelInterface $kernel, Request $request, int $type, bool $catch): Response
|
||||
{
|
||||
// save global state related to trusted headers and proxies
|
||||
$trustedProxies = Request::getTrustedProxies();
|
||||
$trustedHeaderSet = Request::getTrustedHeaderSet();
|
||||
|
||||
// remove untrusted values
|
||||
$remoteAddr = $request->server->get('REMOTE_ADDR');
|
||||
if (!$remoteAddr || !IpUtils::checkIp($remoteAddr, $trustedProxies)) {
|
||||
$trustedHeaders = [
|
||||
'FORWARDED' => $trustedHeaderSet & Request::HEADER_FORWARDED,
|
||||
'X_FORWARDED_FOR' => $trustedHeaderSet & Request::HEADER_X_FORWARDED_FOR,
|
||||
'X_FORWARDED_HOST' => $trustedHeaderSet & Request::HEADER_X_FORWARDED_HOST,
|
||||
'X_FORWARDED_PROTO' => $trustedHeaderSet & Request::HEADER_X_FORWARDED_PROTO,
|
||||
'X_FORWARDED_PORT' => $trustedHeaderSet & Request::HEADER_X_FORWARDED_PORT,
|
||||
'X_FORWARDED_PREFIX' => $trustedHeaderSet & Request::HEADER_X_FORWARDED_PREFIX,
|
||||
];
|
||||
foreach (array_filter($trustedHeaders) as $name => $key) {
|
||||
$request->headers->remove($name);
|
||||
$request->server->remove('HTTP_'.$name);
|
||||
}
|
||||
}
|
||||
|
||||
// compute trusted values, taking any trusted proxies into account
|
||||
$trustedIps = [];
|
||||
$trustedValues = [];
|
||||
foreach (array_reverse($request->getClientIps()) as $ip) {
|
||||
$trustedIps[] = $ip;
|
||||
$trustedValues[] = sprintf('for="%s"', $ip);
|
||||
}
|
||||
if ($ip !== $remoteAddr) {
|
||||
$trustedIps[] = $remoteAddr;
|
||||
$trustedValues[] = sprintf('for="%s"', $remoteAddr);
|
||||
}
|
||||
|
||||
// set trusted values, reusing as much as possible the global trusted settings
|
||||
if (Request::HEADER_FORWARDED & $trustedHeaderSet) {
|
||||
$trustedValues[0] .= sprintf(';host="%s";proto=%s', $request->getHttpHost(), $request->getScheme());
|
||||
$request->headers->set('Forwarded', $v = implode(', ', $trustedValues));
|
||||
$request->server->set('HTTP_FORWARDED', $v);
|
||||
}
|
||||
if (Request::HEADER_X_FORWARDED_FOR & $trustedHeaderSet) {
|
||||
$request->headers->set('X-Forwarded-For', $v = implode(', ', $trustedIps));
|
||||
$request->server->set('HTTP_X_FORWARDED_FOR', $v);
|
||||
} elseif (!(Request::HEADER_FORWARDED & $trustedHeaderSet)) {
|
||||
Request::setTrustedProxies($trustedProxies, $trustedHeaderSet | Request::HEADER_X_FORWARDED_FOR);
|
||||
$request->headers->set('X-Forwarded-For', $v = implode(', ', $trustedIps));
|
||||
$request->server->set('HTTP_X_FORWARDED_FOR', $v);
|
||||
}
|
||||
|
||||
// fix the client IP address by setting it to 127.0.0.1,
|
||||
// which is the core responsibility of this method
|
||||
$request->server->set('REMOTE_ADDR', '127.0.0.1');
|
||||
|
||||
// ensure 127.0.0.1 is set as trusted proxy
|
||||
if (!IpUtils::checkIp('127.0.0.1', $trustedProxies)) {
|
||||
Request::setTrustedProxies(array_merge($trustedProxies, ['127.0.0.1']), Request::getTrustedHeaderSet());
|
||||
}
|
||||
|
||||
try {
|
||||
return $kernel->handle($request, $type, $catch);
|
||||
} finally {
|
||||
// restore global state
|
||||
Request::setTrustedProxies($trustedProxies, $trustedHeaderSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
-77
@@ -1,77 +0,0 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpKernel\HttpCache;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
interface SurrogateInterface
|
||||
{
|
||||
/**
|
||||
* Returns surrogate name.
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* Returns a new cache strategy instance.
|
||||
*/
|
||||
public function createCacheStrategy(): ResponseCacheStrategyInterface;
|
||||
|
||||
/**
|
||||
* Checks that at least one surrogate has Surrogate capability.
|
||||
*/
|
||||
public function hasSurrogateCapability(Request $request): bool;
|
||||
|
||||
/**
|
||||
* Adds Surrogate-capability to the given Request.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addSurrogateCapability(Request $request);
|
||||
|
||||
/**
|
||||
* Adds HTTP headers to specify that the Response needs to be parsed for Surrogate.
|
||||
*
|
||||
* This method only adds an Surrogate HTTP header if the Response has some Surrogate tags.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addSurrogateControl(Response $response);
|
||||
|
||||
/**
|
||||
* Checks that the Response needs to be parsed for Surrogate tags.
|
||||
*/
|
||||
public function needsParsing(Response $response): bool;
|
||||
|
||||
/**
|
||||
* Renders a Surrogate tag.
|
||||
*
|
||||
* @param string|null $alt An alternate URI
|
||||
* @param string $comment A comment to add as an esi:include tag
|
||||
*/
|
||||
public function renderIncludeTag(string $uri, ?string $alt = null, bool $ignoreErrors = true, string $comment = ''): string;
|
||||
|
||||
/**
|
||||
* Replaces a Response Surrogate tags with the included resource content.
|
||||
*/
|
||||
public function process(Request $request, Response $response): Response;
|
||||
|
||||
/**
|
||||
* Handles a Surrogate from the cache.
|
||||
*
|
||||
* @param string $alt An alternative URI
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function handle(HttpCache $cache, string $uri, string $alt, bool $ignoreErrors): string;
|
||||
}
|
||||
Reference in New Issue
Block a user