MDL-25290 cache: Added unit tests for admin and fixed up identified issues
[moodle.git] / cache / locallib.php
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/>.
17 /**
18  * The supplementary cache API.
19  *
20  * This file is part of Moodle's cache API, affectionately called MUC.
21  * It contains elements of the API that are not required in order to use caching.
22  * Things in here are more in line with administration and management of the cache setup and configuration.
23  *
24  * @package    core
25  * @category   cache
26  * @copyright  2012 Sam Hemelryk
27  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28  */
30 defined('MOODLE_INTERNAL') || die();
32 /**
33  * Cache configuration writer.
34  *
35  * This class should only be used when you need to write to the config, all read operations exist within the cache_config.
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_config_writer extends cache_config {
44     /**
45      * Returns an instance of the configuration writer.
46      *
47      * @return cache_config_writer
48      */
49     public static function instance() {
50         $factory = cache_factory::instance();
51         return $factory->create_config_instance(true);
52     }
54     /**
55      * Saves the current configuration.
56      */
57     protected function config_save() {
58         global $CFG;
59         $cachefile = self::get_config_file_path();
60         $directory = dirname($cachefile);
61         if ($directory !== $CFG->dataroot && !file_exists($directory)) {
62             $result = make_writable_directory($directory, false);
63             if (!$result) {
64                 throw new cache_exception('ex_configcannotsave', 'cache', '', null, 'Cannot create config directory.');
65             }
66         }
67         if (!file_exists($directory) || !is_writable($directory)) {
68             throw new cache_exception('ex_configcannotsave', 'cache', '', null, 'Config directory is not writable.');
69         }
71         // Prepare a configuration array to store.
72         $configuration = array();
73         $configuration['stores'] = $this->configstores;
74         $configuration['modemappings'] = $this->configmodemappings;
75         $configuration['definitions'] = $this->configdefinitions;
76         $configuration['definitionmappings'] = $this->configdefinitionmappings;
77         $configuration['locks'] = $this->configlocks;
79         // Prepare the file content.
80         $content = "<?php defined('MOODLE_INTERNAL') || die();\n \$configuration = ".var_export($configuration, true).";";
82         // We need to create a temporary cache lock instance for use here. Remember we are generating the config file
83         // it doesn't exist and thus we can't use the normal API for this (it'll just try to use config).
84         $lockconf = reset($this->configlocks);
85         if ($lockconf === false) {
86             debugging('Your cache configuration file is out of date and needs to be refreshed.', DEBUG_DEVELOPER);
87             // Use the default
88             $lockconf = array(
89                 'name' => 'cachelock_file_default',
90                 'type' => 'cachelock_file',
91                 'dir' => 'filelocks',
92                 'default' => true
93             );
94         }
95         $factory = cache_factory::instance();
96         $locking = $factory->create_lock_instance($lockconf);
97         if ($locking->lock('configwrite', 'config', true)) {
98             // Its safe to use w mode here because we have already acquired the lock.
99             $handle = fopen($cachefile, 'w');
100             fwrite($handle, $content);
101             fflush($handle);
102             fclose($handle);
103             $locking->unlock('configwrite', 'config');
104         } else {
105             throw new cache_exception('ex_configcannotsave', 'cache', '', null, 'Unable to open the cache config file.');
106         }
107     }
109     /**
110      * Adds a plugin instance.
111      *
112      * This function also calls save so you should redirect immediately, or at least very shortly after
113      * calling this method.
114      *
115      * @param string $name The name for the instance (must be unique)
116      * @param string $plugin The name of the plugin.
117      * @param array $configuration The configuration data for the plugin instance.
118      * @return bool
119      * @throws cache_exception
120      */
121     public function add_plugin_instance($name, $plugin, array $configuration = array()) {
122         if (array_key_exists($name, $this->configstores)) {
123             throw new cache_exception('Duplicate name specificed for cache plugin instance. You must provide a unique name.');
124         }
125         $class = 'cachestore_'.$plugin;
126         if (!class_exists($class)) {
127             $plugins = get_plugin_list_with_file('cachestore', 'lib.php');
128             if (!array_key_exists($plugin, $plugins)) {
129                 throw new cache_exception('Invalid plugin name specified. The plugin does not exist or is not valid.');
130             }
131             $file = $plugins[$plugin];
132             if (file_exists($file)) {
133                 require_once($file);
134             }
135             if (!class_exists($class)) {
136                 throw new cache_exception('Invalid cache plugin specified. The plugin does not contain the required class.');
137             }
138         }
139         if (!is_subclass_of($class, 'cache_store')) {
140             throw new cache_exception('Invalid cache plugin specified. The plugin does not extend the required class.');
141         }
142         if (!$class::are_requirements_met()) {
143             throw new cache_exception('Unable to add new cache plugin instance. The requested plugin type is not supported.');
144         }
145         $this->configstores[$name] = array(
146             'name' => $name,
147             'plugin' => $plugin,
148             'configuration' => $configuration,
149             'features' => $class::get_supported_features($configuration),
150             'modes' => $class::get_supported_modes($configuration),
151             'mappingsonly' => !empty($configuration['mappingsonly']),
152             'class' => $class,
153             'default' => false
154         );
155         if (array_key_exists('lock', $configuration)) {
156             $this->configstores[$name]['lock'] = $configuration['lock'];
157             unset($this->configstores[$name]['configuration']['lock']);
158         }
159         $this->config_save();
160         return true;
161     }
163     /**
164      * Sets the mode mappings.
165      *
166      * These determine the default caches for the different modes.
167      * This function also calls save so you should redirect immediately, or at least very shortly after
168      * calling this method.
169      *
170      * @param array $modemappings
171      * @return bool
172      * @throws cache_exception
173      */
174     public function set_mode_mappings(array $modemappings) {
175         $mappings = array(
176             cache_store::MODE_APPLICATION => array(),
177             cache_store::MODE_SESSION => array(),
178             cache_store::MODE_REQUEST => array(),
179         );
180         foreach ($modemappings as $mode => $stores) {
181             if (!array_key_exists($mode, $mappings)) {
182                 throw new cache_exception('The cache mode for the new mapping does not exist');
183             }
184             $sort = 0;
185             foreach ($stores as $store) {
186                 if (!array_key_exists($store, $this->configstores)) {
187                     throw new cache_exception('The instance name for the new mapping does not exist');
188                 }
189                 if (array_key_exists($store, $mappings[$mode])) {
190                     throw new cache_exception('This cache mapping already exists');
191                 }
192                 $mappings[$mode][] = array(
193                     'store' => $store,
194                     'mode' => $mode,
195                     'sort' => $sort++
196                 );
197             }
198         }
199         $this->configmodemappings = array_merge(
200             $mappings[cache_store::MODE_APPLICATION],
201             $mappings[cache_store::MODE_SESSION],
202             $mappings[cache_store::MODE_REQUEST]
203         );
205         $this->config_save();
206         return true;
207     }
209     /**
210      * Edits a give plugin instance.
211      *
212      * The plugin instance is determined by its name, hence you cannot rename plugins.
213      * This function also calls save so you should redirect immediately, or at least very shortly after
214      * calling this method.
215      *
216      * @param string $name
217      * @param string $plugin
218      * @param array $configuration
219      * @return bool
220      * @throws cache_exception
221      */
222     public function edit_plugin_instance($name, $plugin, $configuration) {
223         if (!array_key_exists($name, $this->configstores)) {
224             throw new cache_exception('The requested instance does not exist.');
225         }
226         $plugins = get_plugin_list_with_file('cachestore', 'lib.php');
227         if (!array_key_exists($plugin, $plugins)) {
228             throw new cache_exception('Invalid plugin name specified. The plugin either does not exist or is not valid.');
229         }
230         $class = 'cachestore_'.$plugin;
231         $file = $plugins[$plugin];
232         if (!class_exists($class)) {
233             if (file_exists($file)) {
234                 require_once($file);
235             }
236             if (!class_exists($class)) {
237                 throw new cache_exception('Invalid cache plugin specified. The plugin does not contain the required class.'.$class);
238             }
239         }
240         $this->configstores[$name] = array(
241             'name' => $name,
242             'plugin' => $plugin,
243             'configuration' => $configuration,
244             'features' => $class::get_supported_features($configuration),
245             'modes' => $class::get_supported_modes($configuration),
246             'mappingsonly' => !empty($configuration['mappingsonly']),
247             'class' => $class,
248             'default' => $this->configstores[$name]['default'] // Can't change the default.
249         );
250         if (array_key_exists('lock', $configuration)) {
251             $this->configstores[$name]['lock'] = $configuration['lock'];
252             unset($this->configstores[$name]['configuration']['lock']);
253         }
254         $this->config_save();
255         return true;
256     }
258     /**
259      * Deletes a store instance.
260      *
261      * This function also calls save so you should redirect immediately, or at least very shortly after
262      * calling this method.
263      *
264      * @param string $name The name of the instance to delete.
265      * @return bool
266      * @throws cache_exception
267      */
268     public function delete_store($name) {
269         if (!array_key_exists($name, $this->configstores)) {
270             throw new cache_exception('The requested store does not exist.');
271         }
272         if ($this->configstores[$name]['default']) {
273             throw new cache_exception('The can not delete the default stores.');
274         }
275         foreach ($this->configmodemappings as $mapping) {
276             if ($mapping['store'] === $name) {
277                 throw new cache_exception('You cannot delete a cache store that has mode mappings.');
278             }
279         }
280         foreach ($this->configdefinitionmappings as $mapping) {
281             if ($mapping['store'] === $name) {
282                 throw new cache_exception('You cannot delete a cache store that has definition mappings.');
283             }
284         }
285         unset($this->configstores[$name]);
286         $this->config_save();
287         return true;
288     }
290     /**
291      * Creates the default configuration and saves it.
292      *
293      * This function calls config_save, however it is safe to continue using it afterwards as this function should only ever
294      * be called when there is no configuration file already.
295      */
296     public static function create_default_configuration() {
297         global $CFG;
299         // HACK ALERT.
300         // We probably need to come up with a better way to create the default stores, or at least ensure 100% that the
301         // default store plugins are protected from deletion.
302         require_once($CFG->dirroot.'/cache/stores/file/lib.php');
303         require_once($CFG->dirroot.'/cache/stores/session/lib.php');
304         require_once($CFG->dirroot.'/cache/stores/static/lib.php');
306         $writer = new self;
307         $writer->configstores = array(
308             'default_application' => array(
309                 'name' => 'default_application',
310                 'plugin' => 'file',
311                 'configuration' => array(),
312                 'features' => cachestore_file::get_supported_features(),
313                 'modes' => cache_store::MODE_APPLICATION,
314                 'default' => true,
315             ),
316             'default_session' => array(
317                 'name' => 'default_session',
318                 'plugin' => 'session',
319                 'configuration' => array(),
320                 'features' => cachestore_session::get_supported_features(),
321                 'modes' => cache_store::MODE_SESSION,
322                 'default' => true,
323             ),
324             'default_request' => array(
325                 'name' => 'default_request',
326                 'plugin' => 'static',
327                 'configuration' => array(),
328                 'features' => cachestore_static::get_supported_features(),
329                 'modes' => cache_store::MODE_REQUEST,
330                 'default' => true,
331             )
332         );
333         $writer->configdefinitions = self::locate_definitions();
334         $writer->configmodemappings = array(
335             array(
336                 'mode' => cache_store::MODE_APPLICATION,
337                 'store' => 'default_application',
338                 'sort' => -1
339             ),
340             array(
341                 'mode' => cache_store::MODE_SESSION,
342                 'store' => 'default_session',
343                 'sort' => -1
344             ),
345             array(
346                 'mode' => cache_store::MODE_REQUEST,
347                 'store' => 'default_request',
348                 'sort' => -1
349             )
350         );
351         $writer->configlocks = array(
352             'default_file_lock' => array(
353                 'name' => 'cachelock_file_default',
354                 'type' => 'cachelock_file',
355                 'dir' => 'filelocks',
356                 'default' => true
357             )
358         );
359         $writer->config_save();
360     }
362     /**
363      * Updates the definition in the configuration from those found in the cache files.
364      *
365      * Calls config_save further down, you should redirect immediately or asap after calling this method.
366      */
367     public static function update_definitions() {
368         $config = self::instance();
369         $config->write_definitions_to_cache(self::locate_definitions());
370     }
372     /**
373      * Locates all of the definition files.
374      *
375      * @return array
376      */
377     protected static function locate_definitions() {
378         global $CFG;
380         $files = array();
381         if (file_exists($CFG->dirroot.'/lib/db/caches.php')) {
382             $files['core'] = $CFG->dirroot.'/lib/db/caches.php';
383         }
385         $plugintypes = get_plugin_types();
386         foreach ($plugintypes as $type => $location) {
387             $plugins = get_plugin_list_with_file($type, 'db/caches.php');
388             foreach ($plugins as $plugin => $filepath) {
389                 $component = clean_param($type.'_'.$plugin, PARAM_COMPONENT); // Standardised plugin name.
390                 $files[$component] = $filepath;
391             }
392         }
394         $definitions = array();
395         foreach ($files as $component => $file) {
396             $filedefs = self::load_caches_file($file);
397             foreach ($filedefs as $area => $definition) {
398                 $area = clean_param($area, PARAM_AREA);
399                 $id = $component.'/'.$area;
400                 $definition['component'] = $component;
401                 $definition['area'] = $area;
402                 if (array_key_exists($id, $definitions)) {
403                     debugging('Error: duplicate cache definition found with name '.$name, DEBUG_DEVELOPER);
404                     continue;
405                 }
406                 $definitions[$id] = $definition;
407             }
408         }
410         return $definitions;
411     }
413     /**
414      * Writes the updated definitions for the config file.
415      * @param array $definitions
416      */
417     private function write_definitions_to_cache(array $definitions) {
418         $this->configdefinitions = $definitions;
419         foreach ($this->configdefinitionmappings as $key => $mapping) {
420             if (!array_key_exists($mapping['definition'], $definitions)) {
421                 unset($this->configdefinitionmappings[$key]);
422             }
423         }
424         $this->config_save();
425     }
427     /**
428      * Loads the caches file if it exists.
429      * @param string $file Absolute path to the file.
430      * @return array
431      */
432     private static function load_caches_file($file) {
433         if (!file_exists($file)) {
434             return array();
435         }
436         $definitions = array();
437         include($file);
438         return $definitions;
439     }
441     /**
442      * Sets the mappings for a given definition.
443      *
444      * @param string $definition
445      * @param array $mappings
446      * @throws coding_exception
447      */
448     public function set_definition_mappings($definition, $mappings) {
449         if (!array_key_exists($definition, $this->configdefinitions)) {
450             throw new coding_exception('Invalid definition name passed when updating mappings.');
451         }
452         foreach ($mappings as $store) {
453             if (!array_key_exists($store, $this->configstores)) {
454                 throw new coding_exception('Invalid store name passed when updating definition mappings.');
455             }
456         }
457         foreach ($this->configdefinitionmappings as $key => $mapping) {
458             if ($mapping['definition'] == $definition) {
459                 unset($this->configdefinitionmappings[$key]);
460             }
461         }
462         $sort = count($mappings);
463         foreach ($mappings as $store) {
464             $this->configdefinitionmappings[] = array(
465                 'store' => $store,
466                 'definition' => $definition,
467                 'sort' => $sort
468             );
469             $sort--;
470         }
472         $this->config_save();
473     }
477 /**
478  * A cache helper for administration tasks
479  *
480  * @package    core
481  * @category   cache
482  * @copyright  2012 Sam Hemelryk
483  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
484  */
485 abstract class cache_administration_helper extends cache_helper {
487     /**
488      * Returns an array containing all of the information about stores a renderer needs.
489      * @return array
490      */
491     public static function get_store_summaries() {
492         $return = array();
493         $default = array();
494         $instance = cache_config::instance();
495         $stores = $instance->get_all_stores();
496         foreach ($stores as $name => $details) {
497             $class = $details['class'];
498             $store = new $class($details['name'], $details['configuration']);
499             $record = array(
500                 'name' => $name,
501                 'plugin' => $details['plugin'],
502                 'default' => $details['default'],
503                 'isready' => $store->is_ready(),
504                 'requirementsmet' => $store->are_requirements_met(),
505                 'mappings' => 0,
506                 'modes' => array(
507                     cache_store::MODE_APPLICATION =>
508                         ($store->get_supported_modes($return) & cache_store::MODE_APPLICATION) == cache_store::MODE_APPLICATION,
509                     cache_store::MODE_SESSION =>
510                         ($store->get_supported_modes($return) & cache_store::MODE_SESSION) == cache_store::MODE_SESSION,
511                     cache_store::MODE_REQUEST =>
512                         ($store->get_supported_modes($return) & cache_store::MODE_REQUEST) == cache_store::MODE_REQUEST,
513                 ),
514                 'supports' => array(
515                     'multipleidentifiers' => $store->supports_multiple_indentifiers(),
516                     'dataguarantee' => $store->supports_data_guarantee(),
517                     'nativettl' => $store->supports_native_ttl(),
518                     'nativelocking' => ($store instanceof cache_is_lockable),
519                     'keyawareness' => ($store instanceof cache_is_key_aware),
520                 )
521             );
522             if (empty($details['default'])) {
523                 $return[$name] = $record;
524             } else {
525                 $default[$name] = $record;
526             }
527         }
529         ksort($return);
530         ksort($default);
531         $return = $return + $default;
533         foreach ($instance->get_definition_mappings() as $mapping) {
534             if (!array_key_exists($mapping['store'], $return)) {
535                 continue;
536             }
537             $return[$mapping['store']]['mappings']++;
538         }
540         return $return;
541     }
543     /**
544      * Returns an array of information about plugins, everything a renderer needs.
545      * @return array
546      */
547     public static function get_plugin_summaries() {
548         $return = array();
549         $plugins = get_plugin_list_with_file('cachestore', 'lib.php', true);
550         foreach ($plugins as $plugin => $path) {
551             $class = 'cachestore_'.$plugin;
552             $return[$plugin] = array(
553                 'name' => get_string('pluginname', 'cachestore_'.$plugin),
554                 'requirementsmet' => $class::are_requirements_met(),
555                 'instances' => 0,
556                 'modes' => array(
557                     cache_store::MODE_APPLICATION => ($class::get_supported_modes() & cache_store::MODE_APPLICATION),
558                     cache_store::MODE_SESSION => ($class::get_supported_modes() & cache_store::MODE_SESSION),
559                     cache_store::MODE_REQUEST => ($class::get_supported_modes() & cache_store::MODE_REQUEST),
560                 ),
561                 'supports' => array(
562                     'multipleidentifiers' => ($class::get_supported_features() & cache_store::SUPPORTS_MULTIPLE_IDENTIFIERS),
563                     'dataguarantee' => ($class::get_supported_features() & cache_store::SUPPORTS_DATA_GUARANTEE),
564                     'nativettl' => ($class::get_supported_features() & cache_store::SUPPORTS_NATIVE_TTL),
565                     'nativelocking' => (in_array('cache_is_lockable', class_implements($class))),
566                     'keyawareness' => (array_key_exists('cache_is_key_aware', class_implements($class))),
567                 ),
568                 'canaddinstance' => ($class::can_add_instance())
569             );
570         }
572         $instance = cache_config::instance();
573         $stores = $instance->get_all_stores();
574         foreach ($stores as $store) {
575             $plugin = $store['plugin'];
576             if (array_key_exists($plugin, $return)) {
577                 $return[$plugin]['instances']++;
578             }
579         }
581         return $return;
582     }
584     /**
585      * Returns an array about the definitions. All the information a renderer needs.
586      * @return array
587      */
588     public static function get_definition_summaries() {
589         $instance = cache_config::instance();
590         $definitions = $instance->get_definitions();
592         $storenames = array();
593         foreach ($instance->get_all_stores() as $key => $store) {
594             if (!empty($store['default'])) {
595                 $storenames[$key] = new lang_string('store_'.$key, 'cache');
596             }
597         }
599         $modemappings = array();
600         foreach ($instance->get_mode_mappings() as $mapping) {
601             $mode = $mapping['mode'];
602             if (!array_key_exists($mode, $modemappings)) {
603                 $modemappings[$mode] = array();
604             }
605             if (array_key_exists($mapping['store'], $storenames)) {
606                 $modemappings[$mode][] = $storenames[$mapping['store']];
607             } else {
608                 $modemappings[$mode][] = $mapping['store'];
609             }
610         }
612         $definitionmappings = array();
613         foreach ($instance->get_definition_mappings() as $mapping) {
614             $definition = $mapping['definition'];
615             if (!array_key_exists($definition, $definitionmappings)) {
616                 $definitionmappings[$definition] = array();
617             }
618             if (array_key_exists($mapping['store'], $storenames)) {
619                 $definitionmappings[$definition][] = $storenames[$mapping['store']];
620             } else {
621                 $definitionmappings[$definition][] = $mapping['store'];
622             }
623         }
625         $return = array();
627         foreach ($definitions as $id => $definition) {
629             $mappings = array();
630             if (array_key_exists($id, $definitionmappings)) {
631                 $mappings = $definitionmappings[$id];
632             } else if (empty($definition['mappingsonly'])) {
633                 $mappings = $modemappings[$definition['mode']];
634             }
636             $return[$id] = array(
637                 'id' => $id,
638                 'name' => cache_helper::get_definition_name($definition),
639                 'mode' => $definition['mode'],
640                 'component' => $definition['component'],
641                 'area' => $definition['area'],
642                 'mappings' => $mappings
643             );
644         }
645         return $return;
646     }
648     /**
649      * Returns all of the actions that can be performed on a definition.
650      * @param context $context
651      * @return array
652      */
653     public static function get_definition_actions(context $context) {
654         if (has_capability('moodle/site:config', $context)) {
655             return array(
656                 array(
657                     'text' => get_string('editmappings', 'cache'),
658                     'url' => new moodle_url('/cache/admin.php', array('action' => 'editdefinitionmapping', 'sesskey' => sesskey()))
659                 )
660             );
661         }
662         return array();
663     }
665     /**
666      * Returns all of the actions that can be performed on a store.
667      *
668      * @param string $name The name of the store
669      * @param array $storedetails
670      * @return array
671      */
672     public static function get_store_actions($name, array $storedetails) {
673         $actions = array();
674         if (has_capability('moodle/site:config', get_system_context())) {
675             $baseurl = new moodle_url('/cache/admin.php', array('store' => $name, 'sesskey' => sesskey()));
676             if (empty($storedetails['default'])) {
677                 $actions[] = array(
678                     'text' => get_string('editstore', 'cache'),
679                     'url' => new moodle_url($baseurl, array('action' => 'editstore', 'plugin' => $storedetails['plugin']))
680                 );
681                 $actions[] = array(
682                     'text' => get_string('deletestore', 'cache'),
683                     'url' => new moodle_url($baseurl, array('action' => 'deletestore'))
684                 );
685             }
686             $actions[] = array(
687                 'text' => get_string('purge', 'cache'),
688                 'url' => new moodle_url($baseurl, array('action' => 'purge'))
689             );
690         }
691         return $actions;
692     }
695     /**
696      * Returns all of the actions that can be performed on a plugin.
697      *
698      * @param string $name The name of the plugin
699      * @param array $plugindetails
700      * @return array
701      */
702     public static function get_plugin_actions($name, array $plugindetails) {
703         $actions = array();
704         if (has_capability('moodle/site:config', get_system_context())) {
705             if (!empty($plugindetails['canaddinstance'])) {
706                 $url = new moodle_url('/cache/admin.php', array('action' => 'addstore', 'plugin' => $name, 'sesskey' => sesskey()));
707                 $actions[] = array(
708                     'text' => get_string('addinstance', 'cache'),
709                     'url' => $url
710                 );
711             }
712         }
713         return $actions;
714     }
716     /**
717      * Returns a form that can be used to add a store instance.
718      *
719      * @param string $plugin The plugin to add an instance of
720      * @return cachestore_addinstance_form
721      * @throws coding_exception
722      */
723     public static function get_add_store_form($plugin) {
724         global $CFG; // Needed for includes.
725         $plugins = get_plugin_list('cachestore');
726         if (!array_key_exists($plugin, $plugins)) {
727             throw new coding_exception('Invalid cache plugin used when trying to create an edit form.');
728         }
729         $plugindir = $plugins[$plugin];
730         $class = 'cachestore_addinstance_form';
731         if (file_exists($plugindir.'/addinstanceform.php')) {
732             require_once($plugindir.'/addinstanceform.php');
733             if (class_exists('cachestore_'.$plugin.'_addinstance_form')) {
734                 $class = 'cachestore_'.$plugin.'_addinstance_form';
735                 if (!array_key_exists('cachestore_addinstance_form', class_parents($class))) {
736                     throw new coding_exception('Cache plugin add instance forms must extend cachestore_addinstance_form');
737                 }
738             }
739         }
741         $locks = self::get_possible_locks_for_plugin($plugindir, $plugin);
743         $url = new moodle_url('/cache/admin.php', array('action' => 'addstore'));
744         return new $class($url, array('plugin' => $plugin, 'store' => null, 'locks' => $locks));
745     }
747     /**
748      * Returns a form that can be used to edit a store instance.
749      *
750      * @param string $plugin
751      * @param string $store
752      * @return cachestore_addinstance_form
753      * @throws coding_exception
754      */
755     public static function get_edit_store_form($plugin, $store) {
756         global $CFG; // Needed for includes.
757         $plugins = get_plugin_list('cachestore');
758         if (!array_key_exists($plugin, $plugins)) {
759             throw new coding_exception('Invalid cache plugin used when trying to create an edit form.');
760         }
761         $factory = cache_factory::instance();
762         $config = $factory->create_config_instance();
763         $stores = $config->get_all_stores();
764         if (!array_key_exists($store, $stores)) {
765             throw new coding_exception('Invalid store name given when trying to create an edit form.');
766         }
767         $plugindir = $plugins[$plugin];
768         $class = 'cachestore_addinstance_form';
769         if (file_exists($plugindir.'/addinstanceform.php')) {
770             require_once($plugindir.'/addinstanceform.php');
771             if (class_exists('cachestore_'.$plugin.'_addinstance_form')) {
772                 $class = 'cachestore_'.$plugin.'_addinstance_form';
773                 if (!array_key_exists('cachestore_addinstance_form', class_parents($class))) {
774                     throw new coding_exception('Cache plugin add instance forms must extend cachestore_addinstance_form');
775                 }
776             }
777         }
779         $locks = self::get_possible_locks_for_plugin($plugindir, $plugin);
781         $url = new moodle_url('/cache/admin.php', array('action' => 'editstore'));
782         return new $class($url, array('plugin' => $plugin, 'store' => $store, 'locks' => $locks));
783     }
785     /**
786      * Returns an array of suitable lock instances for use with this plugin, or false if the plugin handles locking itself.
787      *
788      * @param string $plugindir
789      * @param string $plugin
790      * @return array|false
791      */
792     protected static function get_possible_locks_for_plugin($plugindir, $plugin) {
793         global $CFG; // Needed for includes.
794         $supportsnativelocking = false;
795         if (file_exists($plugindir.'/lib.php')) {
796             require_once($plugindir.'/lib.php');
797             $pluginclass = 'cachestore_'.$plugin;
798             if (class_exists($pluginclass)) {
799                 $supportsnativelocking = array_key_exists('cache_is_lockable', class_implements($pluginclass));
800             }
801         }
803         if (!$supportsnativelocking) {
804             $config = cache_config::instance();
805             $locks = array();
806             foreach ($config->get_locks() as $lock => $conf) {
807                 if (!empty($conf['default'])) {
808                     $name = get_string($lock, 'cache');
809                 } else {
810                     $name = $lock;
811                 }
812                 $locks[$lock] = $name;
813             }
814         } else {
815             $locks = false;
816         }
818         return $locks;
819     }
821     /**
822      * Processes the results of the add/edit instance form data for a plugin returning an array of config information suitable to
823      * store in configuration.
824      *
825      * @param stdClass $data The mform data.
826      * @return array
827      * @throws coding_exception
828      */
829     public static function get_store_configuration_from_data(stdClass $data) {
830         global $CFG;
831         $file = $CFG->dirroot.'/cache/stores/'.$data->plugin.'/lib.php';
832         if (!file_exists($file)) {
833             throw new coding_exception('Invalid cache plugin provided. '.$file);
834         }
835         require_once($file);
836         $class = 'cachestore_'.$data->plugin;
837         $method = 'config_get_configuration_array';
838         if (!class_exists($class)) {
839             throw new coding_exception('Invalid cache plugin provided.');
840         }
841         if (method_exists($class, $method)) {
842             return call_user_func(array($class, $method), $data);
843         }
844         return array();
845     }
847     /**
848      * Get an array of stores that are suitable to be used for a given definition.
849      *
850      * @param string $component
851      * @param string $area
852      * @return array Array containing 3 elements
853      *      1. An array of currently used stores
854      *      2. An array of suitable stores
855      *      3. An array of default stores
856      */
857     public static function get_definition_store_options($component, $area) {
858         $factory = cache_factory::instance();
859         $definition = $factory->create_definition($component, $area);
860         $config = cache_config::instance();
861         $currentstores = $config->get_stores_for_definition($definition);
862         $possiblestores = $config->get_stores($definition->get_mode(), $definition->get_requirements_bin());
864         $defaults = array();
865         foreach ($currentstores as $key => $store) {
866             if (!empty($store['default'])) {
867                 $defaults[] = $key;
868                 unset($currentstores[$key]);
869             }
870         }
871         foreach ($possiblestores as $key => $store) {
872             if ($store['default']) {
873                 unset($possiblestores[$key]);
874                 $possiblestores[$key] = $store;
875             }
876         }
877         return array($currentstores, $possiblestores, $defaults);
878     }
880     /**
881      * Get the default stores for all modes.
882      *
883      * @return array An array containing sub-arrays, one for each mode.
884      */
885     public static function get_default_mode_stores() {
886         $instance = cache_config::instance();
887         $storenames = array();
888         foreach ($instance->get_all_stores() as $key => $store) {
889             if (!empty($store['default'])) {
890                 $storenames[$key] = new lang_string('store_'.$key, 'cache');
891             }
892         }
893         $modemappings = array(
894             cache_store::MODE_APPLICATION => array(),
895             cache_store::MODE_SESSION => array(),
896             cache_store::MODE_REQUEST => array(),
897         );
898         foreach ($instance->get_mode_mappings() as $mapping) {
899             $mode = $mapping['mode'];
900             if (!array_key_exists($mode, $modemappings)) {
901                 debugging('Unknown mode in cache store mode mappings', DEBUG_DEVELOPER);
902                 continue;
903             }
904             if (array_key_exists($mapping['store'], $storenames)) {
905                 $modemappings[$mode][$mapping['store']] = $storenames[$mapping['store']];
906             } else {
907                 $modemappings[$mode][$mapping['store']] = $mapping['store'];
908             }
909         }
910         return $modemappings;
911     }
913     /**
914      * Returns an array summarising the locks available in the system
915      */
916     public static function get_lock_summaries() {
917         $locks = array();
918         $instance = cache_config::instance();
919         $stores = $instance->get_all_stores();
920         foreach ($instance->get_locks() as $lock) {
921             $default = !empty($lock['default']);
922             if ($default) {
923                 $name = new lang_string($lock['name'], 'cache');
924             } else {
925                 $name = $lock['name'];
926             }
927             $uses = 0;
928             foreach ($stores as $store) {
929                 if (!empty($store['lock']) && $store['lock'] === $lock['name']) {
930                     $uses++;
931                 }
932             }
933             $lockdata = array(
934                 'name' => $name,
935                 'default' => $default,
936                 'uses' => $uses
937             );
938             $locks[] = $lockdata;
939         }
940         return $locks;
941     }