Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 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 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  defined('MOODLE_INTERNAL') || die();
  34  use cache_store, cache_factory, cache_config_writer, cache_helper;
  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              print_error('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       * @return void
 509       */
 510      public function action_deletestore(string $action) {
 511          global $OUTPUT, $PAGE, $SITE;
 512          $notifysuccess = true;
 513          $storeinstancesummaries = $this->get_store_instance_summaries();
 514  
 515          $store = required_param('store', PARAM_TEXT);
 516          $confirm = optional_param('confirm', false, PARAM_BOOL);
 517  
 518          if (!array_key_exists($store, $storeinstancesummaries)) {
 519              $notifysuccess = false;
 520              $notifications[] = array(get_string('invalidstore', 'cache'), false);
 521          } else if ($storeinstancesummaries[$store]['mappings'] > 0) {
 522              $notifysuccess = false;
 523              $notifications[] = array(get_string('deletestorehasmappings', 'cache'), false);
 524          }
 525  
 526          if ($notifysuccess) {
 527              if (!$confirm) {
 528                  $title = get_string('confirmstoredeletion', 'cache');
 529                  $params = array('store' => $store, 'confirm' => 1, 'action' => $action, 'sesskey' => sesskey());
 530                  $url = new \moodle_url($PAGE->url, $params);
 531                  $button = new \single_button($url, get_string('deletestore', 'cache'));
 532  
 533                  $PAGE->set_title($title);
 534                  $PAGE->set_heading($SITE->fullname);
 535                  echo $OUTPUT->header();
 536                  echo $OUTPUT->heading($title);
 537                  $confirmation = get_string('deletestoreconfirmation', 'cache', $storeinstancesummaries[$store]['name']);
 538                  echo $OUTPUT->confirm($confirmation, $button, $PAGE->url);
 539                  echo $OUTPUT->footer();
 540                  exit;
 541              } else {
 542                  require_sesskey();
 543                  $writer = \cache_config_writer::instance();
 544                  $writer->delete_store_instance($store);
 545                  redirect($PAGE->url, get_string('deletestoresuccess', 'cache'), 5);
 546              }
 547          }
 548      }
 549  
 550      /**
 551       * Performs the edit definition mapping action.
 552       *
 553       * @return array an array of the form to display, and the page title.
 554       * @throws cache_exception
 555       */
 556      public function action_editdefinitionmapping(): array {
 557          global $PAGE;
 558          $definitionsummaries = $this->get_definition_summaries();
 559  
 560          $definition = required_param('definition', PARAM_SAFEPATH);
 561          if (!array_key_exists($definition, $definitionsummaries)) {
 562              throw new \cache_exception('Invalid cache definition requested');
 563          }
 564          $title = get_string('editdefinitionmappings', 'cache', $definition);
 565          $mform = new \cache_definition_mappings_form($PAGE->url, array('definition' => $definition));
 566          if ($mform->is_cancelled()) {
 567              redirect($PAGE->url);
 568          } else if ($data = $mform->get_data()) {
 569              $writer = \cache_config_writer::instance();
 570              $mappings = array();
 571              foreach ($data->mappings as $mapping) {
 572                  if (!empty($mapping)) {
 573                      $mappings[] = $mapping;
 574                  }
 575              }
 576              $writer->set_definition_mappings($definition, $mappings);
 577              redirect($PAGE->url);
 578          }
 579  
 580          $PAGE->navbar->add(get_string('updatedefinitionmapping', 'cache'), $PAGE->url);
 581          return array('form' => $mform, 'title' => $title);
 582      }
 583  
 584      /**
 585       * Performs the edit definition sharing action.
 586       *
 587       * @return array an array of the edit definition sharing form, and the page title.
 588       */
 589      public function action_editdefinitionsharing(): array {
 590          global $PAGE;
 591          $definitionsummaries = $this->get_definition_summaries();
 592  
 593          $definition = required_param('definition', PARAM_SAFEPATH);
 594          if (!array_key_exists($definition, $definitionsummaries)) {
 595              throw new \cache_exception('Invalid cache definition requested');
 596          }
 597          $title = get_string('editdefinitionsharing', 'cache', $definition);
 598          $sharingoptions = $definitionsummaries[$definition]['sharingoptions'];
 599          $customdata = array('definition' => $definition, 'sharingoptions' => $sharingoptions);
 600          $mform = new \cache_definition_sharing_form($PAGE->url, $customdata);
 601          $mform->set_data(array(
 602              'sharing' => $definitionsummaries[$definition]['selectedsharingoption'],
 603              'userinputsharingkey' => $definitionsummaries[$definition]['userinputsharingkey']
 604          ));
 605          if ($mform->is_cancelled()) {
 606              redirect($PAGE->url);
 607          } else if ($data = $mform->get_data()) {
 608              $component = $definitionsummaries[$definition]['component'];
 609              $area = $definitionsummaries[$definition]['area'];
 610              // Purge the stores removing stale data before we alter the sharing option.
 611              \cache_helper::purge_stores_used_by_definition($component, $area);
 612              $writer = \cache_config_writer::instance();
 613              $sharing = array_sum(array_keys($data->sharing));
 614              $userinputsharingkey = $data->userinputsharingkey;
 615              $writer->set_definition_sharing($definition, $sharing, $userinputsharingkey);
 616              redirect($PAGE->url);
 617          }
 618  
 619          $PAGE->navbar->add(get_string('updatedefinitionsharing', 'cache'), $PAGE->url);
 620          return array('form' => $mform, 'title' => $title);
 621      }
 622  
 623      /**
 624       * Performs the edit mode mappings action.
 625       *
 626       * @return array an array of the edit mode mappings form.
 627       */
 628      public function action_editmodemappings(): array {
 629          global $PAGE;
 630          $storeinstancesummaries = $this->get_store_instance_summaries();
 631          $defaultmodestores = $this->get_default_mode_stores();
 632  
 633          $mform = new \cache_mode_mappings_form(null, $storeinstancesummaries);
 634          $mform->set_data(array(
 635              'mode_'.cache_store::MODE_APPLICATION => key($defaultmodestores[cache_store::MODE_APPLICATION]),
 636              'mode_'.cache_store::MODE_SESSION => key($defaultmodestores[cache_store::MODE_SESSION]),
 637              'mode_'.cache_store::MODE_REQUEST => key($defaultmodestores[cache_store::MODE_REQUEST]),
 638          ));
 639          if ($mform->is_cancelled()) {
 640              redirect($PAGE->url);
 641          } else if ($data = $mform->get_data()) {
 642              $mappings = array(
 643                  cache_store::MODE_APPLICATION => array($data->{'mode_'.cache_store::MODE_APPLICATION}),
 644                  cache_store::MODE_SESSION => array($data->{'mode_'.cache_store::MODE_SESSION}),
 645                  cache_store::MODE_REQUEST => array($data->{'mode_'.cache_store::MODE_REQUEST}),
 646              );
 647              $writer = cache_config_writer::instance();
 648              $writer->set_mode_mappings($mappings);
 649              redirect($PAGE->url);
 650          }
 651  
 652          return array('form' => $mform);
 653      }
 654  
 655      /**
 656       * Performs the purge definition action.
 657       *
 658       * @return void
 659       */
 660      public function action_purgedefinition() {
 661          global $PAGE;
 662  
 663          require_sesskey();
 664          $id = required_param('definition', PARAM_SAFEPATH);
 665          list($component, $area) = explode('/', $id, 2);
 666          $factory = cache_factory::instance();
 667          $definition = $factory->create_definition($component, $area);
 668          if ($definition->has_required_identifiers()) {
 669              // We will have to purge the stores used by this definition.
 670              cache_helper::purge_stores_used_by_definition($component, $area);
 671          } else {
 672              // Alrighty we can purge just the data belonging to this definition.
 673              cache_helper::purge_by_definition($component, $area);
 674          }
 675  
 676          $message = get_string('purgexdefinitionsuccess', 'cache', [
 677                      'name' => $definition->get_name(),
 678                      'component' => $component,
 679                      'area' => $area,
 680                  ]);
 681          $purgeagainlink = \html_writer::link(new \moodle_url('/cache/admin.php', [
 682                  'action' => 'purgedefinition', 'sesskey' => sesskey(), 'definition' => $id]),
 683                  get_string('purgeagain', 'cache'));
 684          redirect($PAGE->url, $message . ' ' . $purgeagainlink, 5);
 685      }
 686  
 687      /**
 688       * Performs the purge action.
 689       *
 690       * @return void
 691       */
 692      public function action_purge() {
 693          global $PAGE;
 694  
 695          require_sesskey();
 696          $store = required_param('store', PARAM_TEXT);
 697          cache_helper::purge_store($store);
 698          $message = get_string('purgexstoresuccess', 'cache', ['store' => $store]);
 699          $purgeagainlink = \html_writer::link(new \moodle_url('/cache/admin.php', [
 700                  'action' => 'purgestore', 'sesskey' => sesskey(), 'store' => $store]),
 701                  get_string('purgeagain', 'cache'));
 702          redirect($PAGE->url, $message . ' ' . $purgeagainlink, 5);
 703      }
 704  
 705      /**
 706       * Performs the new lock instance action.
 707       *
 708       * @return array An array containing the new lock instance form.
 709       */
 710      public function action_newlockinstance(): array {
 711          global $PAGE;
 712  
 713          // Adds a new lock instance.
 714          $lock = required_param('lock', PARAM_ALPHANUMEXT);
 715          $mform = $this->get_add_lock_form($lock);
 716          if ($mform->is_cancelled()) {
 717              redirect($PAGE->url);
 718          } else if ($data = $mform->get_data()) {
 719              $factory = cache_factory::instance();
 720              $config = $factory->create_config_instance(true);
 721              $name = $data->name;
 722              $data = $this->get_lock_configuration_from_data($lock, $data);
 723              $config->add_lock_instance($name, $lock, $data);
 724              redirect($PAGE->url, get_string('addlocksuccess', 'cache', $name), 5);
 725          }
 726  
 727          return array('form' => $mform);
 728      }
 729  
 730      /**
 731       * Performs the delete lock action.
 732       *
 733       * @param string $action the action calling this function.
 734       * @return void
 735       */
 736      public function action_deletelock(string $action) {
 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              $notifications[] = array(get_string('invalidlock', 'cache'), false);
 746          } else if ($locks[$lock]['uses'] > 0) {
 747              $notifysuccess = false;
 748              $notifications[] = array(get_string('deletelockhasuses', 'cache'), false);
 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          }
 772      }
 773  
 774      /**
 775       * Outputs the main admin page by generating it through the renderer.
 776       *
 777       * @param \core_cache\output\renderer $renderer the renderer to use to generate the page.
 778       * @return string the HTML for the admin page.
 779       */
 780      public function generate_admin_page(\core_cache\output\renderer $renderer): string {
 781          $context = \context_system::instance();
 782          $html = '';
 783  
 784          $storepluginsummaries = $this->get_store_plugin_summaries();
 785          $storeinstancesummaries = $this->get_store_instance_summaries();
 786          $definitionsummaries = $this->get_definition_summaries();
 787          $defaultmodestores = $this->get_default_mode_stores();
 788          $locks = $this->get_lock_summaries();
 789  
 790          $html .= $renderer->store_plugin_summaries($storepluginsummaries);
 791          $html .= $renderer->store_instance_summariers($storeinstancesummaries, $storepluginsummaries);
 792          $html .= $renderer->definition_summaries($definitionsummaries, $context);
 793          $html .= $renderer->lock_summaries($locks);
 794          $html .= $renderer->additional_lock_actions();
 795  
 796          $applicationstore = join(', ', $defaultmodestores[cache_store::MODE_APPLICATION]);
 797          $sessionstore = join(', ', $defaultmodestores[cache_store::MODE_SESSION]);
 798          $requeststore = join(', ', $defaultmodestores[cache_store::MODE_REQUEST]);
 799          $editurl = new \moodle_url('/cache/admin.php', array('action' => 'editmodemappings'));
 800          $html .= $renderer->mode_mappings($applicationstore, $sessionstore, $requeststore, $editurl);
 801  
 802          return $html;
 803      }
 804  
 805      /**
 806       * Gets usage information about the whole cache system.
 807       *
 808       * This is a slow function and should only be used on an admin information page.
 809       *
 810       * The returned array lists all cache definitions with fields 'cacheid' and 'stores'. For
 811       * each store, the following fields are available:
 812       *
 813       * - name (store name)
 814       * - class (e.g. cachestore_redis)
 815       * - supported (true if we have any information)
 816       * - items (number of items stored)
 817       * - mean (mean size of item)
 818       * - sd (standard deviation for item sizes)
 819       * - margin (margin of error for mean at 95% confidence)
 820       * - storetotal (total usage for store if known, otherwise null)
 821       *
 822       * The storetotal field will be the same for every cache that uses the same store.
 823       *
 824       * @param int $samplekeys Number of keys to sample when checking size of large caches
 825       * @return array Details of cache usage
 826       */
 827      public function get_usage(int $samplekeys): array {
 828          $results = [];
 829  
 830          $factory = cache_factory::instance();
 831  
 832          // Check the caches we already have an instance of, so we don't make another one...
 833          $got = $factory->get_caches_in_use();
 834          $gotid = [];
 835          foreach ($got as $longid => $unused) {
 836              // The IDs here can be of the form cacheid/morestuff if there are parameters in the
 837              // cache. Any entry for a cacheid is good enough to consider that we don't need to make
 838              // another entry ourselves, so we remove the extra bits and track the basic cache id.
 839              $gotid[preg_replace('~^([^/]+/[^/]+)/.*$~', '$1', $longid)] = true;
 840          }
 841  
 842          $storetotals = [];
 843  
 844          $config = $factory->create_config_instance();
 845          foreach ($config->get_definitions() as $configdetails) {
 846              if (!array_key_exists($configdetails['component'] . '/' .  $configdetails['area'], $gotid)) {
 847                  // Where possible (if it doesn't need identifiers), make an instance of the cache, otherwise
 848                  // we can't get the store instances for it (and it won't show up in the list).
 849                  if (empty($configdetails['requireidentifiers'])) {
 850                      \cache::make($configdetails['component'], $configdetails['area']);
 851                  }
 852              }
 853              $definition = $factory->create_definition($configdetails['component'], $configdetails['area']);
 854              $stores = $factory->get_store_instances_in_use($definition);
 855  
 856              // Create object for results about this cache definition.
 857              $currentresult = (object)['cacheid' => $definition->get_id(), 'stores' => []];
 858              $results[$currentresult->cacheid] = $currentresult;
 859  
 860              /** @var cache_store $store */
 861              foreach ($stores as $store) {
 862                  // Skip static cache.
 863                  if ($store instanceof \cachestore_static) {
 864                      continue;
 865                  }
 866  
 867                  // Get cache size details from store.
 868                  $currentstore = $store->cache_size_details($samplekeys);
 869  
 870                  // Add in basic information about store.
 871                  $currentstore->name = $store->my_name();
 872                  $currentstore->class = get_class($store);
 873  
 874                  // Add in store total.
 875                  if (!array_key_exists($currentstore->name, $storetotals)) {
 876                      $storetotals[$currentstore->name] = $store->store_total_size();
 877                  }
 878                  $currentstore->storetotal = $storetotals[$currentstore->name];
 879  
 880                  $currentresult->stores[] = $currentstore;
 881              }
 882          }
 883  
 884          ksort($results);
 885          return $results;
 886      }
 887  }