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 400 and 401]

   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 display administration helper.
  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   * @author     Peter Burnett <peterburnett@catalyst-au.net>
  26   * @copyright  2020 Catalyst IT
  27   * @copyright  2012 Sam Hemelryk
  28   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  29   */
  30  
  31  namespace core_cache\local;
  32  
  33  use cache_store, cache_factory, cache_config_writer, cache_helper;
  34  use core\output\notification;
  35  
  36  /**
  37   * A cache helper for administration tasks
  38   *
  39   * @package    core
  40   * @category   cache
  41   * @copyright  2020 Peter Burnett <peterburnett@catalyst-au.net>
  42   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  43   */
  44  class administration_display_helper extends \core_cache\administration_helper {
  45  
  46      /**
  47       * Please do not call constructor directly. Use cache_factory::get_administration_display_helper() instead.
  48       */
  49      public function __construct() {
  50          // Nothing to do here.
  51      }
  52  
  53      /**
  54       * Returns all of the actions that can be performed on a definition.
  55       *
  56       * @param context $context the system context.
  57       * @param array $definitionsummary information about this cache, from the array returned by
  58       *      core_cache\administration_helper::get_definition_summaries(). Currently only 'sharingoptions'
  59       *      element is used.
  60       * @return array of actions. Each action is an action_url.
  61       */
  62      public function get_definition_actions(\context $context, array $definitionsummary): array {
  63          global $OUTPUT;
  64          if (has_capability('moodle/site:config', $context)) {
  65              $actions = array();
  66              // Edit mappings.
  67              $actions[] = $OUTPUT->action_link(
  68                  new \moodle_url('/cache/admin.php', array('action' => 'editdefinitionmapping',
  69                      'definition' => $definitionsummary['id'])),
  70                  get_string('editmappings', 'cache')
  71              );
  72              // Edit sharing.
  73              if (count($definitionsummary['sharingoptions']) > 1) {
  74                  $actions[] = $OUTPUT->action_link(
  75                      new \moodle_url('/cache/admin.php', array('action' => 'editdefinitionsharing',
  76                          'definition' => $definitionsummary['id'])),
  77                      get_string('editsharing', 'cache')
  78                  );
  79              }
  80              // Purge.
  81              $actions[] = $OUTPUT->action_link(
  82                  new \moodle_url('/cache/admin.php', array('action' => 'purgedefinition',
  83                      'definition' => $definitionsummary['id'], 'sesskey' => sesskey())),
  84                  get_string('purge', 'cache')
  85              );
  86              return $actions;
  87          }
  88          return array();
  89      }
  90  
  91      /**
  92       * Returns all of the actions that can be performed on a store.
  93       *
  94       * @param string $name The name of the store
  95       * @param array $storedetails information about this store, from the array returned by
  96       *      core_cache\administration_helper::get_store_instance_summaries().
  97       * @return array of actions. Each action is an action_url.
  98       */
  99      public function get_store_instance_actions(string $name, array $storedetails): array {
 100          global $OUTPUT;
 101          $actions = array();
 102          if (has_capability('moodle/site:config', \context_system::instance())) {
 103              $baseurl = new \moodle_url('/cache/admin.php', array('store' => $name));
 104              if (empty($storedetails['default'])) {
 105                  // Edit store.
 106                  $actions[] = $OUTPUT->action_link(
 107                      new \moodle_url($baseurl, array('action' => 'editstore', 'plugin' => $storedetails['plugin'])),
 108                      get_string('editstore', 'cache')
 109                  );
 110                  // Delete store.
 111                  $actions[] = $OUTPUT->action_link(
 112                      new \moodle_url($baseurl, array('action' => 'deletestore')),
 113                      get_string('deletestore', 'cache')
 114                  );
 115              }
 116              // Purge store.
 117              $actions[] = $OUTPUT->action_link(
 118                  new \moodle_url($baseurl, array('action' => 'purgestore', 'sesskey' => sesskey())),
 119                  get_string('purge', 'cache')
 120              );
 121          }
 122          return $actions;
 123      }
 124  
 125      /**
 126       * Returns all of the actions that can be performed on a plugin.
 127       *
 128       * @param string $name The name of the plugin
 129       * @param array $plugindetails information about this store, from the array returned by
 130       *      core_cache\administration_helper::get_store_plugin_summaries().
 131       * @return array of actions. Each action is an action_url.
 132       */
 133      public function get_store_plugin_actions(string $name, array $plugindetails): array {
 134          global $OUTPUT;
 135          $actions = array();
 136          if (has_capability('moodle/site:config', \context_system::instance())) {
 137              if (!empty($plugindetails['canaddinstance'])) {
 138                  $url = new \moodle_url('/cache/admin.php',
 139                      array('action' => 'addstore', 'plugin' => $name));
 140                  $actions[] = $OUTPUT->action_link(
 141                      $url,
 142                      get_string('addinstance', 'cache')
 143                  );
 144              }
 145          }
 146          return $actions;
 147      }
 148  
 149      /**
 150       * Returns a form that can be used to add a store instance.
 151       *
 152       * @param string $plugin The plugin to add an instance of
 153       * @return cachestore_addinstance_form
 154       * @throws coding_exception
 155       */
 156      public function get_add_store_form(string $plugin): \cachestore_addinstance_form {
 157          global $CFG; // Needed for includes.
 158          $plugins = \core_component::get_plugin_list('cachestore');
 159          if (!array_key_exists($plugin, $plugins)) {
 160              throw new \coding_exception('Invalid cache plugin used when trying to create an edit form.');
 161          }
 162          $plugindir = $plugins[$plugin];
 163          $class = 'cachestore_addinstance_form';
 164          if (file_exists($plugindir.'/addinstanceform.php')) {
 165              require_once($plugindir.'/addinstanceform.php');
 166              if (class_exists('cachestore_'.$plugin.'_addinstance_form')) {
 167                  $class = 'cachestore_'.$plugin.'_addinstance_form';
 168                  if (!array_key_exists('cachestore_addinstance_form', class_parents($class))) {
 169                      throw new \coding_exception('Cache plugin add instance forms must extend cachestore_addinstance_form');
 170                  }
 171              }
 172          }
 173  
 174          $locks = $this->get_possible_locks_for_stores($plugindir, $plugin);
 175  
 176          $url = new \moodle_url('/cache/admin.php', array('action' => 'addstore'));
 177          return new $class($url, array('plugin' => $plugin, 'store' => null, 'locks' => $locks));
 178      }
 179  
 180      /**
 181       * Returns a form that can be used to edit a store instance.
 182       *
 183       * @param string $plugin
 184       * @param string $store
 185       * @return cachestore_addinstance_form
 186       * @throws coding_exception
 187       */
 188      public function get_edit_store_form(string $plugin, string $store): \cachestore_addinstance_form {
 189          global $CFG; // Needed for includes.
 190          $plugins = \core_component::get_plugin_list('cachestore');
 191          if (!array_key_exists($plugin, $plugins)) {
 192              throw new \coding_exception('Invalid cache plugin used when trying to create an edit form.');
 193          }
 194          $factory = \cache_factory::instance();
 195          $config = $factory->create_config_instance();
 196          $stores = $config->get_all_stores();
 197          if (!array_key_exists($store, $stores)) {
 198              throw new \coding_exception('Invalid store name given when trying to create an edit form.');
 199          }
 200          $plugindir = $plugins[$plugin];
 201          $class = 'cachestore_addinstance_form';
 202          if (file_exists($plugindir.'/addinstanceform.php')) {
 203              require_once($plugindir.'/addinstanceform.php');
 204              if (class_exists('cachestore_'.$plugin.'_addinstance_form')) {
 205                  $class = 'cachestore_'.$plugin.'_addinstance_form';
 206                  if (!array_key_exists('cachestore_addinstance_form', class_parents($class))) {
 207                      throw new \coding_exception('Cache plugin add instance forms must extend cachestore_addinstance_form');
 208                  }
 209              }
 210          }
 211  
 212          $locks = $this->get_possible_locks_for_stores($plugindir, $plugin);
 213  
 214          $url = new \moodle_url('/cache/admin.php', array('action' => 'editstore', 'plugin' => $plugin, 'store' => $store));
 215          $editform = new $class($url, array('plugin' => $plugin, 'store' => $store, 'locks' => $locks));
 216          if (isset($stores[$store]['lock'])) {
 217              $editform->set_data(array('lock' => $stores[$store]['lock']));
 218          }
 219          // See if the cachestore is going to want to load data for the form.
 220          // If it has a customised add instance form then it is going to want to.
 221          $storeclass = 'cachestore_'.$plugin;
 222          $storedata = $stores[$store];
 223          if (array_key_exists('configuration', $storedata) &&
 224              array_key_exists('cache_is_configurable', class_implements($storeclass))) {
 225              $storeclass::config_set_edit_form_data($editform, $storedata['configuration']);
 226          }
 227          return $editform;
 228      }
 229  
 230      /**
 231       * Returns an array of suitable lock instances for use with this plugin, or false if the plugin handles locking itself.
 232       *
 233       * @param string $plugindir
 234       * @param string $plugin
 235       * @return array|false
 236       */
 237      protected function get_possible_locks_for_stores(string $plugindir, string $plugin) {
 238          global $CFG; // Needed for includes.
 239          $supportsnativelocking = false;
 240          if (file_exists($plugindir.'/lib.php')) {
 241              require_once($plugindir.'/lib.php');
 242              $pluginclass = 'cachestore_'.$plugin;
 243              if (class_exists($pluginclass)) {
 244                  $supportsnativelocking = array_key_exists('cache_is_lockable', class_implements($pluginclass));
 245              }
 246          }
 247  
 248          if (!$supportsnativelocking) {
 249              $config = \cache_config::instance();
 250              $locks = array();
 251              foreach ($config->get_locks() as $lock => $conf) {
 252                  if (!empty($conf['default'])) {
 253                      $name = get_string($lock, 'cache');
 254                  } else {
 255                      $name = $lock;
 256                  }
 257                  $locks[$lock] = $name;
 258              }
 259          } else {
 260              $locks = false;
 261          }
 262  
 263          return $locks;
 264      }
 265  
 266      /**
 267       * Processes the results of the add/edit instance form data for a plugin returning an array of config information suitable to
 268       * store in configuration.
 269       *
 270       * @param stdClass $data The mform data.
 271       * @return array
 272       * @throws coding_exception
 273       */
 274      public function get_store_configuration_from_data(\stdClass $data): array {
 275          global $CFG;
 276          $file = $CFG->dirroot.'/cache/stores/'.$data->plugin.'/lib.php';
 277          if (!file_exists($file)) {
 278              throw new \coding_exception('Invalid cache plugin provided. '.$file);
 279          }
 280          require_once($file);
 281          $class = 'cachestore_'.$data->plugin;
 282          if (!class_exists($class)) {
 283              throw new \coding_exception('Invalid cache plugin provided.');
 284          }
 285          if (array_key_exists('cache_is_configurable', class_implements($class))) {
 286              return $class::config_get_configuration_array($data);
 287          }
 288          return array();
 289      }
 290  
 291      /**
 292       * Returns an array of lock plugins for which we can add an instance.
 293       *
 294       * Suitable for use within an mform select element.
 295       *
 296       * @return array
 297       */
 298      public function get_addable_lock_options(): array {
 299          $plugins = \core_component::get_plugin_list_with_class('cachelock', '', 'lib.php');
 300          $options = array();
 301          $len = strlen('cachelock_');
 302          foreach ($plugins as $plugin => $class) {
 303              $method = "$class::can_add_instance";
 304              if (is_callable($method) && !call_user_func($method)) {
 305                  // Can't add an instance of this plugin.
 306                  continue;
 307              }
 308              $options[substr($plugin, $len)] = get_string('pluginname', $plugin);
 309          }
 310          return $options;
 311      }
 312  
 313      /**
 314       * Gets the form to use when adding a lock instance.
 315       *
 316       * @param string $plugin
 317       * @param array $lockplugin
 318       * @return cache_lock_form
 319       * @throws coding_exception
 320       */
 321      public function get_add_lock_form(string $plugin, array $lockplugin = null): \cache_lock_form {
 322          global $CFG; // Needed for includes.
 323          $plugins = \core_component::get_plugin_list('cachelock');
 324          if (!array_key_exists($plugin, $plugins)) {
 325              throw new \coding_exception('Invalid cache lock plugin requested when trying to create a form.');
 326          }
 327          $plugindir = $plugins[$plugin];
 328          $class = 'cache_lock_form';
 329          if (file_exists($plugindir.'/addinstanceform.php') && in_array('cache_is_configurable', class_implements($class))) {
 330              require_once($plugindir.'/addinstanceform.php');
 331              if (class_exists('cachelock_'.$plugin.'_addinstance_form')) {
 332                  $class = 'cachelock_'.$plugin.'_addinstance_form';
 333                  if (!array_key_exists('cache_lock_form', class_parents($class))) {
 334                      throw new \coding_exception('Cache lock plugin add instance forms must extend cache_lock_form');
 335                  }
 336              }
 337          }
 338          return new $class(null, array('lock' => $plugin));
 339      }
 340  
 341      /**
 342       * Gets configuration data from a new lock instance form.
 343       *
 344       * @param string $plugin
 345       * @param stdClass $data
 346       * @return array
 347       * @throws coding_exception
 348       */
 349      public function get_lock_configuration_from_data(string $plugin, \stdClass $data): array {
 350          global $CFG;
 351          $file = $CFG->dirroot.'/cache/locks/'.$plugin.'/lib.php';
 352          if (!file_exists($file)) {
 353              throw new \coding_exception('Invalid cache plugin provided. '.$file);
 354          }
 355          require_once($file);
 356          $class = 'cachelock_'.$plugin;
 357          if (!class_exists($class)) {
 358              throw new \coding_exception('Invalid cache plugin provided.');
 359          }
 360          if (array_key_exists('cache_is_configurable', class_implements($class))) {
 361              return $class::config_get_configuration_array($data);
 362          }
 363          return array();
 364      }
 365  
 366      /**
 367       * Handles the page actions, based on the parameter.
 368       *
 369       * @param string $action the action to handle.
 370       * @param array $forminfo an empty array to be overridden and set.
 371       * @return array the empty or overridden forminfo array.
 372       */
 373      public function perform_cache_actions(string $action, array $forminfo): array {
 374          switch ($action) {
 375              case 'rescandefinitions' : // Rescan definitions.
 376                  $this->action_rescan_definition();
 377                  break;
 378  
 379              case 'addstore' : // Add the requested store.
 380                  $forminfo = $this->action_addstore();
 381                  break;
 382  
 383              case 'editstore' : // Edit the requested store.
 384                  $forminfo = $this->action_editstore();
 385                  break;
 386  
 387              case 'deletestore' : // Delete a given store.
 388                  $this->action_deletestore($action);
 389                  break;
 390  
 391              case 'editdefinitionmapping' : // Edit definition mappings.
 392                  $forminfo = $this->action_editdefinitionmapping();
 393                  break;
 394  
 395              case 'editdefinitionsharing' : // Edit definition sharing.
 396                  $forminfo = $this->action_editdefinitionsharing();
 397                  break;
 398  
 399              case 'editmodemappings': // Edit default mode mappings.
 400                  $forminfo = $this->action_editmodemappings();
 401                  break;
 402  
 403              case 'purgedefinition': // Purge a specific definition.
 404                  $this->action_purgedefinition();
 405                  break;
 406  
 407              case 'purgestore':
 408              case 'purge': // Purge a store cache.
 409                  $this->action_purge();
 410                  break;
 411  
 412              case 'newlockinstance':
 413                  $forminfo = $this->action_newlockinstance();
 414                  break;
 415  
 416              case 'deletelock':
 417                  // Deletes a lock instance.
 418                  $this->action_deletelock($action);
 419                  break;
 420          }
 421  
 422          return $forminfo;
 423      }
 424  
 425      /**
 426       * Performs the rescan definition action.
 427       *
 428       * @return void
 429       */
 430      public function action_rescan_definition() {
 431          global $PAGE;
 432  
 433          require_sesskey();
 434          \cache_config_writer::update_definitions();
 435          redirect($PAGE->url);
 436      }
 437  
 438      /**
 439       * Performs the add store action.
 440       *
 441       * @return array an array of the form to display to the user, and the page title.
 442       */
 443      public function action_addstore() : array {
 444          global $PAGE;
 445          $storepluginsummaries = $this->get_store_plugin_summaries();
 446  
 447          $plugin = required_param('plugin', PARAM_PLUGIN);
 448          if (!$storepluginsummaries[$plugin]['canaddinstance']) {
 449              throw new \moodle_exception('ex_unmetstorerequirements', 'cache');
 450          }
 451          $mform = $this->get_add_store_form($plugin);
 452          $title = get_string('addstore', 'cache', $storepluginsummaries[$plugin]['name']);
 453          if ($mform->is_cancelled()) {
 454              redirect($PAGE->url);
 455          } else if ($data = $mform->get_data()) {
 456              $config = $this->get_store_configuration_from_data($data);
 457              $writer = \cache_config_writer::instance();
 458              unset($config['lock']);
 459              foreach ($writer->get_locks() as $lock => $lockconfig) {
 460                  if ($lock == $data->lock) {
 461                      $config['lock'] = $data->lock;
 462                  }
 463              }
 464              $writer->add_store_instance($data->name, $data->plugin, $config);
 465              redirect($PAGE->url, get_string('addstoresuccess', 'cache', $storepluginsummaries[$plugin]['name']), 5);
 466          }
 467  
 468          $PAGE->navbar->add(get_string('addstore', 'cache', 'cache'), $PAGE->url);
 469          return array('form' => $mform, 'title' => $title);
 470      }
 471  
 472      /**
 473       * Performs the edit store action.
 474       *
 475       * @return array an array of the form to display, and the page title.
 476       */
 477      public function action_editstore(): array {
 478          global $PAGE;
 479          $storepluginsummaries = $this->get_store_plugin_summaries();
 480  
 481          $plugin = required_param('plugin', PARAM_PLUGIN);
 482          $store = required_param('store', PARAM_TEXT);
 483          $mform = $this->get_edit_store_form($plugin, $store);
 484          $title = get_string('addstore', 'cache', $storepluginsummaries[$plugin]['name']);
 485          if ($mform->is_cancelled()) {
 486              redirect($PAGE->url);
 487          } else if ($data = $mform->get_data()) {
 488              $config = $this->get_store_configuration_from_data($data);
 489              $writer = \cache_config_writer::instance();
 490  
 491              unset($config['lock']);
 492              foreach ($writer->get_locks() as $lock => $lockconfig) {
 493                  if ($lock == $data->lock) {
 494                      $config['lock'] = $data->lock;
 495                  }
 496              }
 497              $writer->edit_store_instance($data->name, $data->plugin, $config);
 498              redirect($PAGE->url, get_string('editstoresuccess', 'cache', $storepluginsummaries[$plugin]['name']), 5);
 499          }
 500  
 501          return array('form' => $mform, 'title' => $title);
 502      }
 503  
 504      /**
 505       * Performs the deletestore action.
 506       *
 507       * @param string $action the action calling to this function.
 508       */
 509      public function action_deletestore(string $action): void {
 510          global $OUTPUT, $PAGE, $SITE;
 511          $notifysuccess = true;
 512          $storeinstancesummaries = $this->get_store_instance_summaries();
 513  
 514          $store = required_param('store', PARAM_TEXT);
 515          $confirm = optional_param('confirm', false, PARAM_BOOL);
 516  
 517          if (!array_key_exists($store, $storeinstancesummaries)) {
 518              $notifysuccess = false;
 519              $notification = get_string('invalidstore', 'cache');
 520          } else if ($storeinstancesummaries[$store]['mappings'] > 0) {
 521              $notifysuccess = false;
 522              $notification = get_string('deletestorehasmappings', 'cache');
 523          }
 524  
 525          if ($notifysuccess) {
 526              if (!$confirm) {
 527                  $title = get_string('confirmstoredeletion', 'cache');
 528                  $params = array('store' => $store, 'confirm' => 1, 'action' => $action, 'sesskey' => sesskey());
 529                  $url = new \moodle_url($PAGE->url, $params);
 530                  $button = new \single_button($url, get_string('deletestore', 'cache'));
 531  
 532                  $PAGE->set_title($title);
 533                  $PAGE->set_heading($SITE->fullname);
 534                  echo $OUTPUT->header();
 535                  echo $OUTPUT->heading($title);
 536                  $confirmation = get_string('deletestoreconfirmation', 'cache', $storeinstancesummaries[$store]['name']);
 537                  echo $OUTPUT->confirm($confirmation, $button, $PAGE->url);
 538                  echo $OUTPUT->footer();
 539                  exit;
 540              } else {
 541                  require_sesskey();
 542                  $writer = \cache_config_writer::instance();
 543                  $writer->delete_store_instance($store);
 544                  redirect($PAGE->url, get_string('deletestoresuccess', 'cache'), 5);
 545              }
 546          } else {
 547              redirect($PAGE->url, $notification, null, notification::NOTIFY_ERROR);
 548          }
 549      }
 550  
 551      /**
 552       * Performs the edit definition mapping action.
 553       *
 554       * @return array an array of the form to display, and the page title.
 555       * @throws cache_exception
 556       */
 557      public function action_editdefinitionmapping(): array {
 558          global $PAGE;
 559          $definitionsummaries = $this->get_definition_summaries();
 560  
 561          $definition = required_param('definition', PARAM_SAFEPATH);
 562          if (!array_key_exists($definition, $definitionsummaries)) {
 563              throw new \cache_exception('Invalid cache definition requested');
 564          }
 565          $title = get_string('editdefinitionmappings', 'cache', $definition);
 566          $mform = new \cache_definition_mappings_form($PAGE->url, array('definition' => $definition));
 567          if ($mform->is_cancelled()) {
 568              redirect($PAGE->url);
 569          } else if ($data = $mform->get_data()) {
 570              $writer = \cache_config_writer::instance();
 571              $mappings = array();
 572              foreach ($data->mappings as $mapping) {
 573                  if (!empty($mapping)) {
 574                      $mappings[] = $mapping;
 575                  }
 576              }
 577              $writer->set_definition_mappings($definition, $mappings);
 578              redirect($PAGE->url);
 579          }
 580  
 581          $PAGE->navbar->add(get_string('updatedefinitionmapping', 'cache'), $PAGE->url);
 582          return array('form' => $mform, 'title' => $title);
 583      }
 584  
 585      /**
 586       * Performs the edit definition sharing action.
 587       *
 588       * @return array an array of the edit definition sharing form, and the page title.
 589       */
 590      public function action_editdefinitionsharing(): array {
 591          global $PAGE;
 592          $definitionsummaries = $this->get_definition_summaries();
 593  
 594          $definition = required_param('definition', PARAM_SAFEPATH);
 595          if (!array_key_exists($definition, $definitionsummaries)) {
 596              throw new \cache_exception('Invalid cache definition requested');
 597          }
 598          $title = get_string('editdefinitionsharing', 'cache', $definition);
 599          $sharingoptions = $definitionsummaries[$definition]['sharingoptions'];
 600          $customdata = array('definition' => $definition, 'sharingoptions' => $sharingoptions);
 601          $mform = new \cache_definition_sharing_form($PAGE->url, $customdata);
 602          $mform->set_data(array(
 603              'sharing' => $definitionsummaries[$definition]['selectedsharingoption'],
 604              'userinputsharingkey' => $definitionsummaries[$definition]['userinputsharingkey']
 605          ));
 606          if ($mform->is_cancelled()) {
 607              redirect($PAGE->url);
 608          } else if ($data = $mform->get_data()) {
 609              $component = $definitionsummaries[$definition]['component'];
 610              $area = $definitionsummaries[$definition]['area'];
 611              // Purge the stores removing stale data before we alter the sharing option.
 612              \cache_helper::purge_stores_used_by_definition($component, $area);
 613              $writer = \cache_config_writer::instance();
 614              $sharing = array_sum(array_keys($data->sharing));
 615              $userinputsharingkey = $data->userinputsharingkey;
 616              $writer->set_definition_sharing($definition, $sharing, $userinputsharingkey);
 617              redirect($PAGE->url);
 618          }
 619  
 620          $PAGE->navbar->add(get_string('updatedefinitionsharing', 'cache'), $PAGE->url);
 621          return array('form' => $mform, 'title' => $title);
 622      }
 623  
 624      /**
 625       * Performs the edit mode mappings action.
 626       *
 627       * @return array an array of the edit mode mappings form.
 628       */
 629      public function action_editmodemappings(): array {
 630          global $PAGE;
 631          $storeinstancesummaries = $this->get_store_instance_summaries();
 632          $defaultmodestores = $this->get_default_mode_stores();
 633  
 634          $mform = new \cache_mode_mappings_form(null, $storeinstancesummaries);
 635          $mform->set_data(array(
 636              'mode_'.cache_store::MODE_APPLICATION => key($defaultmodestores[cache_store::MODE_APPLICATION]),
 637              'mode_'.cache_store::MODE_SESSION => key($defaultmodestores[cache_store::MODE_SESSION]),
 638              'mode_'.cache_store::MODE_REQUEST => key($defaultmodestores[cache_store::MODE_REQUEST]),
 639          ));
 640          if ($mform->is_cancelled()) {
 641              redirect($PAGE->url);
 642          } else if ($data = $mform->get_data()) {
 643              $mappings = array(
 644                  cache_store::MODE_APPLICATION => array($data->{'mode_'.cache_store::MODE_APPLICATION}),
 645                  cache_store::MODE_SESSION => array($data->{'mode_'.cache_store::MODE_SESSION}),
 646                  cache_store::MODE_REQUEST => array($data->{'mode_'.cache_store::MODE_REQUEST}),
 647              );
 648              $writer = cache_config_writer::instance();
 649              $writer->set_mode_mappings($mappings);
 650              redirect($PAGE->url);
 651          }
 652  
 653          return array('form' => $mform);
 654      }
 655  
 656      /**
 657       * Performs the purge definition action.
 658       *
 659       * @return void
 660       */
 661      public function action_purgedefinition() {
 662          global $PAGE;
 663  
 664          require_sesskey();
 665          $id = required_param('definition', PARAM_SAFEPATH);
 666          list($component, $area) = explode('/', $id, 2);
 667          $factory = cache_factory::instance();
 668          $definition = $factory->create_definition($component, $area);
 669          if ($definition->has_required_identifiers()) {
 670              // We will have to purge the stores used by this definition.
 671              cache_helper::purge_stores_used_by_definition($component, $area);
 672          } else {
 673              // Alrighty we can purge just the data belonging to this definition.
 674              cache_helper::purge_by_definition($component, $area);
 675          }
 676  
 677          $message = get_string('purgexdefinitionsuccess', 'cache', [
 678                      'name' => $definition->get_name(),
 679                      'component' => $component,
 680                      'area' => $area,
 681                  ]);
 682          $purgeagainlink = \html_writer::link(new \moodle_url('/cache/admin.php', [
 683                  'action' => 'purgedefinition', 'sesskey' => sesskey(), 'definition' => $id]),
 684                  get_string('purgeagain', 'cache'));
 685          redirect($PAGE->url, $message . ' ' . $purgeagainlink, 5);
 686      }
 687  
 688      /**
 689       * Performs the purge action.
 690       *
 691       * @return void
 692       */
 693      public function action_purge() {
 694          global $PAGE;
 695  
 696          require_sesskey();
 697          $store = required_param('store', PARAM_TEXT);
 698          cache_helper::purge_store($store);
 699          $message = get_string('purgexstoresuccess', 'cache', ['store' => $store]);
 700          $purgeagainlink = \html_writer::link(new \moodle_url('/cache/admin.php', [
 701                  'action' => 'purgestore', 'sesskey' => sesskey(), 'store' => $store]),
 702                  get_string('purgeagain', 'cache'));
 703          redirect($PAGE->url, $message . ' ' . $purgeagainlink, 5);
 704      }
 705  
 706      /**
 707       * Performs the new lock instance action.
 708       *
 709       * @return array An array containing the new lock instance form.
 710       */
 711      public function action_newlockinstance(): array {
 712          global $PAGE;
 713  
 714          // Adds a new lock instance.
 715          $lock = required_param('lock', PARAM_ALPHANUMEXT);
 716          $mform = $this->get_add_lock_form($lock);
 717          if ($mform->is_cancelled()) {
 718              redirect($PAGE->url);
 719          } else if ($data = $mform->get_data()) {
 720              $factory = cache_factory::instance();
 721              $config = $factory->create_config_instance(true);
 722              $name = $data->name;
 723              $data = $this->get_lock_configuration_from_data($lock, $data);
 724              $config->add_lock_instance($name, $lock, $data);
 725              redirect($PAGE->url, get_string('addlocksuccess', 'cache', $name), 5);
 726          }
 727  
 728          return array('form' => $mform);
 729      }
 730  
 731      /**
 732       * Performs the delete lock action.
 733       *
 734       * @param string $action the action calling this function.
 735       */
 736      public function action_deletelock(string $action): void {
 737          global $OUTPUT, $PAGE, $SITE;
 738          $notifysuccess = true;
 739          $locks = $this->get_lock_summaries();
 740  
 741          $lock = required_param('lock', PARAM_ALPHANUMEXT);
 742          $confirm = optional_param('confirm', false, PARAM_BOOL);
 743          if (!array_key_exists($lock, $locks)) {
 744              $notifysuccess = false;
 745              $notification = get_string('invalidlock', 'cache');
 746          } else if ($locks[$lock]['uses'] > 0) {
 747              $notifysuccess = false;
 748              $notification = get_string('deletelockhasuses', 'cache');
 749          }
 750          if ($notifysuccess) {
 751              if (!$confirm) {
 752                  $title = get_string('confirmlockdeletion', 'cache');
 753                  $params = array('lock' => $lock, 'confirm' => 1, 'action' => $action, 'sesskey' => sesskey());
 754                  $url = new \moodle_url($PAGE->url, $params);
 755                  $button = new \single_button($url, get_string('deletelock', 'cache'));
 756  
 757                  $PAGE->set_title($title);
 758                  $PAGE->set_heading($SITE->fullname);
 759                  echo $OUTPUT->header();
 760                  echo $OUTPUT->heading($title);
 761                  $confirmation = get_string('deletelockconfirmation', 'cache', $lock);
 762                  echo $OUTPUT->confirm($confirmation, $button, $PAGE->url);
 763                  echo $OUTPUT->footer();
 764                  exit;
 765              } else {
 766                  require_sesskey();
 767                  $writer = cache_config_writer::instance();
 768                  $writer->delete_lock_instance($lock);
 769                  redirect($PAGE->url, get_string('deletelocksuccess', 'cache'), 5);
 770              }
 771          } else {
 772              redirect($PAGE->url, $notification, null, notification::NOTIFY_ERROR);
 773          }
 774      }
 775  
 776      /**
 777       * Outputs the main admin page by generating it through the renderer.
 778       *
 779       * @param \core_cache\output\renderer $renderer the renderer to use to generate the page.
 780       * @return string the HTML for the admin page.
 781       */
 782      public function generate_admin_page(\core_cache\output\renderer $renderer): string {
 783          $context = \context_system::instance();
 784          $html = '';
 785  
 786          $storepluginsummaries = $this->get_store_plugin_summaries();
 787          $storeinstancesummaries = $this->get_store_instance_summaries();
 788          $definitionsummaries = $this->get_definition_summaries();
 789          $defaultmodestores = $this->get_default_mode_stores();
 790          $locks = $this->get_lock_summaries();
 791  
 792          $html .= $renderer->store_plugin_summaries($storepluginsummaries);
 793          $html .= $renderer->store_instance_summariers($storeinstancesummaries, $storepluginsummaries);
 794          $html .= $renderer->definition_summaries($definitionsummaries, $context);
 795          $html .= $renderer->lock_summaries($locks);
 796          $html .= $renderer->additional_lock_actions();
 797  
 798          $applicationstore = join(', ', $defaultmodestores[cache_store::MODE_APPLICATION]);
 799          $sessionstore = join(', ', $defaultmodestores[cache_store::MODE_SESSION]);
 800          $requeststore = join(', ', $defaultmodestores[cache_store::MODE_REQUEST]);
 801          $editurl = new \moodle_url('/cache/admin.php', array('action' => 'editmodemappings'));
 802          $html .= $renderer->mode_mappings($applicationstore, $sessionstore, $requeststore, $editurl);
 803  
 804          return $html;
 805      }
 806  
 807      /**
 808       * Gets usage information about the whole cache system.
 809       *
 810       * This is a slow function and should only be used on an admin information page.
 811       *
 812       * The returned array lists all cache definitions with fields 'cacheid' and 'stores'. For
 813       * each store, the following fields are available:
 814       *
 815       * - name (store name)
 816       * - class (e.g. cachestore_redis)
 817       * - supported (true if we have any information)
 818       * - items (number of items stored)
 819       * - mean (mean size of item)
 820       * - sd (standard deviation for item sizes)
 821       * - margin (margin of error for mean at 95% confidence)
 822       * - storetotal (total usage for store if known, otherwise null)
 823       *
 824       * The storetotal field will be the same for every cache that uses the same store.
 825       *
 826       * @param int $samplekeys Number of keys to sample when checking size of large caches
 827       * @return array Details of cache usage
 828       */
 829      public function get_usage(int $samplekeys): array {
 830          $results = [];
 831  
 832          $factory = cache_factory::instance();
 833  
 834          // Check the caches we already have an instance of, so we don't make another one...
 835          $got = $factory->get_caches_in_use();
 836          $gotid = [];
 837          foreach ($got as $longid => $unused) {
 838              // The IDs here can be of the form cacheid/morestuff if there are parameters in the
 839              // cache. Any entry for a cacheid is good enough to consider that we don't need to make
 840              // another entry ourselves, so we remove the extra bits and track the basic cache id.
 841              $gotid[preg_replace('~^([^/]+/[^/]+)/.*$~', '$1', $longid)] = true;
 842          }
 843  
 844          $storetotals = [];
 845  
 846          $config = $factory->create_config_instance();
 847          foreach ($config->get_definitions() as $configdetails) {
 848              if (!array_key_exists($configdetails['component'] . '/' .  $configdetails['area'], $gotid)) {
 849                  // Where possible (if it doesn't need identifiers), make an instance of the cache, otherwise
 850                  // we can't get the store instances for it (and it won't show up in the list).
 851                  if (empty($configdetails['requireidentifiers'])) {
 852                      \cache::make($configdetails['component'], $configdetails['area']);
 853                  }
 854              }
 855              $definition = $factory->create_definition($configdetails['component'], $configdetails['area']);
 856              $stores = $factory->get_store_instances_in_use($definition);
 857  
 858              // Create object for results about this cache definition.
 859              $currentresult = (object)['cacheid' => $definition->get_id(), 'stores' => []];
 860              $results[$currentresult->cacheid] = $currentresult;
 861  
 862              /** @var cache_store $store */
 863              foreach ($stores as $store) {
 864                  // Skip static cache.
 865                  if ($store instanceof \cachestore_static) {
 866                      continue;
 867                  }
 868  
 869                  // Get cache size details from store.
 870                  $currentstore = $store->cache_size_details($samplekeys);
 871  
 872                  // Add in basic information about store.
 873                  $currentstore->name = $store->my_name();
 874                  $currentstore->class = get_class($store);
 875  
 876                  // Add in store total.
 877                  if (!array_key_exists($currentstore->name, $storetotals)) {
 878                      $storetotals[$currentstore->name] = $store->store_total_size();
 879                  }
 880                  $currentstore->storetotal = $storetotals[$currentstore->name];
 881  
 882                  $currentresult->stores[] = $currentstore;
 883              }
 884          }
 885  
 886          ksort($results);
 887          return $results;
 888      }
 889  }