Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Cache helper class
  19   *
  20   * This file is part of Moodle's cache API, affectionately called MUC.
  21   * It contains the components that are requried in order to use caching.
  22   *
  23   * @package    core
  24   * @category   cache
  25   * @copyright  2012 Sam Hemelryk
  26   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  27   */
  28  
  29  defined('MOODLE_INTERNAL') || die();
  30  
  31  /**
  32   * The cache helper class.
  33   *
  34   * The cache helper class provides common functionality to the cache API and is useful to developers within to interact with
  35   * the cache API in a general way.
  36   *
  37   * @package    core
  38   * @category   cache
  39   * @copyright  2012 Sam Hemelryk
  40   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  41   */
  42  class cache_helper {
  43  
  44      /**
  45       * Statistics gathered by the cache API during its operation will be used here.
  46       * @static
  47       * @var array
  48       */
  49      protected static $stats = array();
  50  
  51      /**
  52       * The instance of the cache helper.
  53       * @var cache_helper
  54       */
  55      protected static $instance;
  56  
  57      /**
  58       * The site identifier used by the cache.
  59       * Set the first time get_site_identifier is called.
  60       * @var string
  61       */
  62      protected static $siteidentifier = null;
  63  
  64      /**
  65       * Returns true if the cache API can be initialised before Moodle has finished initialising itself.
  66       *
  67       * This check is essential when trying to cache the likes of configuration information. It checks to make sure that the cache
  68       * configuration file has been created which allows use to set up caching when ever is required.
  69       *
  70       * @return bool
  71       */
  72      public static function ready_for_early_init() {
  73          return cache_config::config_file_exists();
  74      }
  75  
  76      /**
  77       * Returns an instance of the cache_helper.
  78       *
  79       * This is designed for internal use only and acts as a static store.
  80       * @staticvar null $instance
  81       * @return cache_helper
  82       */
  83      protected static function instance() {
  84          if (is_null(self::$instance)) {
  85              self::$instance = new cache_helper();
  86          }
  87          return self::$instance;
  88      }
  89  
  90      /**
  91       * Constructs an instance of the cache_helper class. Again for internal use only.
  92       */
  93      protected function __construct() {
  94          // Nothing to do here, just making sure you can't get an instance of this.
  95      }
  96  
  97      /**
  98       * Used as a data store for initialised definitions.
  99       * @var array
 100       */
 101      protected $definitions = array();
 102  
 103      /**
 104       * Used as a data store for initialised cache stores
 105       * We use this because we want to avoid establishing multiple instances of a single store.
 106       * @var array
 107       */
 108      protected $stores = array();
 109  
 110      /**
 111       * Returns the class for use as a cache loader for the given mode.
 112       *
 113       * @param int $mode One of cache_store::MODE_
 114       * @return string
 115       * @throws coding_exception
 116       */
 117      public static function get_class_for_mode($mode) {
 118          switch ($mode) {
 119              case cache_store::MODE_APPLICATION :
 120                  return 'cache_application';
 121              case cache_store::MODE_REQUEST :
 122                  return 'cache_request';
 123              case cache_store::MODE_SESSION :
 124                  return 'cache_session';
 125          }
 126          throw new coding_exception('Unknown cache mode passed. Must be one of cache_store::MODE_*');
 127      }
 128  
 129      /**
 130       * Returns the cache stores to be used with the given definition.
 131       * @param cache_definition $definition
 132       * @return array
 133       */
 134      public static function get_cache_stores(cache_definition $definition) {
 135          $instance = cache_config::instance();
 136          $stores = $instance->get_stores_for_definition($definition);
 137          $stores = self::initialise_cachestore_instances($stores, $definition);
 138          return $stores;
 139      }
 140  
 141      /**
 142       * Internal function for initialising an array of stores against a given cache definition.
 143       *
 144       * @param array $stores
 145       * @param cache_definition $definition
 146       * @return cache_store[]
 147       */
 148      protected static function initialise_cachestore_instances(array $stores, cache_definition $definition) {
 149          $return = array();
 150          $factory = cache_factory::instance();
 151          foreach ($stores as $name => $details) {
 152              $store = $factory->create_store_from_config($name, $details, $definition);
 153              if ($store !== false) {
 154                  $return[] = $store;
 155              }
 156          }
 157          return $return;
 158      }
 159  
 160      /**
 161       * Returns a cache_lock instance suitable for use with the store.
 162       *
 163       * @param cache_store $store
 164       * @return cache_lock_interface
 165       */
 166      public static function get_cachelock_for_store(cache_store $store) {
 167          $instance = cache_config::instance();
 168          $lockconf = $instance->get_lock_for_store($store->my_name());
 169          $factory = cache_factory::instance();
 170          return $factory->create_lock_instance($lockconf);
 171      }
 172  
 173      /**
 174       * Returns an array of plugins without using core methods.
 175       *
 176       * This function explicitly does NOT use core functions as it will in some circumstances be called before Moodle has
 177       * finished initialising. This happens when loading configuration for instance.
 178       *
 179       * @return string
 180       */
 181      public static function early_get_cache_plugins() {
 182          global $CFG;
 183          $result = array();
 184          $ignored = array('CVS', '_vti_cnf', 'simpletest', 'db', 'yui', 'tests');
 185          $fulldir = $CFG->dirroot.'/cache/stores';
 186          $items = new DirectoryIterator($fulldir);
 187          foreach ($items as $item) {
 188              if ($item->isDot() or !$item->isDir()) {
 189                  continue;
 190              }
 191              $pluginname = $item->getFilename();
 192              if (in_array($pluginname, $ignored)) {
 193                  continue;
 194              }
 195              if (!is_valid_plugin_name($pluginname)) {
 196                  // Better ignore plugins with problematic names here.
 197                  continue;
 198              }
 199              $result[$pluginname] = $fulldir.'/'.$pluginname;
 200              unset($item);
 201          }
 202          unset($items);
 203          return $result;
 204      }
 205  
 206      /**
 207       * Invalidates a given set of keys from a given definition.
 208       *
 209       * @todo Invalidating by definition should also add to the event cache so that sessions can be invalidated (when required).
 210       *
 211       * @param string $component
 212       * @param string $area
 213       * @param array $identifiers
 214       * @param array $keys
 215       * @return boolean
 216       */
 217      public static function invalidate_by_definition($component, $area, array $identifiers = array(), $keys = array()) {
 218          $cache = cache::make($component, $area, $identifiers);
 219          if (is_array($keys)) {
 220              $cache->delete_many($keys);
 221          } else if (is_scalar($keys)) {
 222              $cache->delete($keys);
 223          } else {
 224              throw new coding_exception('cache_helper::invalidate_by_definition only accepts $keys as array, or scalar.');
 225          }
 226          return true;
 227      }
 228  
 229      /**
 230       * Invalidates a given set of keys by means of an event.
 231       *
 232       * Events cannot determine what identifiers might need to be cleared. Event based purge and invalidation
 233       * are only supported on caches without identifiers.
 234       *
 235       * @param string $event
 236       * @param array $keys
 237       */
 238      public static function invalidate_by_event($event, array $keys) {
 239          $instance = cache_config::instance();
 240          $invalidationeventset = false;
 241          $factory = cache_factory::instance();
 242          $inuse = $factory->get_caches_in_use();
 243          $purgetoken = null;
 244          foreach ($instance->get_definitions() as $name => $definitionarr) {
 245              $definition = cache_definition::load($name, $definitionarr);
 246              if ($definition->invalidates_on_event($event)) {
 247                  // First up check if there is a cache loader for this definition already.
 248                  // If there is we need to invalidate the keys from there.
 249                  $definitionkey = $definition->get_component().'/'.$definition->get_area();
 250                  if (isset($inuse[$definitionkey])) {
 251                      $inuse[$definitionkey]->delete_many($keys);
 252                  }
 253  
 254                  // We should only log events for application and session caches.
 255                  // Request caches shouldn't have events as all data is lost at the end of the request.
 256                  // Events should only be logged once of course and likely several definitions are watching so we
 257                  // track its logging with $invalidationeventset.
 258                  $logevent = ($invalidationeventset === false && $definition->get_mode() !== cache_store::MODE_REQUEST);
 259  
 260                  if ($logevent) {
 261                      // Get the event invalidation cache.
 262                      $cache = cache::make('core', 'eventinvalidation');
 263                      // Get any existing invalidated keys for this cache.
 264                      $data = $cache->get($event);
 265                      if ($data === false) {
 266                          // There are none.
 267                          $data = array();
 268                      }
 269                      // Add our keys to them with the current cache timestamp.
 270                      if (null === $purgetoken) {
 271                          $purgetoken = cache::get_purge_token(true);
 272                      }
 273                      foreach ($keys as $key) {
 274                          $data[$key] = $purgetoken;
 275                      }
 276                      // Set that data back to the cache.
 277                      $cache->set($event, $data);
 278                      // This only needs to occur once.
 279                      $invalidationeventset = true;
 280                  }
 281              }
 282          }
 283      }
 284  
 285      /**
 286       * Purges the cache for a specific definition.
 287       *
 288       * @param string $component
 289       * @param string $area
 290       * @param array $identifiers
 291       * @return bool
 292       */
 293      public static function purge_by_definition($component, $area, array $identifiers = array()) {
 294          // Create the cache.
 295          $cache = cache::make($component, $area, $identifiers);
 296          // Initialise, in case of a store.
 297          if ($cache instanceof cache_store) {
 298              $factory = cache_factory::instance();
 299              $definition = $factory->create_definition($component, $area, null);
 300              $cacheddefinition = clone $definition;
 301              $cacheddefinition->set_identifiers($identifiers);
 302              $cache->initialise($cacheddefinition);
 303          }
 304          // Purge baby, purge.
 305          $cache->purge();
 306          return true;
 307      }
 308  
 309      /**
 310       * Purges a cache of all information on a given event.
 311       *
 312       * Events cannot determine what identifiers might need to be cleared. Event based purge and invalidation
 313       * are only supported on caches without identifiers.
 314       *
 315       * @param string $event
 316       */
 317      public static function purge_by_event($event) {
 318          $instance = cache_config::instance();
 319          $invalidationeventset = false;
 320          $factory = cache_factory::instance();
 321          $inuse = $factory->get_caches_in_use();
 322          $purgetoken = null;
 323          foreach ($instance->get_definitions() as $name => $definitionarr) {
 324              $definition = cache_definition::load($name, $definitionarr);
 325              if ($definition->invalidates_on_event($event)) {
 326                  // First up check if there is a cache loader for this definition already.
 327                  // If there is we need to invalidate the keys from there.
 328                  $definitionkey = $definition->get_component().'/'.$definition->get_area();
 329                  if (isset($inuse[$definitionkey])) {
 330                      $inuse[$definitionkey]->purge();
 331                  } else {
 332                      cache::make($definition->get_component(), $definition->get_area())->purge();
 333                  }
 334  
 335                  // We should only log events for application and session caches.
 336                  // Request caches shouldn't have events as all data is lost at the end of the request.
 337                  // Events should only be logged once of course and likely several definitions are watching so we
 338                  // track its logging with $invalidationeventset.
 339                  $logevent = ($invalidationeventset === false && $definition->get_mode() !== cache_store::MODE_REQUEST);
 340  
 341                  // We need to flag the event in the "Event invalidation" cache if it hasn't already happened.
 342                  if ($logevent && $invalidationeventset === false) {
 343                      // Get the event invalidation cache.
 344                      $cache = cache::make('core', 'eventinvalidation');
 345                      // Create a key to invalidate all.
 346                      if (null === $purgetoken) {
 347                          $purgetoken = cache::get_purge_token(true);
 348                      }
 349                      $data = array(
 350                          'purged' => $purgetoken,
 351                      );
 352                      // Set that data back to the cache.
 353                      $cache->set($event, $data);
 354                      // This only needs to occur once.
 355                      $invalidationeventset = true;
 356                  }
 357              }
 358          }
 359      }
 360  
 361      /**
 362       * Ensure that the stats array is ready to collect information for the given store and definition.
 363       * @param string $store
 364       * @param string $storeclass
 365       * @param string $definition A string that identifies the definition.
 366       * @param int $mode One of cache_store::MODE_*. Since 2.9.
 367       */
 368      protected static function ensure_ready_for_stats($store, $storeclass, $definition, $mode = cache_store::MODE_APPLICATION) {
 369          // This function is performance-sensitive, so exit as quickly as possible
 370          // if we do not need to do anything.
 371          if (isset(self::$stats[$definition]['stores'][$store])) {
 372              return;
 373          }
 374  
 375          if (!array_key_exists($definition, self::$stats)) {
 376              self::$stats[$definition] = array(
 377                  'mode' => $mode,
 378                  'stores' => array(
 379                      $store => array(
 380                          'class' => $storeclass,
 381                          'hits' => 0,
 382                          'misses' => 0,
 383                          'sets' => 0,
 384                          'iobytes' => cache_store::IO_BYTES_NOT_SUPPORTED,
 385                          'locks' => 0,
 386                      )
 387                  )
 388              );
 389          } else if (!array_key_exists($store, self::$stats[$definition]['stores'])) {
 390              self::$stats[$definition]['stores'][$store] = array(
 391                  'class' => $storeclass,
 392                  'hits' => 0,
 393                  'misses' => 0,
 394                  'sets' => 0,
 395                  'iobytes' => cache_store::IO_BYTES_NOT_SUPPORTED,
 396                  'locks' => 0,
 397              );
 398          }
 399      }
 400  
 401      /**
 402       * Returns a string to describe the definition.
 403       *
 404       * This method supports the definition as a string due to legacy requirements.
 405       * It is backwards compatible when a string is passed but is not accurate.
 406       *
 407       * @since 2.9
 408       * @param cache_definition|string $definition
 409       * @return string
 410       */
 411      protected static function get_definition_stat_id_and_mode($definition) {
 412          if (!($definition instanceof cache_definition)) {
 413              // All core calls to this method have been updated, this is the legacy state.
 414              // We'll use application as the default as that is the most common, really this is not accurate of course but
 415              // at this point we can only guess and as it only affects calls to cache stat outside of core (of which there should
 416              // be none) I think that is fine.
 417              debugging('Please update you cache stat calls to pass the definition rather than just its ID.', DEBUG_DEVELOPER);
 418              return array((string)$definition, cache_store::MODE_APPLICATION);
 419          }
 420          return array($definition->get_id(), $definition->get_mode());
 421      }
 422  
 423      /**
 424       * Record a cache hit in the stats for the given store and definition.
 425       *
 426       * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
 427       * cache_definition instance. It is preferable to pass a cache definition instance.
 428       *
 429       * In Moodle 3.9 the first argument changed to also accept a cache_store.
 430       *
 431       * @internal
 432       * @param string|cache_store $store
 433       * @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
 434       *      actual cache_definition object now.
 435       * @param int $hits The number of hits to record (by default 1)
 436       * @param int $readbytes Number of bytes read from the cache or cache_store::IO_BYTES_NOT_SUPPORTED
 437       */
 438      public static function record_cache_hit($store, $definition, int $hits = 1, int $readbytes = cache_store::IO_BYTES_NOT_SUPPORTED): void {
 439          $storeclass = '';
 440          if ($store instanceof cache_store) {
 441              $storeclass = get_class($store);
 442              $store = $store->my_name();
 443          }
 444          list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
 445          self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode);
 446          self::$stats[$definitionstr]['stores'][$store]['hits'] += $hits;
 447          if ($readbytes !== cache_store::IO_BYTES_NOT_SUPPORTED) {
 448              if (self::$stats[$definitionstr]['stores'][$store]['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) {
 449                  self::$stats[$definitionstr]['stores'][$store]['iobytes'] = $readbytes;
 450              } else {
 451                  self::$stats[$definitionstr]['stores'][$store]['iobytes'] += $readbytes;
 452              }
 453          }
 454      }
 455  
 456      /**
 457       * Record a cache miss in the stats for the given store and definition.
 458       *
 459       * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
 460       * cache_definition instance. It is preferable to pass a cache definition instance.
 461       *
 462       * In Moodle 3.9 the first argument changed to also accept a cache_store.
 463       *
 464       * @internal
 465       * @param string|cache_store $store
 466       * @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
 467       *      actual cache_definition object now.
 468       * @param int $misses The number of misses to record (by default 1)
 469       */
 470      public static function record_cache_miss($store, $definition, $misses = 1) {
 471          $storeclass = '';
 472          if ($store instanceof cache_store) {
 473              $storeclass = get_class($store);
 474              $store = $store->my_name();
 475          }
 476          list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
 477          self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode);
 478          self::$stats[$definitionstr]['stores'][$store]['misses'] += $misses;
 479      }
 480  
 481      /**
 482       * Record a cache set in the stats for the given store and definition.
 483       *
 484       * In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
 485       * cache_definition instance. It is preferable to pass a cache definition instance.
 486       *
 487       * In Moodle 3.9 the first argument changed to also accept a cache_store.
 488       *
 489       * @internal
 490       * @param string|cache_store $store
 491       * @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
 492       *      actual cache_definition object now.
 493       * @param int $sets The number of sets to record (by default 1)
 494       * @param int $writebytes Number of bytes written to the cache or cache_store::IO_BYTES_NOT_SUPPORTED
 495       */
 496      public static function record_cache_set($store, $definition, int $sets = 1,
 497              int $writebytes = cache_store::IO_BYTES_NOT_SUPPORTED) {
 498          $storeclass = '';
 499          if ($store instanceof cache_store) {
 500              $storeclass = get_class($store);
 501              $store = $store->my_name();
 502          }
 503          list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
 504          self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode);
 505          self::$stats[$definitionstr]['stores'][$store]['sets'] += $sets;
 506          if ($writebytes !== cache_store::IO_BYTES_NOT_SUPPORTED) {
 507              if (self::$stats[$definitionstr]['stores'][$store]['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) {
 508                  self::$stats[$definitionstr]['stores'][$store]['iobytes'] = $writebytes;
 509              } else {
 510                  self::$stats[$definitionstr]['stores'][$store]['iobytes'] += $writebytes;
 511              }
 512          }
 513      }
 514  
 515      /**
 516       * Return the stats collected so far.
 517       * @return array
 518       */
 519      public static function get_stats() {
 520          return self::$stats;
 521      }
 522  
 523      /**
 524       * Purge all of the cache stores of all of their data.
 525       *
 526       * Think twice before calling this method. It will purge **ALL** caches regardless of whether they have been used recently or
 527       * anything. This will involve full setup of the cache + the purge operation. On a site using caching heavily this WILL be
 528       * painful.
 529       *
 530       * @param bool $usewriter If set to true the cache_config_writer class is used. This class is special as it avoids
 531       *      it is still usable when caches have been disabled.
 532       *      Please use this option only if you really must. It's purpose is to allow the cache to be purged when it would be
 533       *      otherwise impossible.
 534       */
 535      public static function purge_all($usewriter = false) {
 536          $factory = cache_factory::instance();
 537          $config = $factory->create_config_instance($usewriter);
 538          foreach ($config->get_all_stores() as $store) {
 539              self::purge_store($store['name'], $config);
 540          }
 541          foreach ($factory->get_adhoc_caches_in_use() as $cache) {
 542              $cache->purge();
 543          }
 544      }
 545  
 546      /**
 547       * Purges a store given its name.
 548       *
 549       * @param string $storename
 550       * @param cache_config $config
 551       * @return bool
 552       */
 553      public static function purge_store($storename, cache_config $config = null) {
 554          if ($config === null) {
 555              $config = cache_config::instance();
 556          }
 557  
 558          $stores = $config->get_all_stores();
 559          if (!array_key_exists($storename, $stores)) {
 560              // The store does not exist.
 561              return false;
 562          }
 563  
 564          $store = $stores[$storename];
 565          $class = $store['class'];
 566  
 567  
 568          // We check are_requirements_met although we expect is_ready is going to check as well.
 569          if (!$class::are_requirements_met()) {
 570              return false;
 571          }
 572          // Found the store: is it ready?
 573          /* @var cache_store $instance */
 574          $instance = new $class($store['name'], $store['configuration']);
 575          if (!$instance->is_ready()) {
 576              unset($instance);
 577              return false;
 578          }
 579          foreach ($config->get_definitions_by_store($storename) as $id => $definition) {
 580              $definition = cache_definition::load($id, $definition);
 581              $definitioninstance = clone($instance);
 582              $definitioninstance->initialise($definition);
 583              $definitioninstance->purge();
 584              unset($definitioninstance);
 585          }
 586  
 587          return true;
 588      }
 589  
 590      /**
 591       * Purges all of the stores used by a definition.
 592       *
 593       * Unlike cache_helper::purge_by_definition this purges all of the data from the stores not
 594       * just the data relating to the definition.
 595       * This function is useful when you must purge a definition that requires setup but you don't
 596       * want to set it up.
 597       *
 598       * @param string $component
 599       * @param string $area
 600       */
 601      public static function purge_stores_used_by_definition($component, $area) {
 602          $factory = cache_factory::instance();
 603          $config = $factory->create_config_instance();
 604          $definition = $factory->create_definition($component, $area);
 605          $stores = $config->get_stores_for_definition($definition);
 606          foreach ($stores as $store) {
 607              self::purge_store($store['name']);
 608          }
 609      }
 610  
 611      /**
 612       * Returns the translated name of the definition.
 613       *
 614       * @param cache_definition $definition
 615       * @return lang_string
 616       */
 617      public static function get_definition_name($definition) {
 618          if ($definition instanceof cache_definition) {
 619              return $definition->get_name();
 620          }
 621          $identifier = 'cachedef_'.clean_param($definition['area'], PARAM_STRINGID);
 622          $component = $definition['component'];
 623          if ($component === 'core') {
 624              $component = 'cache';
 625          }
 626          return new lang_string($identifier, $component);
 627      }
 628  
 629      /**
 630       * Hashes a descriptive key to make it shorter and still unique.
 631       * @param string|int $key
 632       * @param cache_definition $definition
 633       * @return string
 634       */
 635      public static function hash_key($key, cache_definition $definition) {
 636          if ($definition->uses_simple_keys()) {
 637              if (debugging() && preg_match('#[^a-zA-Z0-9_]#', $key ?? '')) {
 638                  throw new coding_exception('Cache definition '.$definition->get_id().' requires simple keys. Invalid key provided.', $key);
 639              }
 640              // We put the key first so that we can be sure the start of the key changes.
 641              return (string)$key . '-' . $definition->generate_single_key_prefix();
 642          }
 643          $key = $definition->generate_single_key_prefix() . '-' . $key;
 644          return sha1($key);
 645      }
 646  
 647      /**
 648       * Finds all definitions and updates them within the cache config file.
 649       *
 650       * @param bool $coreonly If set to true only core definitions will be updated.
 651       */
 652      public static function update_definitions($coreonly = false) {
 653          global $CFG;
 654          // Include locallib.
 655          require_once($CFG->dirroot.'/cache/locallib.php');
 656          // First update definitions
 657          cache_config_writer::update_definitions($coreonly);
 658          // Second reset anything we have already initialised to ensure we're all up to date.
 659          cache_factory::reset();
 660      }
 661  
 662      /**
 663       * Update the site identifier stored by the cache API.
 664       *
 665       * @param string $siteidentifier
 666       * @return string The new site identifier.
 667       */
 668      public static function update_site_identifier($siteidentifier) {
 669          global $CFG;
 670          // Include locallib.
 671          require_once($CFG->dirroot.'/cache/locallib.php');
 672          $factory = cache_factory::instance();
 673          $factory->updating_started();
 674          $config = $factory->create_config_instance(true);
 675          $siteidentifier = $config->update_site_identifier($siteidentifier);
 676          $factory->updating_finished();
 677          cache_factory::reset();
 678          return $siteidentifier;
 679      }
 680  
 681      /**
 682       * Returns the site identifier.
 683       *
 684       * @return string
 685       */
 686      public static function get_site_identifier() {
 687          global $CFG;
 688          if (!is_null(self::$siteidentifier)) {
 689              return self::$siteidentifier;
 690          }
 691          // If site identifier hasn't been collected yet attempt to get it from the cache config.
 692          $factory = cache_factory::instance();
 693          // If the factory is initialising then we don't want to try to get it from the config or we risk
 694          // causing the cache to enter an infinite initialisation loop.
 695          if (!$factory->is_initialising()) {
 696              $config = $factory->create_config_instance();
 697              self::$siteidentifier = $config->get_site_identifier();
 698          }
 699          if (is_null(self::$siteidentifier)) {
 700              // If the site identifier is still null then config isn't aware of it yet.
 701              // We'll see if the CFG is loaded, and if not we will just use unknown.
 702              // It's very important here that we don't use get_config. We don't want an endless cache loop!
 703              if (!empty($CFG->siteidentifier)) {
 704                  self::$siteidentifier = self::update_site_identifier($CFG->siteidentifier);
 705              } else {
 706                  // It's not being recorded in MUC's config and the config data hasn't been loaded yet.
 707                  // Likely we are initialising.
 708                  return 'unknown';
 709              }
 710          }
 711          return self::$siteidentifier;
 712      }
 713  
 714      /**
 715       * Returns the site version.
 716       *
 717       * @return string
 718       */
 719      public static function get_site_version() {
 720          global $CFG;
 721          return (string)$CFG->version;
 722      }
 723  
 724      /**
 725       * Runs cron routines for MUC.
 726       */
 727      public static function cron() {
 728          self::clean_old_session_data(true);
 729      }
 730  
 731      /**
 732       * Cleans old session data from cache stores used for session based definitions.
 733       *
 734       * @param bool $output If set to true output will be given.
 735       */
 736      public static function clean_old_session_data($output = false) {
 737          global $CFG;
 738          if ($output) {
 739              mtrace('Cleaning up stale session data from cache stores.');
 740          }
 741          $factory = cache_factory::instance();
 742          $config = $factory->create_config_instance();
 743          $definitions = $config->get_definitions();
 744          $purgetime = time() - $CFG->sessiontimeout;
 745          foreach ($definitions as $definitionarray) {
 746              // We are only interested in session caches.
 747              if (!($definitionarray['mode'] & cache_store::MODE_SESSION)) {
 748                  continue;
 749              }
 750              $definition = $factory->create_definition($definitionarray['component'], $definitionarray['area']);
 751              $stores = $config->get_stores_for_definition($definition);
 752              // Turn them into store instances.
 753              $stores = self::initialise_cachestore_instances($stores, $definition);
 754              // Initialise all of the stores used for that definition.
 755              foreach ($stores as $store) {
 756                  // If the store doesn't support searching we can skip it.
 757                  if (!($store instanceof cache_is_searchable)) {
 758                      debugging('Cache stores used for session definitions should ideally be searchable.', DEBUG_DEVELOPER);
 759                      continue;
 760                  }
 761                  // Get all of the keys.
 762                  $keys = $store->find_by_prefix(cache_session::KEY_PREFIX);
 763                  $todelete = array();
 764                  foreach ($store->get_many($keys) as $key => $value) {
 765                      if (strpos($key, cache_session::KEY_PREFIX) !== 0 || !is_array($value) || !isset($value['lastaccess'])) {
 766                          continue;
 767                      }
 768                      if ((int)$value['lastaccess'] < $purgetime || true) {
 769                          $todelete[] = $key;
 770                      }
 771                  }
 772                  if (count($todelete)) {
 773                      $outcome = (int)$store->delete_many($todelete);
 774                      if ($output) {
 775                          $strdef = s($definition->get_id());
 776                          $strstore = s($store->my_name());
 777                          mtrace("- Removed {$outcome} old {$strdef} sessions from the '{$strstore}' cache store.");
 778                      }
 779                  }
 780              }
 781          }
 782      }
 783  
 784      /**
 785       * Returns an array of stores that would meet the requirements for every definition.
 786       *
 787       * These stores would be 100% suitable to map as defaults for cache modes.
 788       *
 789       * @return array[] An array of stores, keys are the store names.
 790       */
 791      public static function get_stores_suitable_for_mode_default() {
 792          $factory = cache_factory::instance();
 793          $config = $factory->create_config_instance();
 794          $requirements = 0;
 795          foreach ($config->get_definitions() as $definition) {
 796              $definition = cache_definition::load($definition['component'].'/'.$definition['area'], $definition);
 797              $requirements = $requirements | $definition->get_requirements_bin();
 798          }
 799          $stores = array();
 800          foreach ($config->get_all_stores() as $name => $store) {
 801              if (!empty($store['features']) && ($store['features'] & $requirements)) {
 802                  $stores[$name] = $store;
 803              }
 804          }
 805          return $stores;
 806      }
 807  
 808      /**
 809       * Returns stores suitable for use with a given definition.
 810       *
 811       * @param cache_definition $definition
 812       * @return cache_store[]
 813       */
 814      public static function get_stores_suitable_for_definition(cache_definition $definition) {
 815          $factory = cache_factory::instance();
 816          $stores = array();
 817          if ($factory->is_initialising() || $factory->stores_disabled()) {
 818              // No suitable stores here.
 819              return $stores;
 820          } else {
 821              $stores = self::get_cache_stores($definition);
 822              // If mappingsonly is set, having 0 stores is ok.
 823              if ((count($stores) === 0) && (!$definition->is_for_mappings_only())) {
 824                  // No suitable stores we found for the definition. We need to come up with a sensible default.
 825                  // If this has happened we can be sure that the user has mapped custom stores to either the
 826                  // mode of the definition. The first alternative to try is the system default for the mode.
 827                  // e.g. the default file store instance for application definitions.
 828                  $config = $factory->create_config_instance();
 829                  foreach ($config->get_stores($definition->get_mode()) as $name => $details) {
 830                      if (!empty($details['default'])) {
 831                          $stores[] = $factory->create_store_from_config($name, $details, $definition);
 832                          break;
 833                      }
 834                  }
 835              }
 836          }
 837          return $stores;
 838      }
 839  
 840      /**
 841       * Returns an array of warnings from the cache API.
 842       *
 843       * The warning returned here are for things like conflicting store instance configurations etc.
 844       * These get shown on the admin notifications page for example.
 845       *
 846       * @param array|null $stores An array of stores to get warnings for, or null for all.
 847       * @return string[]
 848       */
 849      public static function warnings(array $stores = null) {
 850          global $CFG;
 851          if ($stores === null) {
 852              require_once($CFG->dirroot.'/cache/locallib.php');
 853              $stores = core_cache\administration_helper::get_store_instance_summaries();
 854          }
 855          $warnings = array();
 856          foreach ($stores as $store) {
 857              if (!empty($store['warnings'])) {
 858                  $warnings = array_merge($warnings, $store['warnings']);
 859              }
 860          }
 861          return $warnings;
 862      }
 863  
 864      /**
 865       * A helper to determine whether a result was found.
 866       *
 867       * This has been deemed required after people have been confused by the fact that [] == false.
 868       *
 869       * @param mixed $value
 870       * @return bool
 871       */
 872      public static function result_found($value): bool {
 873          return $value !== false;
 874      }
 875  }