Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.
   1  <?php
   2  
   3  namespace Kevinrob\GuzzleCache\Strategy;
   4  
   5  use Kevinrob\GuzzleCache\CacheEntry;
   6  use Kevinrob\GuzzleCache\KeyValueHttpHeader;
   7  use Kevinrob\GuzzleCache\Storage\CacheStorageInterface;
   8  use Kevinrob\GuzzleCache\Storage\VolatileRuntimeStorage;
   9  use Psr\Http\Message\RequestInterface;
  10  use Psr\Http\Message\ResponseInterface;
  11  
  12  /**
  13   * This strategy represents a "private" HTTP client.
  14   * Pay attention to share storage between application with caution!
  15   *
  16   * For example, a response with cache-control header "private, max-age=60"
  17   * will be cached by this strategy.
  18   *
  19   * The rules applied are from RFC 7234.
  20   *
  21   * @see https://tools.ietf.org/html/rfc7234
  22   */
  23  class PrivateCacheStrategy implements CacheStrategyInterface
  24  {
  25      /**
  26       * @var CacheStorageInterface
  27       */
  28      protected $storage;
  29  
  30      /**
  31       * @var int[]
  32       */
  33      protected $statusAccepted = [
  34          200 => 200,
  35          203 => 203,
  36          204 => 204,
  37          300 => 300,
  38          301 => 301,
  39          404 => 404,
  40          405 => 405,
  41          410 => 410,
  42          414 => 414,
  43          418 => 418,
  44          501 => 501,
  45      ];
  46  
  47      /**
  48       * @var string[]
  49       */
  50      protected $ageKey = [
  51          'max-age',
  52      ];
  53  
  54      public function __construct(CacheStorageInterface $cache = null)
  55      {
  56          $this->storage = $cache !== null ? $cache : new VolatileRuntimeStorage();
  57      }
  58  
  59      /**
  60       * @param RequestInterface $request
  61       * @param ResponseInterface $response
  62       * @return CacheEntry|null entry to save, null if can't cache it
  63       */
  64      protected function getCacheObject(RequestInterface $request, ResponseInterface $response)
  65      {
  66          if (!isset($this->statusAccepted[$response->getStatusCode()])) {
  67              // Don't cache it
  68              return;
  69          }
  70  
  71          $cacheControl = new KeyValueHttpHeader($response->getHeader('Cache-Control'));
  72          $varyHeader = new KeyValueHttpHeader($response->getHeader('Vary'));
  73  
  74          if ($varyHeader->has('*')) {
  75              // This will never match with a request
  76              return;
  77          }
  78  
  79          if ($cacheControl->has('no-store')) {
  80              // No store allowed (maybe some sensitives data...)
  81              return;
  82          }
  83  
  84          if ($cacheControl->has('no-cache')) {
  85              // Stale response see RFC7234 section 5.2.1.4
  86              $entry = new CacheEntry($request, $response, new \DateTime('-1 seconds'));
  87  
  88              return $entry->hasValidationInformation() ? $entry : null;
  89          }
  90  
  91          foreach ($this->ageKey as $key) {
  92              if ($cacheControl->has($key)) {
  93                  return new CacheEntry(
  94                      $request,
  95                      $response,
  96                      new \DateTime('+'.(int) $cacheControl->get($key).'seconds')
  97                  );
  98              }
  99          }
 100  
 101          if ($response->hasHeader('Expires')) {
 102              $expireAt = \DateTime::createFromFormat(\DateTime::RFC1123, $response->getHeaderLine('Expires'));
 103              if ($expireAt !== false) {
 104                  return new CacheEntry(
 105                      $request,
 106                      $response,
 107                      $expireAt
 108                  );
 109              }
 110          }
 111  
 112          return new CacheEntry($request, $response, new \DateTime('-1 seconds'));
 113      }
 114  
 115      /**
 116       * Generate a key for the response cache.
 117       *
 118       * @param RequestInterface   $request
 119       * @param null|KeyValueHttpHeader $varyHeaders The vary headers which should be honoured by the cache (optional)
 120       *
 121       * @return string
 122       */
 123      protected function getCacheKey(RequestInterface $request, KeyValueHttpHeader $varyHeaders = null)
 124      {
 125          if (!$varyHeaders) {
 126              return hash('sha256', $request->getMethod().$request->getUri());
 127          }
 128  
 129          $cacheHeaders = [];
 130  
 131          foreach ($varyHeaders as $key => $value) {
 132              if ($request->hasHeader($key)) {
 133                  $cacheHeaders[$key] = $request->getHeader($key);
 134              }
 135          }
 136  
 137          return hash('sha256', $request->getMethod().$request->getUri().json_encode($cacheHeaders));
 138      }
 139  
 140      /**
 141       * Return a CacheEntry or null if no cache.
 142       *
 143       * @param RequestInterface $request
 144       *
 145       * @return CacheEntry|null
 146       */
 147      public function fetch(RequestInterface $request)
 148      {
 149          /** @var int|null $maxAge */
 150          $maxAge = null;
 151  
 152          if ($request->hasHeader('Cache-Control')) {
 153              $reqCacheControl = new KeyValueHttpHeader($request->getHeader('Cache-Control'));
 154              if ($reqCacheControl->has('no-cache')) {
 155                  // Can't return cache
 156                  return null;
 157              }
 158  
 159              $maxAge = $reqCacheControl->get('max-age', null);
 160          } elseif ($request->hasHeader('Pragma')) {
 161              $pragma = new KeyValueHttpHeader($request->getHeader('Pragma'));
 162              if ($pragma->has('no-cache')) {
 163                  // Can't return cache
 164                  return null;
 165              }
 166          }
 167  
 168          $cache = $this->storage->fetch($this->getCacheKey($request));
 169          if ($cache !== null) {
 170              $varyHeaders = $cache->getVaryHeaders();
 171  
 172              // vary headers exist from a previous response, check if we have a cache that matches those headers
 173              if (!$varyHeaders->isEmpty()) {
 174                  $cache = $this->storage->fetch($this->getCacheKey($request, $varyHeaders));
 175  
 176                  if (!$cache) {
 177                      return null;
 178                  }
 179              }
 180  
 181              if ((string)$cache->getOriginalRequest()->getUri() !== (string)$request->getUri()) {
 182                  return null;
 183              }
 184  
 185              if ($maxAge !== null) {
 186                  if ($cache->getAge() > $maxAge) {
 187                      // Cache entry is too old for the request requirements!
 188                      return null;
 189                  }
 190              }
 191  
 192              if (!$cache->isVaryEquals($request)) {
 193                  return null;
 194              }
 195          }
 196  
 197          return $cache;
 198      }
 199  
 200      /**
 201       * @param RequestInterface  $request
 202       * @param ResponseInterface $response
 203       *
 204       * @return bool true if success
 205       */
 206      public function cache(RequestInterface $request, ResponseInterface $response)
 207      {
 208          $reqCacheControl = new KeyValueHttpHeader($request->getHeader('Cache-Control'));
 209          if ($reqCacheControl->has('no-store')) {
 210              // No caching allowed
 211              return false;
 212          }
 213  
 214          $cacheObject = $this->getCacheObject($request, $response);
 215          if ($cacheObject !== null) {
 216              // store the cache against the URI-only key
 217              $success = $this->storage->save(
 218                  $this->getCacheKey($request),
 219                  $cacheObject
 220              );
 221  
 222              $varyHeaders = $cacheObject->getVaryHeaders();
 223  
 224              if (!$varyHeaders->isEmpty()) {
 225                  // also store the cache against the vary headers based key
 226                  $success = $this->storage->save(
 227                      $this->getCacheKey($request, $varyHeaders),
 228                      $cacheObject
 229                  );
 230              }
 231  
 232              return $success;
 233          }
 234  
 235          return false;
 236      }
 237  
 238      /**
 239       * @param RequestInterface $request
 240       * @param ResponseInterface $response
 241       *
 242       * @return bool true if success
 243       */
 244      public function update(RequestInterface $request, ResponseInterface $response)
 245      {
 246          return $this->cache($request, $response);
 247      }
 248  
 249      /**
 250       * {@inheritdoc}
 251       */
 252      public function delete(RequestInterface $request)
 253      {
 254          return $this->storage->delete($this->getCacheKey($request));
 255      }
 256  }