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.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402]

   1  <?php
   2  
   3  /**
   4   * SCSSPHP
   5   *
   6   * @copyright 2012-2020 Leaf Corcoran
   7   *
   8   * @license http://opensource.org/licenses/MIT MIT
   9   *
  10   * @link http://scssphp.github.io/scssphp
  11   */
  12  
  13  namespace ScssPhp\ScssPhp;
  14  
  15  use Exception;
  16  use ScssPhp\ScssPhp\Version;
  17  
  18  /**
  19   * The scss cache manager.
  20   *
  21   * In short:
  22   *
  23   * allow to put in cache/get from cache a generic result from a known operation on a generic dataset,
  24   * taking in account options that affects the result
  25   *
  26   * The cache manager is agnostic about data format and only the operation is expected to be described by string
  27   */
  28  
  29  /**
  30   * SCSS cache
  31   *
  32   * @author Cedric Morin <cedric@yterium.com>
  33   *
  34   * @internal
  35   */
  36  class Cache
  37  {
  38      const CACHE_VERSION = 1;
  39  
  40      /**
  41       * directory used for storing data
  42       *
  43       * @var string|false
  44       */
  45      public static $cacheDir = false;
  46  
  47      /**
  48       * prefix for the storing data
  49       *
  50       * @var string
  51       */
  52      public static $prefix = 'scssphp_';
  53  
  54      /**
  55       * force a refresh : 'once' for refreshing the first hit on a cache only, true to never use the cache in this hit
  56       *
  57       * @var bool|string
  58       */
  59      public static $forceRefresh = false;
  60  
  61      /**
  62       * specifies the number of seconds after which data cached will be seen as 'garbage' and potentially cleaned up
  63       *
  64       * @var int
  65       */
  66      public static $gcLifetime = 604800;
  67  
  68      /**
  69       * array of already refreshed cache if $forceRefresh==='once'
  70       *
  71       * @var array<string, bool>
  72       */
  73      protected static $refreshed = [];
  74  
  75      /**
  76       * Constructor
  77       *
  78       * @param array $options
  79       *
  80       * @phpstan-param array{cacheDir?: string, prefix?: string, forceRefresh?: string} $options
  81       */
  82      public function __construct($options)
  83      {
  84          // check $cacheDir
  85          if (isset($options['cacheDir'])) {
  86              self::$cacheDir = $options['cacheDir'];
  87          }
  88  
  89          if (empty(self::$cacheDir)) {
  90              throw new Exception('cacheDir not set');
  91          }
  92  
  93          if (isset($options['prefix'])) {
  94              self::$prefix = $options['prefix'];
  95          }
  96  
  97          if (empty(self::$prefix)) {
  98              throw new Exception('prefix not set');
  99          }
 100  
 101          if (isset($options['forceRefresh'])) {
 102              self::$forceRefresh = $options['forceRefresh'];
 103          }
 104  
 105          self::checkCacheDir();
 106      }
 107  
 108      /**
 109       * Get the cached result of $operation on $what,
 110       * which is known as dependant from the content of $options
 111       *
 112       * @param string   $operation    parse, compile...
 113       * @param mixed    $what         content key (e.g., filename to be treated)
 114       * @param array    $options      any option that affect the operation result on the content
 115       * @param int|null $lastModified last modified timestamp
 116       *
 117       * @return mixed
 118       *
 119       * @throws \Exception
 120       */
 121      public function getCache($operation, $what, $options = [], $lastModified = null)
 122      {
 123          $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options);
 124  
 125          if (
 126              ((self::$forceRefresh === false) || (self::$forceRefresh === 'once' &&
 127              isset(self::$refreshed[$fileCache]))) && file_exists($fileCache)
 128          ) {
 129              $cacheTime = filemtime($fileCache);
 130  
 131              if (
 132                  (\is_null($lastModified) || $cacheTime > $lastModified) &&
 133                  $cacheTime + self::$gcLifetime > time()
 134              ) {
 135                  $c = file_get_contents($fileCache);
 136                  $c = unserialize($c);
 137  
 138                  if (\is_array($c) && isset($c['value'])) {
 139                      return $c['value'];
 140                  }
 141              }
 142          }
 143  
 144          return null;
 145      }
 146  
 147      /**
 148       * Put in cache the result of $operation on $what,
 149       * which is known as dependant from the content of $options
 150       *
 151       * @param string $operation
 152       * @param mixed  $what
 153       * @param mixed  $value
 154       * @param array  $options
 155       *
 156       * @return void
 157       */
 158      public function setCache($operation, $what, $value, $options = [])
 159      {
 160          $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options);
 161  
 162          $c = ['value' => $value];
 163          $c = serialize($c);
 164  
 165          file_put_contents($fileCache, $c);
 166  
 167          if (self::$forceRefresh === 'once') {
 168              self::$refreshed[$fileCache] = true;
 169          }
 170      }
 171  
 172      /**
 173       * Get the cache name for the caching of $operation on $what,
 174       * which is known as dependant from the content of $options
 175       *
 176       * @param string $operation
 177       * @param mixed  $what
 178       * @param array  $options
 179       *
 180       * @return string
 181       */
 182      private static function cacheName($operation, $what, $options = [])
 183      {
 184          $t = [
 185            'version' => self::CACHE_VERSION,
 186            'scssphpVersion' => Version::VERSION,
 187            'operation' => $operation,
 188            'what' => $what,
 189            'options' => $options
 190          ];
 191  
 192          $t = self::$prefix
 193            . sha1(json_encode($t))
 194            . ".$operation"
 195            . ".scsscache";
 196  
 197          return $t;
 198      }
 199  
 200      /**
 201       * Check that the cache dir exists and is writeable
 202       *
 203       * @return void
 204       *
 205       * @throws \Exception
 206       */
 207      public static function checkCacheDir()
 208      {
 209          self::$cacheDir = str_replace('\\', '/', self::$cacheDir);
 210          self::$cacheDir = rtrim(self::$cacheDir, '/') . '/';
 211  
 212          if (! is_dir(self::$cacheDir)) {
 213              throw new Exception('Cache directory doesn\'t exist: ' . self::$cacheDir);
 214          }
 215  
 216          if (! is_writable(self::$cacheDir)) {
 217              throw new Exception('Cache directory isn\'t writable: ' . self::$cacheDir);
 218          }
 219      }
 220  
 221      /**
 222       * Delete unused cached files
 223       *
 224       * @return void
 225       */
 226      public static function cleanCache()
 227      {
 228          static $clean = false;
 229  
 230          if ($clean || empty(self::$cacheDir)) {
 231              return;
 232          }
 233  
 234          $clean = true;
 235  
 236          // only remove files with extensions created by SCSSPHP Cache
 237          // css files removed based on the list files
 238          $removeTypes = ['scsscache' => 1];
 239  
 240          $files = scandir(self::$cacheDir);
 241  
 242          if (! $files) {
 243              return;
 244          }
 245  
 246          $checkTime = time() - self::$gcLifetime;
 247  
 248          foreach ($files as $file) {
 249              // don't delete if the file wasn't created with SCSSPHP Cache
 250              if (strpos($file, self::$prefix) !== 0) {
 251                  continue;
 252              }
 253  
 254              $parts = explode('.', $file);
 255              $type = array_pop($parts);
 256  
 257              if (! isset($removeTypes[$type])) {
 258                  continue;
 259              }
 260  
 261              $fullPath = self::$cacheDir . $file;
 262              $mtime = filemtime($fullPath);
 263  
 264              // don't delete if it's a relatively new file
 265              if ($mtime > $checkTime) {
 266                  continue;
 267              }
 268  
 269              unlink($fullPath);
 270          }
 271      }
 272  }