diff --git a/lib/classes/component.php b/lib/classes/component.php
index da1ec54b051..d297a131c32 100644
--- a/lib/classes/component.php
+++ b/lib/classes/component.php
@@ -116,6 +116,7 @@ class core_component {
'GuzzleHttp\\Psr7' => 'lib/guzzlehttp/psr7/src',
'GuzzleHttp\\Promise' => 'lib/guzzlehttp/promises/src',
'GuzzleHttp' => 'lib/guzzlehttp/guzzle/src',
+ 'Kevinrob\\GuzzleCache' => 'lib/guzzlehttp/kevinrob/guzzlecache/src',
);
/**
diff --git a/lib/guzzlehttp/kevinrob/guzzlecache/LICENSE b/lib/guzzlehttp/kevinrob/guzzlecache/LICENSE
new file mode 100644
index 00000000000..f56457772c0
--- /dev/null
+++ b/lib/guzzlehttp/kevinrob/guzzlecache/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Kevin Robatel
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/lib/guzzlehttp/kevinrob/guzzlecache/README.md b/lib/guzzlehttp/kevinrob/guzzlecache/README.md
new file mode 100644
index 00000000000..25d60240345
--- /dev/null
+++ b/lib/guzzlehttp/kevinrob/guzzlecache/README.md
@@ -0,0 +1,292 @@
+# guzzle-cache-middleware
+
+[](https://packagist.org/packages/kevinrob/guzzle-cache-middleware) [](https://packagist.org/packages/kevinrob/guzzle-cache-middleware) [](https://packagist.org/packages/kevinrob/guzzle-cache-middleware)
+ [](https://scrutinizer-ci.com/g/Kevinrob/guzzle-cache-middleware/?branch=master) [](https://scrutinizer-ci.com/g/Kevinrob/guzzle-cache-middleware/?branch=master)
+
+
+A HTTP Cache for [Guzzle](https://github.com/guzzle/guzzle) 6+. It's a simple Middleware to be added in the HandlerStack.
+
+## Goals
+- RFC 7234 compliance
+- Performance and transparency
+- Assured compatibility with PSR-7
+
+## Built-in storage interfaces
+- [Doctrine cache](https://github.com/doctrine/cache)
+- [Laravel cache](https://laravel.com/docs/5.2/cache)
+- [Flysystem](https://github.com/thephpleague/flysystem)
+- [PSR6](https://github.com/php-fig/cache)
+- [WordPress Object Cache](https://codex.wordpress.org/Class_Reference/WP_Object_Cache)
+
+## Installation
+
+`composer require kevinrob/guzzle-cache-middleware`
+
+or add it the your `composer.json` and run `composer update kevinrob/guzzle-cache-middleware`.
+
+# Why?
+Performance. It's very common to do some HTTP calls to an API for rendering a page and it takes times to do it.
+
+# How?
+With a simple Middleware added at the top of the `HandlerStack` of Guzzle.
+
+```php
+use GuzzleHttp\Client;
+use GuzzleHttp\HandlerStack;
+use Kevinrob\GuzzleCache\CacheMiddleware;
+
+// Create default HandlerStack
+$stack = HandlerStack::create();
+
+// Add this middleware to the top with `push`
+$stack->push(new CacheMiddleware(), 'cache');
+
+// Initialize the client with the handler option
+$client = new Client(['handler' => $stack]);
+```
+
+# Examples
+
+## Doctrine/Cache
+You can use a cache from `Doctrine/Cache`:
+```php
+[...]
+use Doctrine\Common\Cache\FilesystemCache;
+use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
+use Kevinrob\GuzzleCache\Storage\DoctrineCacheStorage;
+
+[...]
+$stack->push(
+ new CacheMiddleware(
+ new PrivateCacheStrategy(
+ new DoctrineCacheStorage(
+ new FilesystemCache('/tmp/')
+ )
+ )
+ ),
+ 'cache'
+);
+```
+
+You can use `ChainCache` for using multiple `CacheProvider` instances. With that provider, you have to sort the different caches from the faster to the slower. Like that, you can have a very fast cache.
+```php
+[...]
+use Doctrine\Common\Cache\ChainCache;
+use Doctrine\Common\Cache\ArrayCache;
+use Doctrine\Common\Cache\FilesystemCache;
+use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
+use Kevinrob\GuzzleCache\Storage\DoctrineCacheStorage;
+
+[...]
+$stack->push(new CacheMiddleware(
+ new PrivateCacheStrategy(
+ new DoctrineCacheStorage(
+ new ChainCache([
+ new ArrayCache(),
+ new FilesystemCache('/tmp/'),
+ ])
+ )
+ )
+), 'cache');
+```
+
+## Laravel cache
+You can use a cache with Laravel, e.g. Redis, Memcache etc.:
+```php
+[...]
+use Illuminate\Support\Facades\Cache;
+use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
+use Kevinrob\GuzzleCache\Storage\LaravelCacheStorage;
+
+[...]
+
+$stack->push(
+ new CacheMiddleware(
+ new PrivateCacheStrategy(
+ new LaravelCacheStorage(
+ Cache::store('redis')
+ )
+ )
+ ),
+ 'cache'
+);
+```
+
+## Flysystem
+```php
+[...]
+use League\Flysystem\Adapter\Local;
+use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
+use Kevinrob\GuzzleCache\Storage\FlysystemStorage;
+
+[...]
+
+$stack->push(
+ new CacheMiddleware(
+ new PrivateCacheStrategy(
+ new FlysystemStorage(
+ new Local('/path/to/cache')
+ )
+ )
+ ),
+ 'cache'
+);
+```
+
+## WordPress Object Cache
+```php
+[...]
+use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
+use Kevinrob\GuzzleCache\Storage\WordPressObjectCacheStorage;
+
+[...]
+
+$stack->push(
+ new CacheMiddleware(
+ new PrivateCacheStrategy(
+ new WordPressObjectCacheStorage()
+ )
+ ),
+ 'cache'
+);
+```
+
+## Public and shared
+It's possible to add a public shared cache to the stack:
+```php
+[...]
+use Doctrine\Common\Cache\FilesystemCache;
+use Doctrine\Common\Cache\PredisCache;
+use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
+use Kevinrob\GuzzleCache\Strategy\PublicCacheStrategy;
+use Kevinrob\GuzzleCache\Storage\DoctrineCacheStorage;
+
+[...]
+// Private caching
+$stack->push(
+ new CacheMiddleware(
+ new PrivateCacheStrategy(
+ new DoctrineCacheStorage(
+ new FilesystemCache('/tmp/')
+ )
+ )
+ ),
+ 'private-cache'
+);
+
+// Public caching
+$stack->push(
+ new CacheMiddleware(
+ new PublicCacheStrategy(
+ new DoctrineCacheStorage(
+ new PredisCache(
+ new Predis\Client('tcp://10.0.0.1:6379')
+ )
+ )
+ )
+ ),
+ 'shared-cache'
+);
+```
+
+## Greedy caching
+In some cases servers might send insufficient or no caching headers at all.
+Using the greedy caching strategy allows defining an expiry TTL on your own while
+disregarding any possibly present caching headers:
+```php
+[...]
+use Kevinrob\GuzzleCache\KeyValueHttpHeader;
+use Kevinrob\GuzzleCache\Strategy\GreedyCacheStrategy;
+use Kevinrob\GuzzleCache\Storage\DoctrineCacheStorage;
+use Doctrine\Common\Cache\FilesystemCache;
+
+[...]
+// Greedy caching
+$stack->push(
+ new CacheMiddleware(
+ new GreedyCacheStrategy(
+ new DoctrineCacheStorage(
+ new FilesystemCache('/tmp/')
+ ),
+ 1800, // the TTL in seconds
+ new KeyValueHttpHeader(['Authorization']) // Optional - specify the headers that can change the cache key
+ )
+ ),
+ 'greedy-cache'
+);
+```
+
+## Delegate caching
+Because your client may call different apps, on different domains, you may need to define which strategy is suitable to your requests.
+
+To solve this, all you have to do is to define a default cache strategy, and override it by implementing your own Request Matchers.
+
+Here's an example:
+```php
+namespace App\RequestMatcher;
+
+use Kevinrob\GuzzleCache\Strategy\Delegate\RequestMatcherInterface;
+use Psr\Http\Message\RequestInterface;
+
+class ExampleOrgRequestMatcher implements RequestMatcherInterface
+{
+
+ /**
+ * @inheritDoc
+ */
+ public function matches(RequestInterface $request)
+ {
+ return false !== strpos($request->getUri()->getHost(), 'example.org');
+ }
+}
+```
+
+```php
+namespace App\RequestMatcher;
+
+use Kevinrob\GuzzleCache\Strategy\Delegate\RequestMatcherInterface;
+use Psr\Http\Message\RequestInterface;
+
+class TwitterRequestMatcher implements RequestMatcherInterface
+{
+
+ /**
+ * @inheritDoc
+ */
+ public function matches(RequestInterface $request)
+ {
+ return false !== strpos($request->getUri()->getHost(), 'twitter.com');
+ }
+}
+```
+
+```php
+require_once __DIR__ . '/vendor/autoload.php';
+
+use App\RequestMatcher\ExampleOrgRequestMatcher;
+use App\RequestMatcher\TwitterRequestMatcher;
+use GuzzleHttp\Client;
+use GuzzleHttp\HandlerStack;
+use Kevinrob\GuzzleCache\CacheMiddleware;
+use Kevinrob\GuzzleCache\Strategy;
+
+$strategy = new Strategy\Delegate\DelegatingCacheStrategy($defaultStrategy = new Strategy\NullCacheStrategy());
+$strategy->registerRequestMatcher(new ExampleOrgRequestMatcher(), new Strategy\PublicCacheStrategy());
+$strategy->registerRequestMatcher(new TwitterRequestMatcher(), new Strategy\PrivateCacheStrategy());
+
+$stack = HandlerStack::create();
+$stack->push(new CacheMiddleware($strategy));
+$guzzle = new Client(['handler' => $stack]);
+```
+
+With this example:
+* All requests to `example.org` will be handled by `PublicCacheStrategy`
+* All requests to `twitter.com` will be handled by `PrivateCacheStrategy`
+* All other requests won't be cached.
+
+## Drupal
+See [Guzzle Cache](https://www.drupal.org/project/guzzle_cache) module.
+
+# Links that talk about the project
+- [Speeding Up APIs/Apps/Smart Toasters with HTTP Response Caching](https://apisyouwonthate.com/blog/speeding-up-apis-apps-smart-toasters-with-http-response-caching)
+- [Caching HTTP-Requests with Guzzle 6 and PSR-6](http://a.kabachnik.info/caching-http-requests-with-guzzle-6-and-psr-6.html)
diff --git a/lib/guzzlehttp/kevinrob/guzzlecache/composer.json b/lib/guzzlehttp/kevinrob/guzzlecache/composer.json
new file mode 100644
index 00000000000..0508384a169
--- /dev/null
+++ b/lib/guzzlehttp/kevinrob/guzzlecache/composer.json
@@ -0,0 +1,57 @@
+{
+ "name": "kevinrob/guzzle-cache-middleware",
+ "type": "library",
+ "description": "A HTTP/1.1 Cache for Guzzle 6. It's a simple Middleware to be added in the HandlerStack. (RFC 7234)",
+ "keywords": ["guzzle", "guzzle6", "cache", "http", "http 1.1", "psr6", "psr7", "handler", "middleware", "cache-control", "rfc7234", "performance", "php", "promise", "expiration", "validation", "Etag", "flysystem", "doctrine"],
+ "homepage": "https://github.com/Kevinrob/guzzle-cache-middleware",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Kevin Robatel",
+ "email": "kevinrob2@gmail.com",
+ "homepage": "https://github.com/Kevinrob"
+ }
+ ],
+ "require": {
+ "php": ">=7.2.0",
+ "guzzlehttp/guzzle": "^6.0 || ^7.0",
+ "guzzlehttp/psr7": "^1.7.0 || ^2.0.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5.15 || ^9.5",
+ "doctrine/cache": "^1.10",
+ "league/flysystem": "^1.0",
+ "psr/cache": "^1.0",
+ "cache/array-adapter": "^0.4 || ^0.5 || ^1.0",
+ "illuminate/cache": "^5.0",
+ "cache/simple-cache-bridge": "^0.1 || ^1.0",
+ "symfony/phpunit-bridge": "^4.4 || ^5.0",
+ "symfony/cache": "^4.4 || ^5.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Kevinrob\\GuzzleCache\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Kevinrob\\GuzzleCache\\Tests\\": "tests/"
+ }
+ },
+ "suggest": {
+ "guzzlehttp/guzzle": "For using this library. It was created for Guzzle6 (but you can use it with any PSR-7 HTTP client).",
+ "doctrine/cache": "This library has a lot of ready-to-use cache storage (to be used with Kevinrob\\GuzzleCache\\Storage\\DoctrineCacheStorage). Use only versions >=1.4.0 < 2.0.0",
+ "league/flysystem": "To be used with Kevinrob\\GuzzleCache\\Storage\\FlysystemStorage",
+ "psr/cache": "To be used with Kevinrob\\GuzzleCache\\Storage\\Psr6CacheStorage",
+ "psr/simple-cache": "To be used with Kevinrob\\GuzzleCache\\Storage\\Psr16CacheStorage",
+ "laravel/framework": "To be used with Kevinrob\\GuzzleCache\\Storage\\LaravelCacheStorage"
+ },
+ "scripts": {
+ "test": "vendor/bin/phpunit"
+ },
+ "config": {
+ "allow-plugins": {
+ "kylekatarnls/update-helper": true
+ }
+ }
+}
diff --git a/lib/guzzlehttp/kevinrob/guzzlecache/src/BodyStore.php b/lib/guzzlehttp/kevinrob/guzzlecache/src/BodyStore.php
new file mode 100644
index 00000000000..0ff38a1a233
--- /dev/null
+++ b/lib/guzzlehttp/kevinrob/guzzlecache/src/BodyStore.php
@@ -0,0 +1,46 @@
+body = $body;
+ $this->toRead = mb_strlen($this->body);
+ }
+
+ /**
+ * @param int $length
+ * @return false|string
+ */
+ public function __invoke(int $length)
+ {
+ if ($this->toRead <= 0) {
+ return false;
+ }
+
+ $length = min($length, $this->toRead);
+
+ $body = mb_substr(
+ $this->body,
+ $this->read,
+ $length
+ );
+ $this->toRead -= $length;
+ $this->read += $length;
+ return $body;
+ }
+}
diff --git a/lib/guzzlehttp/kevinrob/guzzlecache/src/CacheEntry.php b/lib/guzzlehttp/kevinrob/guzzlecache/src/CacheEntry.php
new file mode 100644
index 00000000000..7e3c8bc37ab
--- /dev/null
+++ b/lib/guzzlehttp/kevinrob/guzzlecache/src/CacheEntry.php
@@ -0,0 +1,302 @@
+dateCreated = new \DateTime();
+
+ $this->request = $request;
+ $this->response = $response;
+ $this->staleAt = $staleAt;
+
+ $values = new KeyValueHttpHeader($response->getHeader('Cache-Control'));
+
+ if ($staleIfErrorTo === null && $values->has('stale-if-error')) {
+ $this->staleIfErrorTo = (new \DateTime(
+ '@'.($this->staleAt->getTimestamp() + (int) $values->get('stale-if-error'))
+ ));
+ } else {
+ $this->staleIfErrorTo = $staleIfErrorTo;
+ }
+
+ if ($staleWhileRevalidateTo === null && $values->has('stale-while-revalidate')) {
+ $this->staleWhileRevalidateTo = new \DateTime(
+ '@'.($this->staleAt->getTimestamp() + (int) $values->get('stale-while-revalidate'))
+ );
+ } else {
+ $this->staleWhileRevalidateTo = $staleWhileRevalidateTo;
+ }
+ }
+
+ /**
+ * @return ResponseInterface
+ */
+ public function getResponse()
+ {
+ return $this->response
+ ->withHeader('Age', $this->getAge());
+ }
+
+ /**
+ * @return ResponseInterface
+ */
+ public function getOriginalResponse()
+ {
+ return $this->response;
+ }
+
+ /**
+ * @return RequestInterface
+ */
+ public function getOriginalRequest()
+ {
+ return $this->request;
+ }
+
+ /**
+ * @param RequestInterface $request
+ * @return bool
+ */
+ public function isVaryEquals(RequestInterface $request)
+ {
+ if ($this->response->hasHeader('Vary')) {
+ if ($this->request === null) {
+ return false;
+ }
+
+ foreach ($this->getVaryHeaders() as $key => $value) {
+ if (!$this->request->hasHeader($key)
+ && !$request->hasHeader($key)
+ ) {
+ // Absent from both
+ continue;
+ } elseif ($this->request->getHeaderLine($key)
+ == $request->getHeaderLine($key)
+ ) {
+ // Same content
+ continue;
+ }
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the vary headers that should be honoured by the cache.
+ *
+ * @return KeyValueHttpHeader
+ */
+ public function getVaryHeaders()
+ {
+ return new KeyValueHttpHeader($this->response->getHeader('Vary'));
+ }
+
+ /**
+ * @return \DateTime
+ */
+ public function getStaleAt()
+ {
+ return $this->staleAt;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isFresh()
+ {
+ return !$this->isStale();
+ }
+
+ /**
+ * @return bool
+ */
+ public function isStale()
+ {
+ return $this->getStaleAge() > 0;
+ }
+
+ /**
+ * @return int positive value equal staled
+ */
+ public function getStaleAge()
+ {
+ // This object is immutable
+ if ($this->timestampStale === null) {
+ $this->timestampStale = $this->staleAt->getTimestamp();
+ }
+
+ return time() - $this->timestampStale;
+ }
+
+ /**
+ * @return bool
+ */
+ public function serveStaleIfError()
+ {
+ return $this->staleIfErrorTo !== null
+ && $this->staleIfErrorTo->getTimestamp() >= (new \DateTime())->getTimestamp();
+ }
+
+ /**
+ * @return bool
+ */
+ public function staleWhileValidate()
+ {
+ return $this->staleWhileRevalidateTo !== null
+ && $this->staleWhileRevalidateTo->getTimestamp() >= (new \DateTime())->getTimestamp();
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasValidationInformation()
+ {
+ return $this->response->hasHeader('Etag') || $this->response->hasHeader('Last-Modified');
+ }
+
+ /**
+ * Time in seconds how long the entry should be kept in the cache
+ *
+ * This will not give the time (in seconds) that the response will still be fresh for
+ * from the HTTP point of view, but an upper bound on how long it is necessary and
+ * reasonable to keep the response in a cache (to re-use it or re-validate it later on).
+ *
+ * @return int TTL in seconds (0 = infinite)
+ */
+ public function getTTL()
+ {
+ if ($this->hasValidationInformation()) {
+ // No TTL if we have a way to re-validate the cache
+ return 0;
+ }
+
+ $ttl = 0;
+
+ // Keep it when stale if error
+ if ($this->staleIfErrorTo !== null) {
+ $ttl = max($ttl, $this->staleIfErrorTo->getTimestamp() - time());
+ }
+
+ // Keep it when stale-while-revalidate
+ if ($this->staleWhileRevalidateTo !== null) {
+ $ttl = max($ttl, $this->staleWhileRevalidateTo->getTimestamp() - time());
+ }
+
+ // Keep it until it become stale
+ $ttl = max($ttl, $this->staleAt->getTimestamp() - time());
+
+ // Don't return 0, it's reserved for infinite TTL
+ return $ttl !== 0 ? (int) $ttl : -1;
+ }
+
+ /**
+ * @return int Age in seconds
+ */
+ public function getAge()
+ {
+ return time() - $this->dateCreated->getTimestamp();
+ }
+
+ public function __sleep()
+ {
+ // Stream/Resource can't be serialized... So we copy the content into an implementation of `Psr\Http\Message\StreamInterface`
+ if ($this->response !== null) {
+ $responseBody = (string)$this->response->getBody();
+ $this->response = $this->response->withBody(
+ new PumpStream(
+ new BodyStore($responseBody),
+ [
+ 'size' => mb_strlen($responseBody),
+ ]
+ )
+ );
+ }
+
+ $requestBody = (string)$this->request->getBody();
+ $this->request = $this->request->withBody(
+ new PumpStream(
+ new BodyStore($requestBody),
+ [
+ 'size' => mb_strlen($requestBody)
+ ]
+ )
+ );
+
+ return array_keys(get_object_vars($this));
+ }
+
+ public function __wakeup()
+ {
+ // We re-create the stream of the response
+ if ($this->response !== null) {
+ $this->response = $this->response
+ ->withBody(
+ \GuzzleHttp\Psr7\Utils::streamFor((string) $this->response->getBody())
+ );
+ }
+ $this->request = $this->request
+ ->withBody(
+ \GuzzleHttp\Psr7\Utils::streamFor((string) $this->request->getBody())
+ );
+ }
+
+}
diff --git a/lib/guzzlehttp/kevinrob/guzzlecache/src/CacheMiddleware.php b/lib/guzzlehttp/kevinrob/guzzlecache/src/CacheMiddleware.php
new file mode 100644
index 00000000000..bb55f0cd5be
--- /dev/null
+++ b/lib/guzzlehttp/kevinrob/guzzlecache/src/CacheMiddleware.php
@@ -0,0 +1,402 @@
+ true];
+
+ /**
+ * List of safe methods
+ *
+ * https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1
+ *
+ * @var array
+ */
+ protected $safeMethods = ['GET' => true, 'HEAD' => true, 'OPTIONS' => true, 'TRACE' => true];
+
+ /**
+ * @param CacheStrategyInterface|null $cacheStrategy
+ */
+ public function __construct(CacheStrategyInterface $cacheStrategy = null)
+ {
+ $this->cacheStorage = $cacheStrategy !== null ? $cacheStrategy : new PrivateCacheStrategy();
+
+ register_shutdown_function([$this, 'purgeReValidation']);
+ }
+
+ /**
+ * @param Client $client
+ */
+ public function setClient(Client $client)
+ {
+ $this->client = $client;
+ }
+
+ /**
+ * @param CacheStrategyInterface $cacheStorage
+ */
+ public function setCacheStorage(CacheStrategyInterface $cacheStorage)
+ {
+ $this->cacheStorage = $cacheStorage;
+ }
+
+ /**
+ * @return CacheStrategyInterface
+ */
+ public function getCacheStorage()
+ {
+ return $this->cacheStorage;
+ }
+
+ /**
+ * @param array $methods
+ */
+ public function setHttpMethods(array $methods)
+ {
+ $this->httpMethods = $methods;
+ }
+
+ public function getHttpMethods()
+ {
+ return $this->httpMethods;
+ }
+
+ /**
+ * Will be called at the end of the script.
+ */
+ public function purgeReValidation()
+ {
+ \GuzzleHttp\Promise\inspect_all($this->waitingRevalidate);
+ }
+
+ /**
+ * @param callable $handler
+ *
+ * @return callable
+ */
+ public function __invoke(callable $handler)
+ {
+ return function (RequestInterface $request, array $options) use (&$handler) {
+ if (!isset($this->httpMethods[strtoupper($request->getMethod())])) {
+ // No caching for this method allowed
+
+ return $handler($request, $options)->then(
+ function (ResponseInterface $response) use ($request) {
+ if (!isset($this->safeMethods[$request->getMethod()])) {
+ // Invalidate cache after a call of non-safe method on the same URI
+ $response = $this->invalidateCache($request, $response);
+ }
+
+ return $response->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_MISS);
+ }
+ );
+ }
+
+ if ($request->hasHeader(self::HEADER_RE_VALIDATION)) {
+ // It's a re-validation request, so bypass the cache!
+ return $handler($request->withoutHeader(self::HEADER_RE_VALIDATION), $options);
+ }
+
+ // Retrieve information from request (Cache-Control)
+ $reqCacheControl = new KeyValueHttpHeader($request->getHeader('Cache-Control'));
+ $onlyFromCache = $reqCacheControl->has('only-if-cached');
+ $staleResponse = $reqCacheControl->has('max-stale')
+ && $reqCacheControl->get('max-stale') === '';
+ $maxStaleCache = $reqCacheControl->get('max-stale', null);
+ $minFreshCache = $reqCacheControl->get('min-fresh', null);
+
+ // If cache => return new FulfilledPromise(...) with response
+ $cacheEntry = $this->cacheStorage->fetch($request);
+ if ($cacheEntry instanceof CacheEntry) {
+ $body = $cacheEntry->getResponse()->getBody();
+ if ($body->tell() > 0) {
+ $body->rewind();
+ }
+
+ if ($cacheEntry->isFresh()
+ && ($minFreshCache === null || $cacheEntry->getStaleAge() + (int)$minFreshCache <= 0)
+ ) {
+ // Cache HIT!
+ return new FulfilledPromise(
+ $cacheEntry->getResponse()->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_HIT)
+ );
+ } elseif ($staleResponse
+ || ($maxStaleCache !== null && $cacheEntry->getStaleAge() <= $maxStaleCache)
+ ) {
+ // Staled cache!
+ return new FulfilledPromise(
+ $cacheEntry->getResponse()->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_HIT)
+ );
+ } elseif ($cacheEntry->hasValidationInformation() && !$onlyFromCache) {
+ // Re-validation header
+ $request = static::getRequestWithReValidationHeader($request, $cacheEntry);
+
+ if ($cacheEntry->staleWhileValidate()) {
+ static::addReValidationRequest($request, $this->cacheStorage, $cacheEntry);
+
+ return new FulfilledPromise(
+ $cacheEntry->getResponse()
+ ->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_STALE)
+ );
+ }
+ }
+ } else {
+ $cacheEntry = null;
+ }
+
+ if ($cacheEntry === null && $onlyFromCache) {
+ // Explicit asking of a cached response => 504
+ return new FulfilledPromise(
+ new Response(504)
+ );
+ }
+
+ /** @var Promise $promise */
+ $promise = $handler($request, $options);
+
+ return $promise->then(
+ function (ResponseInterface $response) use ($request, $cacheEntry) {
+ // Check if error and looking for a staled content
+ if ($response->getStatusCode() >= 500) {
+ $responseStale = static::getStaleResponse($cacheEntry);
+ if ($responseStale instanceof ResponseInterface) {
+ return $responseStale;
+ }
+ }
+
+ $update = false;
+
+ if ($response->getStatusCode() == 304 && $cacheEntry instanceof CacheEntry) {
+ // Not modified => cache entry is re-validate
+ /** @var ResponseInterface $response */
+ $response = $response
+ ->withStatus($cacheEntry->getResponse()->getStatusCode())
+ ->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_HIT);
+ $response = $response->withBody($cacheEntry->getResponse()->getBody());
+
+ // Merge headers of the "304 Not Modified" and the cache entry
+ /**
+ * @var string $headerName
+ * @var string[] $headerValue
+ */
+ foreach ($cacheEntry->getOriginalResponse()->getHeaders() as $headerName => $headerValue) {
+ if (!$response->hasHeader($headerName) && $headerName !== self::HEADER_CACHE_INFO) {
+ $response = $response->withHeader($headerName, $headerValue);
+ }
+ }
+
+ $update = true;
+ } else {
+ $response = $response->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_MISS);
+ }
+
+ return static::addToCache($this->cacheStorage, $request, $response, $update);
+ },
+ function ($reason) use ($cacheEntry) {
+ if ($reason instanceof TransferException) {
+ $response = static::getStaleResponse($cacheEntry);
+ if ($response instanceof ResponseInterface) {
+ return $response;
+ }
+ }
+
+ return new RejectedPromise($reason);
+ }
+ );
+ };
+ }
+
+ /**
+ * @param CacheStrategyInterface $cache
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @param bool $update cache
+ * @return ResponseInterface
+ */
+ protected static function addToCache(
+ CacheStrategyInterface $cache,
+ RequestInterface $request,
+ ResponseInterface $response,
+ $update = false
+ ) {
+ $body = $response->getBody();
+
+ // If the body is not seekable, we have to replace it by a seekable one
+ if (!$body->isSeekable()) {
+ $response = $response->withBody(
+ \GuzzleHttp\Psr7\Utils::streamFor($body->getContents())
+ );
+ }
+
+ if ($update) {
+ $cache->update($request, $response);
+ } else {
+ $cache->cache($request, $response);
+ }
+
+ // always rewind back to the start otherwise other middlewares may get empty "content"
+ if ($body->isSeekable()) {
+ $response->getBody()->rewind();
+ }
+
+ return $response;
+ }
+
+ /**
+ * @param RequestInterface $request
+ * @param CacheStrategyInterface $cacheStorage
+ * @param CacheEntry $cacheEntry
+ *
+ * @return bool if added
+ */
+ protected function addReValidationRequest(
+ RequestInterface $request,
+ CacheStrategyInterface &$cacheStorage,
+ CacheEntry $cacheEntry
+ ) {
+ // Add the promise for revalidate
+ if ($this->client !== null) {
+ /** @var RequestInterface $request */
+ $request = $request->withHeader(self::HEADER_RE_VALIDATION, '1');
+ $this->waitingRevalidate[] = $this->client
+ ->sendAsync($request)
+ ->then(function (ResponseInterface $response) use ($request, &$cacheStorage, $cacheEntry) {
+ $update = false;
+
+ if ($response->getStatusCode() == 304) {
+ // Not modified => cache entry is re-validate
+ /** @var ResponseInterface $response */
+ $response = $response->withStatus($cacheEntry->getResponse()->getStatusCode());
+ $response = $response->withBody($cacheEntry->getResponse()->getBody());
+
+ // Merge headers of the "304 Not Modified" and the cache entry
+ foreach ($cacheEntry->getResponse()->getHeaders() as $headerName => $headerValue) {
+ if (!$response->hasHeader($headerName)) {
+ $response = $response->withHeader($headerName, $headerValue);
+ }
+ }
+
+ $update = true;
+ }
+
+ static::addToCache($cacheStorage, $request, $response, $update);
+ });
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @param CacheEntry|null $cacheEntry
+ *
+ * @return null|ResponseInterface
+ */
+ protected static function getStaleResponse(CacheEntry $cacheEntry = null)
+ {
+ // Return staled cache entry if we can
+ if ($cacheEntry instanceof CacheEntry && $cacheEntry->serveStaleIfError()) {
+ return $cacheEntry->getResponse()
+ ->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_STALE);
+ }
+
+ return;
+ }
+
+ /**
+ * @param RequestInterface $request
+ * @param CacheEntry $cacheEntry
+ *
+ * @return RequestInterface
+ */
+ protected static function getRequestWithReValidationHeader(RequestInterface $request, CacheEntry $cacheEntry)
+ {
+ if ($cacheEntry->getResponse()->hasHeader('Last-Modified')) {
+ $request = $request->withHeader(
+ 'If-Modified-Since',
+ $cacheEntry->getResponse()->getHeader('Last-Modified')
+ );
+ }
+ if ($cacheEntry->getResponse()->hasHeader('Etag')) {
+ $request = $request->withHeader(
+ 'If-None-Match',
+ $cacheEntry->getResponse()->getHeader('Etag')
+ );
+ }
+
+ return $request;
+ }
+
+ /**
+ * @param CacheStrategyInterface|null $cacheStorage
+ *
+ * @return CacheMiddleware the Middleware for Guzzle HandlerStack
+ *
+ * @deprecated Use constructor => `new CacheMiddleware()`
+ */
+ public static function getMiddleware(CacheStrategyInterface $cacheStorage = null)
+ {
+ return new self($cacheStorage);
+ }
+
+ /**
+ * @param RequestInterface $request
+ *
+ * @param ResponseInterface $response
+ *
+ * @return ResponseInterface
+ */
+ private function invalidateCache(RequestInterface $request, ResponseInterface $response)
+ {
+ foreach (array_keys($this->httpMethods) as $method) {
+ $this->cacheStorage->delete($request->withMethod($method));
+ }
+
+ return $response->withHeader(self::HEADER_INVALIDATION, true);
+ }
+}
diff --git a/lib/guzzlehttp/kevinrob/guzzlecache/src/KeyValueHttpHeader.php b/lib/guzzlehttp/kevinrob/guzzlecache/src/KeyValueHttpHeader.php
new file mode 100644
index 00000000000..d931d1ac302
--- /dev/null
+++ b/lib/guzzlehttp/kevinrob/guzzlecache/src/KeyValueHttpHeader.php
@@ -0,0 +1,131 @@
+@\,;\:\\\\"\/\[\]\?\=\{\}\x7F]+)(?:\=(?:([^\x00-\x20\(\)<>@\,;\:\\\\"\/\[\]\?\=\{\}\x7F]+)|(?:\"((?:[^"\\\\]|\\\\.)*)\")))?/';
+
+ /**
+ * @var string[]
+ */
+ protected $values = [];
+
+ /**
+ * @param array $values
+ */
+ public function __construct(array $values)
+ {
+ foreach ($values as $value) {
+ $matches = [];
+ if (preg_match_all(self::REGEX_SPLIT, $value, $matches, PREG_SET_ORDER)) {
+ foreach ($matches as $match) {
+ $val = '';
+ if (count($match) == 3) {
+ $val = $match[2];
+ } elseif (count($match) > 3) {
+ $val = $match[3];
+ }
+
+ $this->values[$match[1]] = $val;
+ }
+ }
+ }
+ }
+
+ /**
+ * @param string $key
+ *
+ * @return bool
+ */
+ public function has($key)
+ {
+ // For performance, we can use isset,
+ // but it will not match if value == 0
+ return isset($this->values[$key]) || array_key_exists($key, $this->values);
+ }
+
+ /**
+ * @param string $key
+ * @param string $default the value to return if don't exist
+ * @return string
+ */
+ public function get($key, $default = '')
+ {
+ if ($this->has($key)) {
+ return $this->values[$key];
+ }
+
+ return $default;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isEmpty()
+ {
+ return count($this->values) === 0;
+ }
+
+ /**
+ * Return the current element
+ * @link http://php.net/manual/en/iterator.current.php
+ * @return mixed Can return any type.
+ * @since 5.0.0
+ */
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ return current($this->values);
+ }
+
+ /**
+ * Move forward to next element
+ * @link http://php.net/manual/en/iterator.next.php
+ * @return void Any returned value is ignored.
+ * @since 5.0.0
+ */
+ public function next(): void
+ {
+ next($this->values);
+ }
+
+ /**
+ * Return the key of the current element
+ * @link http://php.net/manual/en/iterator.key.php
+ * @return mixed scalar on success, or null on failure.
+ * @since 5.0.0
+ *
+ */
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ return key($this->values);
+ }
+
+ /**
+ * Checks if current position is valid
+ * @link http://php.net/manual/en/iterator.valid.php
+ * @return boolean The return value will be casted to boolean and then evaluated.
+ * Returns true on success or false on failure.
+ * @since 5.0.0
+ */
+ public function valid(): bool
+ {
+ return key($this->values) !== null;
+ }
+
+ /**
+ * Rewind the Iterator to the first element
+ * @link http://php.net/manual/en/iterator.rewind.php
+ * @return void Any returned value is ignored.
+ * @since 5.0.0
+ */
+ public function rewind(): void
+ {
+ reset($this->values);
+ }
+}
diff --git a/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/CacheStorageInterface.php b/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/CacheStorageInterface.php
new file mode 100644
index 00000000000..0ec9e1af08a
--- /dev/null
+++ b/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/CacheStorageInterface.php
@@ -0,0 +1,30 @@
+cache = $cache;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function fetch($key)
+ {
+ try {
+ $cache = unserialize(gzuncompress($this->cache->fetch($key)));
+ if ($cache instanceof CacheEntry) {
+ return $cache;
+ }
+ } catch (\Exception $ignored) {
+ return;
+ }
+
+ return;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save($key, CacheEntry $data)
+ {
+ try {
+ $lifeTime = $data->getTTL();
+ if ($lifeTime >= 0) {
+ return $this->cache->save(
+ $key,
+ gzcompress(serialize($data)),
+ $lifeTime
+ );
+ }
+ } catch (\Exception $ignored) {
+ // No fail if we can't save it the storage
+ }
+
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete($key)
+ {
+ try {
+ return $this->cache->delete($key);
+ } catch (\Exception $ignored) {
+ // Don't fail if we can't delete it
+ }
+
+ return false;
+ }
+}
diff --git a/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/DoctrineCacheStorage.php b/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/DoctrineCacheStorage.php
new file mode 100644
index 00000000000..9f5f1609511
--- /dev/null
+++ b/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/DoctrineCacheStorage.php
@@ -0,0 +1,74 @@
+cache = $cache;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function fetch($key)
+ {
+ try {
+ $cache = unserialize($this->cache->fetch($key));
+ if ($cache instanceof CacheEntry) {
+ return $cache;
+ }
+ } catch (\Exception $ignored) {
+ return;
+ }
+
+ return;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save($key, CacheEntry $data)
+ {
+ try {
+ $lifeTime = $data->getTTL();
+ if ($lifeTime >= 0) {
+ return $this->cache->save(
+ $key,
+ serialize($data),
+ $lifeTime
+ );
+ }
+ } catch (\Exception $ignored) {
+ // No fail if we can't save it the storage
+ }
+
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete($key)
+ {
+ try {
+ return $this->cache->delete($key);
+ } catch (\Exception $ignored) {
+ // Don't fail if we can't delete it
+ }
+
+ return false;
+ }
+}
diff --git a/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/FlysystemStorage.php b/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/FlysystemStorage.php
new file mode 100644
index 00000000000..e2db2e922d7
--- /dev/null
+++ b/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/FlysystemStorage.php
@@ -0,0 +1,61 @@
+filesystem = new Filesystem($adapter);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function fetch($key)
+ {
+ if ($this->filesystem->has($key)) {
+ // The file exist, read it!
+ $data = @unserialize(
+ $this->filesystem->read($key)
+ );
+
+ if ($data instanceof CacheEntry) {
+ return $data;
+ }
+ }
+
+ return;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function save($key, CacheEntry $data)
+ {
+ return $this->filesystem->put($key, serialize($data));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete($key)
+ {
+ try {
+ return $this->filesystem->delete($key);
+ } catch (FileNotFoundException $ex) {
+ return true;
+ }
+ }
+}
diff --git a/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/LaravelCacheStorage.php b/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/LaravelCacheStorage.php
new file mode 100644
index 00000000000..3e3416d95f2
--- /dev/null
+++ b/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/LaravelCacheStorage.php
@@ -0,0 +1,84 @@
+cache = $cache;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function fetch($key)
+ {
+ try {
+ $cache = unserialize($this->cache->get($key, ''));
+ if ($cache instanceof CacheEntry) {
+ return $cache;
+ }
+ } catch (\Exception $ignored) {
+ return;
+ }
+
+ return;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save($key, CacheEntry $data)
+ {
+ try {
+ $lifeTime = $this->getLifeTime($data);
+ if ($lifeTime === 0) {
+ return $this->cache->forever(
+ $key,
+ serialize($data)
+ );
+ } else if ($lifeTime > 0) {
+ return $this->cache->add(
+ $key,
+ serialize($data),
+ $lifeTime
+ );
+ }
+ } catch (\Exception $ignored) {
+ // No fail if we can't save it the storage
+ }
+
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete($key)
+ {
+ return $this->cache->forget($key);
+ }
+
+ protected function getLifeTime(CacheEntry $data)
+ {
+ $version = app()->version();
+ if (preg_match('/^\d+(\.\d+)?(\.\d+)?/', $version) && version_compare($version, '5.8.0') < 0) {
+ // getTTL returns seconds, Laravel needs minutes before v5.8
+ return $data->getTTL() / 60;
+ }
+
+ return $data->getTTL();
+ }
+}
diff --git a/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/Psr16CacheStorage.php b/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/Psr16CacheStorage.php
new file mode 100644
index 00000000000..fe97a257087
--- /dev/null
+++ b/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/Psr16CacheStorage.php
@@ -0,0 +1,52 @@
+cache = $cache;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function fetch($key)
+ {
+ $data = $this->cache->get($key);
+ if ($data instanceof CacheEntry) {
+ return $data;
+ }
+
+ return null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save($key, CacheEntry $data)
+ {
+ $ttl = $data->getTTL();
+ if ($ttl === 0) {
+ return $this->cache->set($key, $data);
+ }
+ return $this->cache->set($key, $data, $data->getTTL());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete($key)
+ {
+ return $this->cache->delete($key);
+ }
+}
diff --git a/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/Psr6CacheStorage.php b/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/Psr6CacheStorage.php
new file mode 100644
index 00000000000..b1344ef4620
--- /dev/null
+++ b/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/Psr6CacheStorage.php
@@ -0,0 +1,92 @@
+cachePool = $cachePool;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function fetch($key)
+ {
+ $item = $this->cachePool->getItem($key);
+ $this->lastItem = $item;
+
+ $cache = $item->get();
+
+ if ($cache instanceof CacheEntry) {
+ return $cache;
+ }
+
+ return null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save($key, CacheEntry $data)
+ {
+ if ($this->lastItem && $this->lastItem->getKey() == $key) {
+ $item = $this->lastItem;
+ } else {
+ $item = $this->cachePool->getItem($key);
+ }
+
+ $this->lastItem = null;
+
+ $item->set($data);
+
+ $ttl = $data->getTTL();
+ if ($ttl === 0) {
+ // No expiration
+ $item->expiresAfter(null);
+ } else {
+ $item->expiresAfter($ttl);
+ }
+
+ return $this->cachePool->save($item);
+ }
+
+ /**
+ * @param string $key
+ *
+ * @return bool
+ */
+ public function delete($key)
+ {
+ if (null !== $this->lastItem && $this->lastItem->getKey() === $key) {
+ $this->lastItem = null;
+ }
+
+ return $this->cachePool->deleteItem($key);
+ }
+}
diff --git a/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/VolatileRuntimeStorage.php b/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/VolatileRuntimeStorage.php
new file mode 100644
index 00000000000..ec77d30f82e
--- /dev/null
+++ b/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/VolatileRuntimeStorage.php
@@ -0,0 +1,60 @@
+cache[$key])) {
+ return $this->cache[$key];
+ }
+
+ return;
+ }
+
+ /**
+ * @param string $key
+ * @param CacheEntry $data
+ *
+ * @return bool
+ */
+ public function save($key, CacheEntry $data)
+ {
+ $this->cache[$key] = $data;
+
+ return true;
+ }
+
+ /**
+ * @param string $key
+ *
+ * @return bool
+ */
+ public function delete($key)
+ {
+ if (true === array_key_exists($key, $this->cache)) {
+ unset($this->cache[$key]);
+
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/WordPressObjectCacheStorage.php b/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/WordPressObjectCacheStorage.php
new file mode 100644
index 00000000000..c81e739d91f
--- /dev/null
+++ b/lib/guzzlehttp/kevinrob/guzzlecache/src/Storage/WordPressObjectCacheStorage.php
@@ -0,0 +1,73 @@
+group = $group;
+ }
+
+ /**
+ * @param string $key
+ *
+ * @return CacheEntry|null the data or false
+ */
+ public function fetch($key)
+ {
+ try {
+ $cache = unserialize(wp_cache_get($key, $this->group));
+ if ($cache instanceof CacheEntry) {
+ return $cache;
+ }
+ } catch (\Exception $ignored) {
+ // Don't fail if we can't load it
+ }
+
+ return null;
+ }
+
+ /**
+ * @param string $key
+ * @param CacheEntry $data
+ *
+ * @return bool
+ */
+ public function save($key, CacheEntry $data)
+ {
+ try {
+ return wp_cache_set($key, serialize($data), $this->group, $data->getTTL());
+ } catch (\Exception $ignored) {
+ // Don't fail if we can't save it
+ }
+
+ return false;
+ }
+
+ /**
+ * @param string $key
+ *
+ * @return bool
+ */
+ public function delete($key)
+ {
+ try {
+ return wp_cache_delete($key, $this->group);
+ } catch (\Exception $ignored) {
+ // Don't fail if we can't delete it
+ }
+
+ return false;
+ }
+}
diff --git a/lib/guzzlehttp/kevinrob/guzzlecache/src/Strategy/CacheStrategyInterface.php b/lib/guzzlehttp/kevinrob/guzzlecache/src/Strategy/CacheStrategyInterface.php
new file mode 100644
index 00000000000..a88df2d1c67
--- /dev/null
+++ b/lib/guzzlehttp/kevinrob/guzzlecache/src/Strategy/CacheStrategyInterface.php
@@ -0,0 +1,42 @@
+defaultCacheStrategy = $defaultCacheStrategy ?: new NullCacheStrategy();
+ }
+
+ /**
+ * @param CacheStrategyInterface $defaultCacheStrategy
+ */
+ public function setDefaultCacheStrategy(CacheStrategyInterface $defaultCacheStrategy)
+ {
+ $this->defaultCacheStrategy = $defaultCacheStrategy;
+ }
+
+ /**
+ * @param RequestMatcherInterface $requestMatcher
+ * @param CacheStrategyInterface $cacheStrategy
+ */
+ final public function registerRequestMatcher(RequestMatcherInterface $requestMatcher, CacheStrategyInterface $cacheStrategy)
+ {
+ $this->requestMatchers[] = [
+ $requestMatcher,
+ $cacheStrategy,
+ ];
+ }
+
+ /**
+ * @param RequestInterface $request
+ * @return CacheStrategyInterface
+ */
+ private function getStrategyFor(RequestInterface $request)
+ {
+ /**
+ * @var RequestMatcherInterface $requestMatcher
+ * @var CacheStrategyInterface $cacheStrategy
+ */
+ foreach ($this->requestMatchers as $requestMatcher) {
+ list($requestMatcher, $cacheStrategy) = $requestMatcher;
+ if ($requestMatcher->matches($request)) {
+ return $cacheStrategy;
+ }
+ }
+
+ return $this->defaultCacheStrategy;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function fetch(RequestInterface $request)
+ {
+ return $this->getStrategyFor($request)->fetch($request);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function cache(RequestInterface $request, ResponseInterface $response)
+ {
+ return $this->getStrategyFor($request)->cache($request, $response);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function update(RequestInterface $request, ResponseInterface $response)
+ {
+ return $this->getStrategyFor($request)->update($request, $response);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete(RequestInterface $request)
+ {
+ return $this->getStrategyFor($request)->delete($request);
+ }
+}
diff --git a/lib/guzzlehttp/kevinrob/guzzlecache/src/Strategy/Delegate/RequestMatcherInterface.php b/lib/guzzlehttp/kevinrob/guzzlecache/src/Strategy/Delegate/RequestMatcherInterface.php
new file mode 100644
index 00000000000..5890bc22344
--- /dev/null
+++ b/lib/guzzlehttp/kevinrob/guzzlecache/src/Strategy/Delegate/RequestMatcherInterface.php
@@ -0,0 +1,15 @@
+defaultTtl = $defaultTtl;
+ $this->varyHeaders = $varyHeaders;
+ parent::__construct($cache);
+ }
+
+ protected function getCacheKey(RequestInterface $request, KeyValueHttpHeader $varyHeaders = null)
+ {
+ if (null === $varyHeaders || $varyHeaders->isEmpty()) {
+ return hash(
+ 'sha256',
+ 'greedy'.$request->getMethod().$request->getUri()
+ );
+ }
+
+ $cacheHeaders = [];
+ foreach ($varyHeaders as $key => $value) {
+ if ($request->hasHeader($key)) {
+ $cacheHeaders[$key] = $request->getHeader($key);
+ }
+ }
+
+ return hash(
+ 'sha256',
+ 'greedy'.$request->getMethod().$request->getUri().json_encode($cacheHeaders)
+ );
+ }
+
+ public function cache(RequestInterface $request, ResponseInterface $response)
+ {
+ $warningMessage = sprintf('%d - "%s" "%s"',
+ 299,
+ 'Cached although the response headers indicate not to do it!',
+ (new \DateTime())->format(\DateTime::RFC1123)
+ );
+
+ $response = $response->withAddedHeader('Warning', $warningMessage);
+
+ if ($cacheObject = $this->getCacheObject($request, $response)) {
+ return $this->storage->save(
+ $this->getCacheKey($request, $this->varyHeaders),
+ $cacheObject
+ );
+ }
+
+ return false;
+ }
+
+ protected function getCacheObject(RequestInterface $request, ResponseInterface $response)
+ {
+ if (!array_key_exists($response->getStatusCode(), $this->statusAccepted)) {
+ // Don't cache it
+ return null;
+ }
+
+ if (null !== $this->varyHeaders && $this->varyHeaders->has('*')) {
+ // This will never match with a request
+ return;
+ }
+
+ $response = $response->withoutHeader('Etag')->withoutHeader('Last-Modified');
+
+ $ttl = $this->defaultTtl;
+ if ($request->hasHeader(self::HEADER_TTL)) {
+ $ttlHeaderValues = $request->getHeader(self::HEADER_TTL);
+ $ttl = (int)reset($ttlHeaderValues);
+ }
+
+ return new CacheEntry($request->withoutHeader(self::HEADER_TTL), $response, new \DateTime(sprintf('+%d seconds', $ttl)));
+ }
+
+ public function fetch(RequestInterface $request)
+ {
+ $cache = $this->storage->fetch($this->getCacheKey($request, $this->varyHeaders));
+ return $cache;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete(RequestInterface $request)
+ {
+ return $this->storage->delete($this->getCacheKey($request));
+ }
+}
diff --git a/lib/guzzlehttp/kevinrob/guzzlecache/src/Strategy/NullCacheStrategy.php b/lib/guzzlehttp/kevinrob/guzzlecache/src/Strategy/NullCacheStrategy.php
new file mode 100644
index 00000000000..22e3929a885
--- /dev/null
+++ b/lib/guzzlehttp/kevinrob/guzzlecache/src/Strategy/NullCacheStrategy.php
@@ -0,0 +1,43 @@
+ 200,
+ 203 => 203,
+ 204 => 204,
+ 300 => 300,
+ 301 => 301,
+ 404 => 404,
+ 405 => 405,
+ 410 => 410,
+ 414 => 414,
+ 418 => 418,
+ 501 => 501,
+ ];
+
+ /**
+ * @var string[]
+ */
+ protected $ageKey = [
+ 'max-age',
+ ];
+
+ public function __construct(CacheStorageInterface $cache = null)
+ {
+ $this->storage = $cache !== null ? $cache : new VolatileRuntimeStorage();
+ }
+
+ /**
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return CacheEntry|null entry to save, null if can't cache it
+ */
+ protected function getCacheObject(RequestInterface $request, ResponseInterface $response)
+ {
+ if (!isset($this->statusAccepted[$response->getStatusCode()])) {
+ // Don't cache it
+ return;
+ }
+
+ $cacheControl = new KeyValueHttpHeader($response->getHeader('Cache-Control'));
+ $varyHeader = new KeyValueHttpHeader($response->getHeader('Vary'));
+
+ if ($varyHeader->has('*')) {
+ // This will never match with a request
+ return;
+ }
+
+ if ($cacheControl->has('no-store')) {
+ // No store allowed (maybe some sensitives data...)
+ return;
+ }
+
+ if ($cacheControl->has('no-cache')) {
+ // Stale response see RFC7234 section 5.2.1.4
+ $entry = new CacheEntry($request, $response, new \DateTime('-1 seconds'));
+
+ return $entry->hasValidationInformation() ? $entry : null;
+ }
+
+ foreach ($this->ageKey as $key) {
+ if ($cacheControl->has($key)) {
+ return new CacheEntry(
+ $request,
+ $response,
+ new \DateTime('+'.(int) $cacheControl->get($key).'seconds')
+ );
+ }
+ }
+
+ if ($response->hasHeader('Expires')) {
+ $expireAt = \DateTime::createFromFormat(\DateTime::RFC1123, $response->getHeaderLine('Expires'));
+ if ($expireAt !== false) {
+ return new CacheEntry(
+ $request,
+ $response,
+ $expireAt
+ );
+ }
+ }
+
+ return new CacheEntry($request, $response, new \DateTime('-1 seconds'));
+ }
+
+ /**
+ * Generate a key for the response cache.
+ *
+ * @param RequestInterface $request
+ * @param null|KeyValueHttpHeader $varyHeaders The vary headers which should be honoured by the cache (optional)
+ *
+ * @return string
+ */
+ protected function getCacheKey(RequestInterface $request, KeyValueHttpHeader $varyHeaders = null)
+ {
+ if (!$varyHeaders) {
+ return hash('sha256', $request->getMethod().$request->getUri());
+ }
+
+ $cacheHeaders = [];
+
+ foreach ($varyHeaders as $key => $value) {
+ if ($request->hasHeader($key)) {
+ $cacheHeaders[$key] = $request->getHeader($key);
+ }
+ }
+
+ return hash('sha256', $request->getMethod().$request->getUri().json_encode($cacheHeaders));
+ }
+
+ /**
+ * Return a CacheEntry or null if no cache.
+ *
+ * @param RequestInterface $request
+ *
+ * @return CacheEntry|null
+ */
+ public function fetch(RequestInterface $request)
+ {
+ /** @var int|null $maxAge */
+ $maxAge = null;
+
+ if ($request->hasHeader('Cache-Control')) {
+ $reqCacheControl = new KeyValueHttpHeader($request->getHeader('Cache-Control'));
+ if ($reqCacheControl->has('no-cache')) {
+ // Can't return cache
+ return null;
+ }
+
+ $maxAge = $reqCacheControl->get('max-age', null);
+ } elseif ($request->hasHeader('Pragma')) {
+ $pragma = new KeyValueHttpHeader($request->getHeader('Pragma'));
+ if ($pragma->has('no-cache')) {
+ // Can't return cache
+ return null;
+ }
+ }
+
+ $cache = $this->storage->fetch($this->getCacheKey($request));
+ if ($cache !== null) {
+ $varyHeaders = $cache->getVaryHeaders();
+
+ // vary headers exist from a previous response, check if we have a cache that matches those headers
+ if (!$varyHeaders->isEmpty()) {
+ $cache = $this->storage->fetch($this->getCacheKey($request, $varyHeaders));
+
+ if (!$cache) {
+ return null;
+ }
+ }
+
+ if ((string)$cache->getOriginalRequest()->getUri() !== (string)$request->getUri()) {
+ return null;
+ }
+
+ if ($maxAge !== null) {
+ if ($cache->getAge() > $maxAge) {
+ // Cache entry is too old for the request requirements!
+ return null;
+ }
+ }
+
+ if (!$cache->isVaryEquals($request)) {
+ return null;
+ }
+ }
+
+ return $cache;
+ }
+
+ /**
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ *
+ * @return bool true if success
+ */
+ public function cache(RequestInterface $request, ResponseInterface $response)
+ {
+ $reqCacheControl = new KeyValueHttpHeader($request->getHeader('Cache-Control'));
+ if ($reqCacheControl->has('no-store')) {
+ // No caching allowed
+ return false;
+ }
+
+ $cacheObject = $this->getCacheObject($request, $response);
+ if ($cacheObject !== null) {
+ // store the cache against the URI-only key
+ $success = $this->storage->save(
+ $this->getCacheKey($request),
+ $cacheObject
+ );
+
+ $varyHeaders = $cacheObject->getVaryHeaders();
+
+ if (!$varyHeaders->isEmpty()) {
+ // also store the cache against the vary headers based key
+ $success = $this->storage->save(
+ $this->getCacheKey($request, $varyHeaders),
+ $cacheObject
+ );
+ }
+
+ return $success;
+ }
+
+ return false;
+ }
+
+ /**
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ *
+ * @return bool true if success
+ */
+ public function update(RequestInterface $request, ResponseInterface $response)
+ {
+ return $this->cache($request, $response);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete(RequestInterface $request)
+ {
+ return $this->storage->delete($this->getCacheKey($request));
+ }
+}
diff --git a/lib/guzzlehttp/kevinrob/guzzlecache/src/Strategy/PublicCacheStrategy.php b/lib/guzzlehttp/kevinrob/guzzlecache/src/Strategy/PublicCacheStrategy.php
new file mode 100644
index 00000000000..873ac2c123a
--- /dev/null
+++ b/lib/guzzlehttp/kevinrob/guzzlecache/src/Strategy/PublicCacheStrategy.php
@@ -0,0 +1,42 @@
+ageKey, 's-maxage');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getCacheObject(RequestInterface $request, ResponseInterface $response)
+ {
+ $cacheControl = new KeyValueHttpHeader($response->getHeader('Cache-Control'));
+ if ($cacheControl->has('private')) {
+ return;
+ }
+
+ return parent::getCacheObject($request, $response);
+ }
+}
diff --git a/lib/guzzlehttp/readme_moodle.txt b/lib/guzzlehttp/readme_moodle.txt
index 38de60367c4..ae933f96c91 100644
--- a/lib/guzzlehttp/readme_moodle.txt
+++ b/lib/guzzlehttp/readme_moodle.txt
@@ -14,3 +14,8 @@ Update guzzle psr-7 message implementation library
1. Download the latest psr-7 library from https://github.com/guzzle/psr7
2. Copy the src directory to lib/guzzlehttp/psr-7 folder
3. Copy the associated files LICENCE, README.md etc. to psr-7 directory
+
+Update guzzle cache implementation library
+1. Download the latest Guzzle cache middleware library from https://github.com/Kevinrob/guzzle-cache-middleware
+2. Copy the src directory to guzzlehttp/kevinrob/guzzlecache folder
+3. Copy the associated files LICENCE, README.md etc. to guzzlecache directory
diff --git a/lib/thirdpartylibs.xml b/lib/thirdpartylibs.xml
index 4f1c617ce3f..04fe52563ec 100644
--- a/lib/thirdpartylibs.xml
+++ b/lib/thirdpartylibs.xml
@@ -682,4 +682,12 @@ All rights reserved.
MIT
https://github.com/symfony/deprecation-contracts
+
+ guzzlehttp/kevinrob/guzzlecache
+ Guzzle cache middleware
+ A HTTP Cache for Guzzle as a simple Middleware to be added in the HandlerStack.
+ 4.0.2
+ MIT
+ https://github.com/Kevinrob/guzzle-cache-middleware
+