Merge branch 'wip-MDL-25290-m24-compact' of git://github.com/samhemelryk/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Sun, 14 Oct 2012 21:32:40 +0000 (23:32 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Sun, 14 Oct 2012 21:32:40 +0000 (23:32 +0200)
59 files changed:
admin/settings/plugins.php
admin/settings/server.php
cache/README.md [new file with mode: 0644]
cache/admin.php [new file with mode: 0644]
cache/classes/config.php [new file with mode: 0644]
cache/classes/definition.php [new file with mode: 0644]
cache/classes/dummystore.php [new file with mode: 0644]
cache/classes/factory.php [new file with mode: 0644]
cache/classes/helper.php [new file with mode: 0644]
cache/classes/interfaces.php [new file with mode: 0644]
cache/classes/loaders.php [new file with mode: 0644]
cache/forms.php [new file with mode: 0644]
cache/lib.php [new file with mode: 0644]
cache/locallib.php [new file with mode: 0644]
cache/locks/file/lang/en/cachelock_file.php [new file with mode: 0644]
cache/locks/file/lib.php [new file with mode: 0644]
cache/renderer.php [new file with mode: 0644]
cache/stores/file/addinstanceform.php [new file with mode: 0644]
cache/stores/file/lang/en/cachestore_file.php [new file with mode: 0644]
cache/stores/file/lib.php [new file with mode: 0644]
cache/stores/file/version.php [new file with mode: 0644]
cache/stores/memcache/addinstanceform.php [new file with mode: 0644]
cache/stores/memcache/lang/en/cachestore_memcache.php [new file with mode: 0644]
cache/stores/memcache/lib.php [new file with mode: 0644]
cache/stores/memcache/settings.php [new file with mode: 0644]
cache/stores/memcache/version.php [new file with mode: 0644]
cache/stores/memcached/addinstanceform.php [new file with mode: 0644]
cache/stores/memcached/lang/en/cachestore_memcached.php [new file with mode: 0644]
cache/stores/memcached/lib.php [new file with mode: 0644]
cache/stores/memcached/settings.php [new file with mode: 0644]
cache/stores/memcached/version.php [new file with mode: 0644]
cache/stores/mongodb/addinstanceform.php [new file with mode: 0644]
cache/stores/mongodb/lang/en/cachestore_mongodb.php [new file with mode: 0644]
cache/stores/mongodb/lib.php [new file with mode: 0644]
cache/stores/mongodb/settings.php [new file with mode: 0644]
cache/stores/mongodb/version.php [new file with mode: 0644]
cache/stores/session/lang/en/cachestore_session.php [new file with mode: 0644]
cache/stores/session/lib.php [new file with mode: 0644]
cache/stores/session/version.php [new file with mode: 0644]
cache/stores/static/lang/en/cachestore_static.php [new file with mode: 0644]
cache/stores/static/lib.php [new file with mode: 0644]
cache/stores/static/version.php [new file with mode: 0644]
cache/testperformance.php [new file with mode: 0644]
cache/tests/cache_test.php [new file with mode: 0644]
cache/tests/fixtures/lib.php [new file with mode: 0644]
cache/tests/locallib_test.php [new file with mode: 0644]
config-dist.php
lang/en/admin.php
lang/en/cache.php [new file with mode: 0644]
lang/en/plugin.php
lib/db/caches.php [new file with mode: 0644]
lib/moodlelib.php
lib/phpunit/classes/util.php
lib/pluginlib.php
lib/sessionlib.php
lib/setup.php
phpunit.xml.dist
theme/base/style/admin.css
theme/base/style/core.css

index 4f22f08..c2b2b22 100644 (file)
@@ -492,6 +492,22 @@ foreach (get_plugin_list('tool') as $plugin => $plugindir) {
     }
 }
 
+// Now add the Cache plugins
+if ($hassiteconfig) {
+    $ADMIN->add('modules', new admin_category('cache', new lang_string('caching', 'cache')));
+    $ADMIN->add('cache', new admin_externalpage('cacheconfig', new lang_string('cacheconfig', 'cache'), $CFG->wwwroot .'/cache/admin.php'));
+    $ADMIN->add('cache', new admin_externalpage('cachetestperformance', new lang_string('testperformance', 'cache'), $CFG->wwwroot . '/cache/testperformance.php'));
+    $ADMIN->add('cache', new admin_category('cachestores', new lang_string('cachestores', 'cache')));
+    foreach (get_plugin_list('cachestore') as $plugin => $path) {
+        $settingspath = $path.'/settings.php';
+        if (file_exists($settingspath)) {
+            $settings = new admin_settingpage('cachestore_'.$plugin.'_settings', new lang_string('pluginname', 'cachestore_'.$plugin), 'moodle/site:config');
+            include($settingspath);
+            $ADMIN->add('cachestores', $settings);
+        }
+    }
+}
+
 /// Add all local plugins - must be always last!
 if ($hassiteconfig) {
     $ADMIN->add('modules', new admin_category('localplugins', new lang_string('localplugins')));
index 461f06c..0c8746e 100644 (file)
@@ -216,6 +216,7 @@ $temp->add(new admin_setting_configselect('memcachedpconn', new lang_string('mem
                                           array( '0' => new lang_string('no'),
                                                  '1' => new lang_string('yes'))));
 */
+
 $ADMIN->add('server', $temp);
 
 
diff --git a/cache/README.md b/cache/README.md
new file mode 100644 (file)
index 0000000..8317cb9
--- /dev/null
@@ -0,0 +1,207 @@
+MUC development code
+====================
+
+Congratulations you've found the MUC development code.
+This code is still very much in development and as such is not (and is know to not) function correctly or completely at the moment.
+Of course that will all be well and truly sorted out WELL before this gets integrated.
+
+Sample code snippets
+--------------------
+
+A definition:
+
+     $definitions = array(
+        'core_string' => array(                       // Required, unique
+            'mode' => cache_store::MODE_APPLICATION,  // Required
+            'component' => 'core',                    // Required
+            'area' => 'string',                       // Required
+            'requireidentifiers' => array(            // Optional
+                'lang',
+                'component'
+            ),
+            'requiredataguarantee' => false,          // Optional
+            'requiremultipleidentifiers' => false,    // Optional
+            'overrideclass' => null,                  // Optional
+            'overrideclassfile' => null,              // Optional
+            'datasource' => null,                     // Optional
+            'datasourcefile' => null,                 // Optional
+            'persistent' => false,                    // Optional
+            'ttl' => 0,                               // Optional
+            'mappingsonly' => false                   // Optional
+            'invalidationevents' => array(            // Optional
+                'contextmarkeddirty'
+            ),
+        )
+    );
+
+Getting a something from a cache using the definition:
+
+    $cache = cache::make('core', 'string');
+    if (!$component = $cache->get('component')) {
+        // get returns false if its not there and can't be loaded.
+        $component = generate_data();
+        $cache->set($component);
+    }
+
+The same thing but from using params:
+
+    $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'string');
+    if (!$component = $cache->get('component')) {
+        // get returns false if its not there and can't be loaded.
+        $component = generate_data();
+        $cache->set($component);
+    }
+
+If a data source had been specified in the definition the following would be all that was needed.
+
+    $cache = cache::make('core', 'string');
+    $component = $cache->get('component');
+
+The bits that make up the cache API
+-----------------------------------
+
+There are several parts that _**will**_ make up this solution:
+
+### Loader
+The loader is central to the whole thing.
+It is used by the end developer to get an object that handles caching.
+90% of end developers will not need to know or use anything else about the cache API.
+In order to get a loader you must use one of two static methods, make, or make_with_params.
+To the end developer interacting with the loader is simple and is dictated by the cache_loader interface.
+Internally there is lots of magic going on. The important parts to know about are:
+* There are two ways to get with a loader, the first with a definition (discussed below) the second with params. When params are used they are turned into an adhoc definition with default params.
+* A loader get passed three things when being constructed, a definition, a store, and another loader or datasource if there is either.
+* If a loader is the third arg then requests will be chained to provide redundancy.
+* If a data source is provided then requests for an item that is not cached will be passed to the data source and that will be expected to load the data. If it loads data that data is stored in each store on its way back to the user.
+* There are three core loaders. One for each application, session, and request.
+* A custom loader can be used. It will be provided by the definition (thus cannot be used with adhoc definitions) and must override the appropriate core loader
+* The loader handles ttl for stores that don't natively support ttl.
+* The application loader handles locking for stores that don't natively support locking.
+
+### Store
+The store is the bridge between the cache API and a cache solution.
+Cache store plugins exist within moodle/cache/store.
+The administrator of a site can configure multiple instances of each plugin, the configuration gets initialised as a store for the loader when required in code (during construction of the loader).
+The following points highlight things you should know about stores.
+* A cache_store interface is used to define the requirements of a store plugin.
+* The store plugin can inherit the cache_is_lockable interface to handle its own locking.
+* The store plugin can inherit the cache_is_key_aware interface to handle is own has checks.
+* Store plugins inform the cache API about the things they support. Features can be required by a definition.
+** Data guarantee - Data is guaranteed to exist in the cache once it is set there. It is never cleaned up to free space or because it has not been recently used.
+** Multiple identifiers - Rather than a single string key, the parts that make up the key are passed as an array.
+** Native TTL support - When required the store supports native ttl and doesn't require the cache API to manage ttl of things given to the store.
+
+### Definition
+_Definitions were not a part of the previous proposal._
+Definitions are cache definitions. They will be located within a new file for each component/plugin at **db/caches.php**.
+They can be used to set all of the requirements of a cache instance and are used to ensure that a cache can only be interacted with in the same way no matter where it is being used.
+It also ensure that caches are easy to use, the config is stored in the definition and the developer using the cache does not need to know anything about it.
+When getting a loader you can either provide a definition name, or a set or params.
+* If you provide a definition name then the matching definition is found and used to construct a loader for you.
+* If you provide params then an adhoc definition is created. It will have defaults and will not have any special requirements or options set.
+
+Definitions are designed to be used in situations where things are more than basic.
+
+The following settings are required for a definition:
+* name - Identifies the definition and must be unique.
+* mode - Application, session, request.
+* component - The component associated the definition is associated with.
+* area - Describes the stuff being cached.
+
+The following optional settings can also be defined:
+* requireidentifiers - Any identifiers the definition requires. Must be provided when creating the loader.
+* requiredataguarantee - If set to true then only stores that support data guarantee will be used.
+* requiremultipleidentifiers - If set to true then only stores that support multiple identifiers will be used.
+* overrideclass - If provided this class will be used for the loader. It must extend on of the core loader classes (based upon mode).
+* overrideclassfile - Included if required when using the overrideclass param.
+* datasource - If provided this class will be used as a data source for the definition. It must implement the cache_data_source interface.
+* datasourcefile - Included if required when using the datasource param.
+* persistent - If set to true the loader will be stored when first created and provided to subsequent requests. More on this later.
+* ttl - Can be used to set a ttl value for data being set for this cache.
+* mappingsonly - This definition can only be used if there is a store mapping for it. More on this later.
+* invalidationevents - An array of events that should trigger this cache to invalidate.
+
+The persist option.
+As noted the persist option causes the loader generated for this definition to be stored when first created. Subsequent requests for this definition will be given the original loader instance.
+Data passed to or retrieved from the loader and its chained loaders gets cached by the instance.
+This option should be used when you know you will require the loader several times and perhaps in different areas of code.
+Because it caches key=>value data it avoids the need to re-fetch things from stores after the first request. Its good for performance, bad for memory.
+It should be used sparingly.
+
+The mappingsonly option.
+The administrator of a site can create mappings between stores and definitions. Allowing them to designate stores for specific definitions (caches).
+Setting this option to true means that the definition can only be used if a mapping has been made for it.
+Normally if no mappings exist then the default store for the definition mode is used.
+
+### Data source
+Data sources allow cache _misses_ (requests for a key that doesn't exist) to be handled and loaded internally.
+The loader gets used as the last resort if provided and means that code using the cache doesn't need to handle the situation that information isn't cached.
+They can be specified in a cache definition and must implement the cache_data_source interface.
+
+### How it all chains together.
+Consider the following if you can:
+
+Basic request for information (no frills):
+
+    => Code calls get
+        => Loader handles get, passes the request to its store
+            <= Memcache doesn't have the data. sorry.
+        <= Loader returns the result.
+    |= Code couldn't get the data from the cache. It must generate it and then ask the loader to cache it.
+
+Advanced initial request for information not already cached (has chained stores and data source):
+
+    => Code calls get
+        => Loader handles get, passes the request to its store
+            => Memcache handles request, doesn't have it passes it to the chained store
+                => File (default store) doesn't have it requests it from the loader
+                    => Data source - makes required db calls, processes information
+                        ...database calls...
+                        ...processing and moulding...
+                    <= Data source returns the information
+                <= File caches the information on its way back through
+            <= Memcache caches the information on its way back through
+        <= Loader returns the data to the user.
+    |= Code the code now has the data.
+
+Subsequent request for information:
+
+    => Code calls get
+        => Loader handles get, passes the request to its store
+            <= Store returns the data
+        <= Loader returns the data
+    |= Code has the data
+
+Other internal magic you should be aware of
+-------------------------------------------
+The following should fill you in on a bit more of the behind the scenes stuff for the cache API.
+
+### Helper class
+There is a helper class called cache_helper which is abstract with static methods.
+This class handles much of the internal generation and initialisation requirements.
+In normal use this class will not be needed outside of the API (mostly internal use only)
+
+### Configuration
+There are two configuration classes cache_config and cache_config_writer.
+The reader class is used for every request, the writer is only used when modifying the configuration.
+Because the cache API is designed to cache database configuration and meta data it must be able to operate prior to database configuration being loaded.
+To get around this we store the configuration information in a file in the dataroot.
+The configuration file contains information on the configured store instances, definitions collected from definition files, and mappings.
+That information is stored and loaded in the same way we work with the lang string files.
+This means that we use the cache API as soon as it has been included.
+
+### Invalidation
+Cache information can be invalidated in two ways.
+1. pass a definition name and the keys to be invalidated (or none to invalidate the whole cache).
+2. pass an event and the keys to be invalidated.
+
+The first method is designed to be used when you have a single known definition you want to invalidate entries from within.
+The second method is a lot more intensive for the system. There are defined invalidation events that definitions can "subscribe" to (through the definitions invalidationevents option).
+When you invalidate by event the cache API finds all of the definitions that subscribe to the event, it then loads the stores for each of those definitions and purges the keys from each store.
+This is obviously a recursive and therefor intense process.
+
+TODO's and things still to think about
+--------------------------------------
+
+ 1. Definitions don't really need/use the component/area identifiers presently. They may be useful in the future... they may not be.
+    We should consider whether we leave them, or remove them.
\ No newline at end of file
diff --git a/cache/admin.php b/cache/admin.php
new file mode 100644 (file)
index 0000000..aeed928
--- /dev/null
@@ -0,0 +1,201 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The administration and management interface for the cache setup and configuration.
+ *
+ * This file is part of Moodle's cache API, affectionately called MUC.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once('../config.php');
+require_once($CFG->dirroot.'/lib/adminlib.php');
+require_once($CFG->dirroot.'/cache/locallib.php');
+require_once($CFG->dirroot.'/cache/forms.php');
+
+$action = optional_param('action', null, PARAM_ALPHA);
+
+admin_externalpage_setup('cacheconfig');
+$context = context_system::instance();
+
+$stores = cache_administration_helper::get_store_instance_summaries();
+$plugins = cache_administration_helper::get_store_plugin_summaries();
+$definitions = cache_administration_helper::get_definition_summaries();
+$defaultmodestores = cache_administration_helper::get_default_mode_stores();
+$locks = cache_administration_helper::get_lock_summaries();
+
+$title = new lang_string('cacheadmin', 'cache');
+$mform = null;
+$notification = null;
+$notifysuccess = true;
+
+if (!empty($action) && confirm_sesskey()) {
+    switch ($action) {
+        case 'rescandefinitions' : // Rescan definitions.
+            cache_config_writer::update_definitions();
+            redirect($PAGE->url);
+            break;
+        case 'addstore' : // Add the requested store.
+            $plugin = required_param('plugin', PARAM_PLUGIN);
+            $mform = cache_administration_helper::get_add_store_form($plugin);
+            $title = get_string('addstore', 'cache', $plugins[$plugin]['name']);
+            if ($mform->is_cancelled()) {
+                rediect($PAGE->url);
+            } else if ($data = $mform->get_data()) {
+                $config = cache_administration_helper::get_store_configuration_from_data($data);
+                $writer = cache_config_writer::instance();
+                unset($config['lock']);
+                foreach ($writer->get_locks() as $lock => $lockconfig) {
+                    if ($lock == $data->lock) {
+                        $config['lock'] = $data->lock;
+                    }
+                }
+                $writer->add_store_instance($data->name, $data->plugin, $config);
+                redirect($PAGE->url, get_string('addstoresuccess', 'cache', $plugins[$plugin]['name']), 5);
+            }
+            break;
+        case 'editstore' : // Edit the requested store.
+            $plugin = required_param('plugin', PARAM_PLUGIN);
+            $store = required_param('store', PARAM_TEXT);
+            $mform = cache_administration_helper::get_edit_store_form($plugin, $store);
+            $title = get_string('addstore', 'cache', $plugins[$plugin]['name']);
+            if ($mform->is_cancelled()) {
+                rediect($PAGE->url);
+            } else if ($data = $mform->get_data()) {
+                $config = cache_administration_helper::get_store_configuration_from_data($data);
+                $writer = cache_config_writer::instance();
+                unset($config['lock']);
+                foreach ($writer->get_locks() as $lock => $lockconfig) {
+                    if ($lock == $data->lock) {
+                        $config['lock'] = $data->lock;
+                    }
+                }
+                $writer->edit_store_instance($data->name, $data->plugin, $config);
+                redirect($PAGE->url, get_string('editstoresuccess', 'cache', $plugins[$plugin]['name']), 5);
+            }
+            break;
+        case 'deletestore' : // Delete a given store.
+            $store = required_param('store', PARAM_TEXT);
+            $confirm = optional_param('confirm', false, PARAM_BOOL);
+
+            if (!array_key_exists($store, $stores)) {
+                $notifysuccess = false;
+                $notification = get_string('invalidstore');
+            } else if ($stores[$store]['mappings'] > 0) {
+                $notifysuccess = false;
+                $notification = get_string('deletestorehasmappings');
+            }
+
+            if ($notifysuccess) {
+                if (!$confirm) {
+                    $title = get_string('confirmstoredeletion', 'cache');
+                    $params = array('store' => $store, 'confirm' => 1, 'action' => $action, 'sesskey' => sesskey());
+                    $url = new moodle_url($PAGE->url, $params);
+                    $button = new single_button($url, get_string('deletestore', 'cache'));
+
+                    $PAGE->set_title($title);
+                    $PAGE->set_heading($SITE->fullname);
+                    echo $OUTPUT->header();
+                    echo $OUTPUT->heading($title);
+                    $confirmation = get_string('deletestoreconfirmation', 'cache', $stores[$store]['name']);
+                    echo $OUTPUT->confirm($confirmation, $button, $PAGE->url);
+                    echo $OUTPUT->footer();
+                    exit;
+                } else {
+                    $writer = cache_config_writer::instance();
+                    $writer->delete_store_instance($store);
+                    redirect($PAGE->url, get_string('deletestoresuccess', 'cache'), 5);
+                }
+            }
+            break;
+        case 'editdefinitionmapping' : // Edit definition mappings.
+            $definition = required_param('definition', PARAM_TEXT);
+            $title = get_string('editdefinitionmappings', 'cache', $definition);
+            $mform = new cache_definition_mappings_form($PAGE->url, array('definition' => $definition));
+            if ($mform->is_cancelled()) {
+                redirect($PAGE->url);
+            } else if ($data = $mform->get_data()) {
+                $writer = cache_config_writer::instance();
+                $mappings = array();
+                foreach ($data->mappings as $mapping) {
+                    if (!empty($mapping)) {
+                        $mappings[] = $mapping;
+                    }
+                }
+                $writer->set_definition_mappings($definition, $mappings);
+                redirect($PAGE->url);
+            }
+            break;
+        case 'editmodemappings': // Edit default mode mappings.
+            $mform = new cache_mode_mappings_form(null, $stores);
+            $mform->set_data(array(
+                'mode_'.cache_store::MODE_APPLICATION => key($defaultmodestores[cache_store::MODE_APPLICATION]),
+                'mode_'.cache_store::MODE_SESSION => key($defaultmodestores[cache_store::MODE_SESSION]),
+                'mode_'.cache_store::MODE_REQUEST => key($defaultmodestores[cache_store::MODE_REQUEST]),
+            ));
+            if ($mform->is_cancelled()) {
+                redirect($PAGE->url);
+            } else if ($data = $mform->get_data()) {
+                $mappings = array(
+                    cache_store::MODE_APPLICATION => array($data->{'mode_'.cache_store::MODE_APPLICATION}),
+                    cache_store::MODE_SESSION => array($data->{'mode_'.cache_store::MODE_SESSION}),
+                    cache_store::MODE_REQUEST => array($data->{'mode_'.cache_store::MODE_REQUEST}),
+                );
+                $writer = cache_config_writer::instance();
+                $writer->set_mode_mappings($mappings);
+                redirect($PAGE->url);
+            }
+            break;
+
+        case 'purge': // Purge a store cache.
+            $store = required_param('store', PARAM_TEXT);
+            cache_helper::purge_store($store);
+            redirect($PAGE->url, get_string('purgestoresuccess', 'cache'), 5);
+            break;
+    }
+}
+
+$PAGE->set_title($title);
+$PAGE->set_heading($SITE->fullname);
+$renderer = $PAGE->get_renderer('core_cache');
+
+echo $renderer->header();
+echo $renderer->heading($title);
+
+if (!is_null($notification)) {
+    echo $renderer->notification($notification, ($notifysuccess)?'notifysuccess' : 'notifyproblem');
+}
+
+if ($mform instanceof moodleform) {
+    $mform->display();
+} else {
+    echo $renderer->store_plugin_summaries($plugins);
+    echo $renderer->store_instance_summariers($stores, $plugins);
+    echo $renderer->definition_summaries($definitions, cache_administration_helper::get_definition_actions($context));
+    echo $renderer->lock_summaries($locks);
+
+    $applicationstore = join(', ', $defaultmodestores[cache_store::MODE_APPLICATION]);
+    $sessionstore = join(', ', $defaultmodestores[cache_store::MODE_SESSION]);
+    $requeststore = join(', ', $defaultmodestores[cache_store::MODE_REQUEST]);
+    $editurl = new moodle_url('/cache/admin.php', array('action' => 'editmodemappings', 'sesskey' => sesskey()));
+    echo $renderer->mode_mappings($applicationstore, $sessionstore, $requeststore, $editurl);
+}
+
+echo $renderer->footer();
diff --git a/cache/classes/config.php b/cache/classes/config.php
new file mode 100644 (file)
index 0000000..0917a83
--- /dev/null
@@ -0,0 +1,472 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Cache configuration reader
+ *
+ * This file is part of Moodle's cache API, affectionately called MUC.
+ * It contains the components that are requried in order to use caching.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cache configuration reader.
+ *
+ * This class is used to interact with the cache's configuration.
+ * The configuration is stored in the Moodle data directory.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cache_config {
+
+    /**
+     * The configured stores
+     * @var array
+     */
+    protected $configstores = array();
+
+    /**
+     * The configured mode mappings
+     * @var array
+     */
+    protected $configmodemappings = array();
+
+    /**
+     * The configured definitions as picked up from cache.php files
+     * @var array
+     */
+    protected $configdefinitions = array();
+
+    /**
+     * The definition mappings that have been configured.
+     * @var array
+     */
+    protected $configdefinitionmappings = array();
+
+    /**
+     * An array of configured cache lock instances.
+     * @var array
+     */
+    protected $configlocks = array();
+
+    /**
+     * Please use cache_config::instance to get an instance of the cache config that is ready to be used.
+     */
+    public function __construct() {
+        // Nothing to do here but look pretty.
+    }
+
+    /**
+     * Gets an instance of the cache_configuration class.
+     *
+     * @return cache_config
+     */
+    public static function instance() {
+        $factory = cache_factory::instance();
+        return $factory->create_config_instance();
+    }
+
+    /**
+     * Checks if the configuration file exists.
+     *
+     * @return bool True if it exists
+     */
+    public static function config_file_exists() {
+        // Allow for late static binding.
+        return file_exists(self::get_config_file_path());
+    }
+
+    /**
+     * Returns the expected path to the configuration file.
+     *
+     * @return string The absolute path
+     */
+    protected static function get_config_file_path() {
+        global $CFG;
+        if (!empty($CFG->altcacheconfigpath)) {
+            $path = $CFG->altcacheconfigpath;
+            if (is_dir($path) && is_writable($path)) {
+                // Its a writable directory, thats fine.
+                return $path.'/cacheconfig.php';
+            } else if (is_writable(dirname($path)) && (!file_exists($path) || is_writable($path))) {
+                // Its a file, either it doesn't exist and the directory is writable or the file exists and is writable.
+                return $path;
+            }
+        }
+        // Return the default location within dataroot.
+        return $CFG->dataroot.'/muc/config.php';
+    }
+
+    /**
+     * Loads the configuration file and parses its contents into the expected structure.
+     *
+     * @return boolean
+     */
+    public function load() {
+        global $CFG;
+
+        $configuration = $this->include_configuration();
+
+        $this->configstores = array();
+        $this->configdefinitions = array();
+        $this->configlocks = array();
+        $this->configmodemappings = array();
+        $this->configdefinitionmappings = array();
+        $this->configlockmappings = array();
+
+        // Filter the lock instances.
+        $defaultlock = null;
+        foreach ($configuration['locks'] as $conf) {
+            if (!is_array($conf)) {
+                // Something is very wrong here.
+                continue;
+            }
+            if (!array_key_exists('name', $conf)) {
+                // Not a valid definition configuration.
+                continue;
+            }
+            $name = $conf['name'];
+            if (array_key_exists($name, $this->configlocks)) {
+                debugging('Duplicate cache lock detected. This should never happen.', DEBUG_DEVELOPER);
+                continue;
+            }
+            $conf['default'] = (!empty($conf['default']));
+            if ($defaultlock === null || $conf['default']) {
+                $defaultlock = $name;
+            }
+            $this->configlocks[$name] = $conf;
+        }
+
+        // Filter the stores.
+        $availableplugins = cache_helper::early_get_cache_plugins();
+        foreach ($configuration['stores'] as $store) {
+            if (!is_array($store) || !array_key_exists('name', $store) || !array_key_exists('plugin', $store)) {
+                // Not a valid instance configuration.
+                debugging('Invalid cache store in config. Missing name or plugin.', DEBUG_DEVELOPER);
+                continue;
+            }
+            $plugin = $store['plugin'];
+            $class = 'cachestore_'.$plugin;
+            $exists = array_key_exists($plugin, $availableplugins);
+            if (!$exists && (!class_exists($class) || !is_subclass_of($class, 'cache_store'))) {
+                // Not a valid plugin, or has been uninstalled, just skip it an carry on.
+                debugging('Invalid cache store in config. Not an available plugin.', DEBUG_DEVELOPER);
+                continue;
+            }
+            $file = $CFG->dirroot.'/cache/stores/'.$plugin.'/lib.php';
+            if (!class_exists($class) && file_exists($file)) {
+                require_once($file);
+            }
+            if (!class_exists($class)) {
+                continue;
+            }
+            if (!array_key_exists('cache_store', class_implements($class))) {
+                continue;
+            }
+            if (!array_key_exists('configuration', $store) || !is_array($store['configuration'])) {
+                $store['configuration'] = array();
+            }
+            $store['class'] = $class;
+            $store['default'] = !empty($store['default']);
+            if (!array_key_exists('lock', $store) || !array_key_exists($store['lock'], $this->configlocks)) {
+                $store['lock'] = $defaultlock;
+            }
+
+            $this->configstores[$store['name']] = $store;
+        }
+
+        // Filter the definitions.
+        foreach ($configuration['definitions'] as $id => $conf) {
+            if (!is_array($conf)) {
+                // Something is very wrong here.
+                continue;
+            }
+            if (!array_key_exists('mode', $conf) || !array_key_exists('component', $conf) || !array_key_exists('area', $conf)) {
+                // Not a valid definition configuration.
+                continue;
+            }
+            if (array_key_exists($id, $this->configdefinitions)) {
+                debugging('Duplicate cache definition detected. This should never happen.', DEBUG_DEVELOPER);
+                continue;
+            }
+            $conf['mode'] = (int)$conf['mode'];
+            if ($conf['mode'] < cache_store::MODE_APPLICATION || $conf['mode'] > cache_store::MODE_REQUEST) {
+                // Invalid cache mode used for the definition.
+                continue;
+            }
+            $this->configdefinitions[$id] = $conf;
+        }
+
+        // Filter the mode mappings.
+        foreach ($configuration['modemappings'] as $mapping) {
+            if (!is_array($mapping) || !array_key_exists('mode', $mapping) || !array_key_exists('store', $mapping)) {
+                // Not a valid mapping configuration.
+                debugging('A cache mode mapping entry is invalid.', DEBUG_DEVELOPER);
+                continue;
+            }
+            if (!array_key_exists($mapping['store'], $this->configstores)) {
+                // Mapped array instance doesn't exist.
+                debugging('A cache mode mapping exists for a mode or store that does not exist.', DEBUG_DEVELOPER);
+                continue;
+            }
+            $mapping['mode'] = (int)$mapping['mode'];
+            if ($mapping['mode'] < 0 || $mapping['mode'] > 4) {
+                // Invalid cache type used for the mapping.
+                continue;
+            }
+            if (!array_key_exists('sort', $mapping)) {
+                $mapping['sort'] = 0;
+            }
+            $this->configmodemappings[] = $mapping;
+        }
+
+        // Filter the definition mappings.
+        foreach ($configuration['definitionmappings'] as $mapping) {
+            if (!is_array($mapping) || !array_key_exists('definition', $mapping) || !array_key_exists('store', $mapping)) {
+                // Not a valid mapping configuration.
+                continue;
+            }
+            if (!array_key_exists($mapping['store'], $this->configstores)) {
+                // Mapped array instance doesn't exist.
+                continue;
+            }
+            if (!array_key_exists($mapping['definition'], $this->configdefinitions)) {
+                // Mapped array instance doesn't exist.
+                continue;
+            }
+            if (!array_key_exists('sort', $mapping)) {
+                $mapping['sort'] = 0;
+            }
+            $this->configdefinitionmappings[] = $mapping;
+        }
+
+        usort($this->configmodemappings, array($this, 'sort_mappings'));
+        usort($this->configdefinitionmappings, array($this, 'sort_mappings'));
+
+        return true;
+    }
+
+    /**
+     * Includes the configuration file and makes sure it contains the expected bits.
+     *
+     * You need to ensure that the config file exists before this is called.
+     *
+     * @return array
+     * @throws cache_exception
+     */
+    protected function include_configuration() {
+        $configuration = array();
+        $cachefile = self::get_config_file_path();
+
+        if (!file_exists($cachefile)) {
+            throw new cache_exception('Default cache config could not be found. It should have already been created by now.');
+        }
+        include($cachefile);
+        if (!is_array($configuration)) {
+            throw new cache_exception('Invalid cache configuration file');
+        }
+        if (!array_key_exists('stores', $configuration) || !is_array($configuration['stores'])) {
+            $configuration['stores'] = array();
+        }
+        if (!array_key_exists('modemappings', $configuration) || !is_array($configuration['modemappings'])) {
+            $configuration['modemappings'] = array();
+        }
+        if (!array_key_exists('definitions', $configuration) || !is_array($configuration['definitions'])) {
+            $configuration['definitions'] = array();
+        }
+        if (!array_key_exists('definitionmappings', $configuration) || !is_array($configuration['definitionmappings'])) {
+            $configuration['definitionmappings'] = array();
+        }
+        if (!array_key_exists('locks', $configuration) || !is_array($configuration['locks'])) {
+            $configuration['locks'] = array();
+        }
+
+        return $configuration;
+    }
+
+    /**
+     * Used to sort cache config arrays based upon a sort key.
+     *
+     * Highest number at the top.
+     *
+     * @param array $a
+     * @param array $b
+     * @return int
+     */
+    protected function sort_mappings(array $a, array $b) {
+        if ($a['sort'] == $b['sort']) {
+            return 0;
+        }
+        return ($a['sort'] < $b['sort']) ? 1 : -1;
+    }
+
+    /**
+     * Gets a definition from the config given its name.
+     *
+     * @param string $id
+     * @return bool
+     */
+    public function get_definition_by_id($id) {
+        if (array_key_exists($id, $this->configdefinitions)) {
+            return $this->configdefinitions[$id];
+        }
+        return false;
+    }
+
+    /**
+     * Returns all the known definitions.
+     *
+     * @return array
+     */
+    public function get_definitions() {
+        return $this->configdefinitions;
+    }
+
+    /**
+     * Returns all of the stores that are suitable for the given mode and requirements.
+     *
+     * @param int $mode One of cache_store::MODE_*
+     * @param int $requirements The requirements of the cache as a binary flag
+     * @return array An array of suitable stores.
+     */
+    public function get_stores($mode, $requirements = 0) {
+        $stores = array();
+        foreach ($this->configstores as $name => $store) {
+            // If the mode is supported and all of the requirements are provided features.
+            if (($store['modes'] & $mode) && ($store['features'] & $requirements) === $requirements) {
+                $stores[$name] = $store;
+            }
+        }
+        return $stores;
+    }
+
+    /**
+     * Gets all of the stores that are to be used for the given definition.
+     *
+     * @param cache_definition $definition
+     * @return array
+     */
+    public function get_stores_for_definition(cache_definition $definition) {
+        // Check if MUC has been disabled.
+        if (defined('NO_CACHE_STORES') && NO_CACHE_STORES !== false) {
+            // Yip its been disabled.
+            // To facilitate this we are going to always return an empty array of stores to use.
+            // This will force all cache instances to use the cachestore_dummy.
+            // MUC will still be used essentially so that code using it will still continue to function but because no cache stores
+            // are being used interaction with MUC will be purely based around a static var.
+            return array();
+        }
+
+        $availablestores = $this->get_stores($definition->get_mode(), $definition->get_requirements_bin());
+        $stores = array();
+        $id = $definition->get_id();
+
+        // Now get any mappings and give them priority.
+        foreach ($this->configdefinitionmappings as $mapping) {
+            if ($mapping['definition'] !== $id) {
+                continue;
+            }
+            $storename = $mapping['store'];
+            if (!array_key_exists($storename, $availablestores)) {
+                continue;
+            }
+            if (array_key_exists($storename, $stores)) {
+                $store = $stores[$storename];
+                unset($stores[$storename]);
+                $stores[$storename] = $store;
+            } else {
+                $stores[$storename] = $availablestores[$storename];
+            }
+        }
+
+        if (empty($stores) && !$definition->is_for_mappings_only()) {
+            $mode = $definition->get_mode();
+            // Load the default stores.
+            foreach ($this->configmodemappings as $mapping) {
+                if ($mapping['mode'] === $mode && array_key_exists($mapping['store'], $availablestores)) {
+                    $store = $availablestores[$mapping['store']];
+                    if (empty($store['mappingsonly'])) {
+                        $stores[$mapping['store']] = $store;
+                    }
+                }
+            }
+        }
+
+        return $stores;
+    }
+
+    /**
+     * Returns all of the configured stores
+     * @return array
+     */
+    public function get_all_stores() {
+        return $this->configstores;
+    }
+
+    /**
+     * Returns all of the configured mode mappings
+     * @return array
+     */
+    public function get_mode_mappings() {
+        return $this->configmodemappings;
+    }
+
+    /**
+     * Returns all of the known definition mappings.
+     * @return array
+     */
+    public function get_definition_mappings() {
+        return $this->configdefinitionmappings;
+    }
+
+    /**
+     * Returns an array of the configured locks.
+     * @return array Array of name => config
+     */
+    public function get_locks() {
+        return $this->configlocks;
+    }
+
+    /**
+     * Returns the lock store configuration to use with a given store.
+     * @param string $storename
+     * @return array
+     * @throws cache_exception
+     */
+    public function get_lock_for_store($storename) {
+        if (array_key_exists($storename, $this->configstores)) {
+            if (array_key_exists($this->configstores[$storename]['lock'], $this->configlocks)) {
+                $lock = $this->configstores[$storename]['lock'];
+                return $this->configlocks[$lock];
+            }
+        }
+        foreach ($this->configlocks as $lockconf) {
+            if (!empty($lockconf['default'])) {
+                return $lockconf;
+            }
+        }
+        throw new cache_exception('ex_nodefaultlock');
+    }
+}
\ No newline at end of file
diff --git a/cache/classes/definition.php b/cache/classes/definition.php
new file mode 100644 (file)
index 0000000..a4196aa
--- /dev/null
@@ -0,0 +1,715 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Cache definition class
+ *
+ * This file is part of Moodle's cache API, affectionately called MUC.
+ * It contains the components that are requried in order to use caching.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The cache definition class.
+ *
+ * Cache definitions need to be defined in db/caches.php files.
+ * They can be constructed with the following options.
+ *
+ * Required settings:
+ *     + mode
+ *          [int] Sets the mode for the definition. Must be one of cache_store::MODE_*
+ *
+ * Optional settings:
+ *     + requireidentifiers
+ *          [array] An array of identifiers that must be provided to the cache when it is created.
+ *     + requiredataguarantee
+ *          [bool] If set to true then only stores that can guarantee data will remain available once set will be used.
+ *     + requiremultipleidentifiers
+ *          [bool] If set to true then only stores that support multiple identifiers will be used.
+ *     + requirelockingread
+ *          [bool] If set to true then a lock will be gained before reading from the cache store. It is recommended not to use
+ *          this setting unless 100% absolutely positively required. Remember 99.9% of caches will NOT need this setting.
+ *          This setting will only be used for application caches presently.
+ *     + requirelockingwrite
+ *          [bool] If set to true then a lock will be gained before writing to the cache store. As above this is not recommended
+ *          unless truly needed. Please think about the order of your code and deal with race conditions there first.
+ *          This setting will only be used for application caches presently.
+ *     + maxsize
+ *          [int] If set this will be used as the maximum number of entries within the cache store for this definition.
+ *          Its important to note that cache stores don't actually have to acknowledge this setting or maintain it as a hard limit.
+ *     + overrideclass
+ *          [string] A class to use as the loader for this cache. This is an advanced setting and will allow the developer of the
+ *          definition to take 100% control of the caching solution.
+ *          Any class used here must inherit the cache_loader interface and must extend default cache loader for the mode they are
+ *          using.
+ *     + overrideclassfile
+ *          [string] Suplements the above setting indicated the file containing the class to be used. This file is included when
+ *          required.
+ *     + datasource
+ *          [string] A class to use as the data loader for this definition.
+ *          Any class used here must inherit the cache_data_loader interface.
+ *     + datasourcefile
+ *          [string] Suplements the above setting indicated the file containing the class to be used. This file is included when
+ *          required.
+ *     + persistent
+ *          [bool] This setting does two important things. First it tells the cache API to only instantiate the cache structure for
+ *          this definition once, further requests will be given the original instance.
+ *          Second the cache loader will keep an array of the items set and retrieved to the cache during the request.
+ *          This has several advantages including better performance without needing to start passing the cache instance between
+ *          function calls, the downside is that the cache instance + the items used stay within memory.
+ *          Consider using this setting when you know that there are going to be many calls to the cache for the same information
+ *          or when you are converting existing code to the cache and need to access the cache within functions but don't want
+ *          to add it as an argument to the function.
+ *     + persistentmaxsize
+ *          [int] This supplements the above setting by limiting the number of items in the caches persistent array of items.
+ *          Tweaking this setting lower will allow you to minimise the memory implications above while hopefully still managing to
+ *          offset calls to the cache store.
+ *     + ttl
+ *          [int] A time to live for the data (in seconds). It is strongly recommended that you don't make use of this and
+ *          instead try to create an event driven invalidation system.
+ *          Not all cache stores will support this natively and there are undesired performance impacts if the cache store does not.
+ *     + mappingsonly
+ *          [bool] If set to true only the mapped cache store(s) will be used and the default mode store will not. This is a super
+ *          advanced setting and should not be used unless absolutely required. It allows you to avoid the default stores for one
+ *          reason or another.
+ *     + invalidationevents
+ *          [array] An array of events that should cause this cache to invalidate some or all of the items within it.
+ *
+ * For examples take a look at lib/db/caches.php
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cache_definition {
+
+    /**
+     * The identifier for the definition
+     * @var string
+     */
+    protected $id;
+
+    /**
+     * The mode for the defintion. One of cache_store::MODE_*
+     * @var int
+     */
+    protected $mode;
+
+    /**
+     * The component this definition is associated with.
+     * @var string
+     */
+    protected $component;
+
+    /**
+     * The area this definition is associated with.
+     * @var string
+     */
+    protected $area;
+
+    /**
+     * An array of identifiers that must be provided when the definition is used to create a cache.
+     * @var array
+     */
+    protected $requireidentifiers = array();
+
+    /**
+     * If set to true then only stores that guarantee data may be used with this definition.
+     * @var bool
+     */
+    protected $requiredataguarantee = false;
+
+    /**
+     * If set to true then only stores that support multple identifiers may be used with this definition.
+     * @var bool
+     */
+    protected $requiremultipleidentifiers = false;
+
+    /**
+     * If set to true then we know that this definition requires the locking functionality.
+     * This gets set during construction based upon the settings requirelockingread and requirelockingwrite.
+     * @var bool
+     */
+    protected $requirelocking = false;
+
+    /**
+     * Set to true if this definition requires read locking.
+     * @var bool
+     */
+    protected $requirelockingread = false;
+
+    /**
+     * Gets set to true if this definition requires write locking.
+     * @var bool
+     */
+    protected $requirelockingwrite = false;
+
+    /**
+     * Sets the maximum number of items that can exist in the cache.
+     * Please note this isn't a hard limit, and doesn't need to be enforced by the caches. They can choose to do so optionally.
+     * @var int
+     */
+    protected $maxsize = null;
+
+    /**
+     * The class to use as the cache loader for this definition.
+     * @var string
+     */
+    protected $overrideclass = null;
+
+    /**
+     * The file in which the override class exists. This will be included if required.
+     * @var string Absolute path
+     */
+    protected $overrideclassfile = null;
+
+    /**
+     * The data source class to use with this definition.
+     * @var string
+     */
+    protected $datasource = null;
+
+    /**
+     * The file in which the data source class exists. This will be included if required.
+     * @var string
+     */
+    protected $datasourcefile = null;
+
+    /**
+     * The data source class aggregate to use. This is a super advanced setting.
+     * @var string
+     */
+    protected $datasourceaggregate = null;
+
+    /**
+     * Set to true if the definitions cache should be persistent
+     * @var bool
+     */
+    protected $persistent = false;
+
+    /**
+     * The persistent item array max size.
+     * @var int
+     */
+    protected $persistentmaxsize = false;
+
+    /**
+     * The TTL for data in this cache. Please don't use this, instead use event driven invalidation.
+     * @var int
+     */
+    protected $ttl = 0;
+
+    /**
+     * Set to true if this cache should only use mapped cache stores and not the default mode cache store.
+     * @var bool
+     */
+    protected $mappingsonly = false;
+
+    /**
+     * An array of events that should cause this cache to invalidate.
+     * @var array
+     */
+    protected $invalidationevents = array();
+
+    /**
+     * An array of identifiers provided to this cache when it was initialised.
+     * @var array
+     */
+    protected $identifiers = array();
+
+    /**
+     * Key prefix for use with single key cache stores
+     * @var string
+     */
+    protected $keyprefixsingle = null;
+
+    /**
+     * Key prefix to use with cache stores that support multi keys.
+     * @var array
+     */
+    protected $keyprefixmulti = null;
+
+    /**
+     * A hash identifier of this definition.
+     * @var string
+     */
+    protected $definitionhash = null;
+
+    /**
+     * Creates a cache definition given a definition from the cache configuration or from a caches.php file.
+     *
+     * @param string $id
+     * @param array $definition
+     * @param string $datasourceaggregate
+     * @return cache_definition
+     * @throws coding_exception
+     */
+    public static function load($id, array $definition, $datasourceaggregate = null) {
+        if (!array_key_exists('mode', $definition)) {
+            throw new coding_exception('You must provide a mode when creating a cache definition');
+        }
+        if (!array_key_exists('component', $definition)) {
+            throw new coding_exception('You must provide a mode when creating a cache definition');
+        }
+        if (!array_key_exists('area', $definition)) {
+            throw new coding_exception('You must provide a mode when creating a cache definition');
+        }
+        $mode = (int)$definition['mode'];
+        $component = (string)$definition['component'];
+        $area = (string)$definition['area'];
+
+        // Set the defaults.
+        $requireidentifiers = array();
+        $requiredataguarantee = false;
+        $requiremultipleidentifiers = false;
+        $requirelockingread = false;
+        $requirelockingwrite = false;
+        $maxsize = null;
+        $overrideclass = null;
+        $overrideclassfile = null;
+        $datasource = null;
+        $datasourcefile = null;
+        $persistent = false;
+        $persistentmaxsize = false;
+        $ttl = 0;
+        $mappingsonly = false;
+        $invalidationevents = array();
+
+        if (array_key_exists('requireidentifiers', $definition)) {
+            $requireidentifiers = (array)$definition['requireidentifiers'];
+        }
+        if (array_key_exists('requiredataguarantee', $definition)) {
+            $requiredataguarantee = (bool)$definition['requiredataguarantee'];
+        }
+        if (array_key_exists('requiremultipleidentifiers', $definition)) {
+            $requiremultipleidentifiers = (bool)$definition['requiremultipleidentifiers'];
+        }
+
+        if (array_key_exists('requirelockingread', $definition)) {
+            $requirelockingread = (bool)$definition['requirelockingread'];
+        }
+        if (array_key_exists('requirelockingwrite', $definition)) {
+            $requirelockingwrite = (bool)$definition['requirelockingwrite'];
+        }
+        $requirelocking = $requirelockingwrite || $requirelockingread;
+
+        if (array_key_exists('maxsize', $definition)) {
+            $maxsize = (int)$definition['maxsize'];
+        }
+
+        if (array_key_exists('overrideclass', $definition)) {
+            $overrideclass = $definition['overrideclass'];
+        }
+        if (array_key_exists('overrideclassfile', $definition)) {
+            $overrideclassfile = $definition['overrideclassfile'];
+        }
+
+        if (array_key_exists('datasource', $definition)) {
+            $datasource = $definition['datasource'];
+        }
+        if (array_key_exists('datasourcefile', $definition)) {
+            $datasourcefile = $definition['datasourcefile'];
+        }
+
+        if (array_key_exists('persistent', $definition)) {
+            $persistent = (bool)$definition['persistent'];
+        }
+        if (array_key_exists('persistentmaxsize', $definition)) {
+            $persistentmaxsize = (int)$definition['persistentmaxsize'];
+        }
+        if (array_key_exists('ttl', $definition)) {
+            $ttl = (int)$definition['ttl'];
+        }
+        if (array_key_exists('mappingsonly', $definition)) {
+            $mappingsonly = (bool)$definition['mappingsonly'];
+        }
+        if (array_key_exists('invalidationevents', $definition)) {
+            $invalidationevents = (array)$definition['invalidationevents'];
+        }
+
+        if (!is_null($overrideclass)) {
+            if (!is_null($overrideclassfile)) {
+                if (!file_exists($overrideclassfile)) {
+                    throw new coding_exception('The override class file does not exist.');
+                }
+                require_once($overrideclassfile);
+            }
+            if (!class_exists($overrideclass)) {
+                throw new coding_exception('The override class does not exist.');
+            }
+
+            // Make sure that the provided class extends the default class for the mode.
+            if (get_parent_class($overrideclass) !== cache_helper::get_class_for_mode($mode)) {
+                throw new coding_exception('The override class does not immediately extend the relevant cache class.');
+            }
+        }
+
+        if (!is_null($datasource)) {
+            if (!is_null($datasourcefile)) {
+                if (!file_exists($datasourcefile)) {
+                    throw new coding_exception('The override class file does not exist.');
+                }
+                require_once($datasourcefile);
+            }
+            if (!class_exists($datasource)) {
+                throw new coding_exception('The override class does not exist.');
+            }
+            if (!array_key_exists('cache_data_source', class_implements($datasource))) {
+                throw new coding_exception('Cache data source classes must implement the cache_data_source interface');
+            }
+        }
+
+        $cachedefinition = new cache_definition();
+        $cachedefinition->id = $id;
+        $cachedefinition->mode = $mode;
+        $cachedefinition->component = $component;
+        $cachedefinition->area = $area;
+        $cachedefinition->requireidentifiers = $requireidentifiers;
+        $cachedefinition->requiredataguarantee = $requiredataguarantee;
+        $cachedefinition->requiremultipleidentifiers = $requiremultipleidentifiers;
+        $cachedefinition->requirelocking = $requirelocking;
+        $cachedefinition->requirelockingread = $requirelockingread;
+        $cachedefinition->requirelockingwrite = $requirelockingwrite;
+        $cachedefinition->maxsize = $maxsize;
+        $cachedefinition->overrideclass = $overrideclass;
+        $cachedefinition->overrideclassfile = $overrideclassfile;
+        $cachedefinition->datasource = $datasource;
+        $cachedefinition->datasourcefile = $datasourcefile;
+        $cachedefinition->datasourceaggregate = $datasourceaggregate;
+        $cachedefinition->persistent = $persistent;
+        $cachedefinition->persistentmaxsize = $persistentmaxsize;
+        $cachedefinition->ttl = $ttl;
+        $cachedefinition->mappingsonly = $mappingsonly;
+        $cachedefinition->invalidationevents = $invalidationevents;
+
+        return $cachedefinition;
+    }
+
+    /**
+     * Creates an ah-hoc cache definition given the required params.
+     *
+     * Please note that when using an adhoc definition you cannot set any of the optional params.
+     * This is because we cannot guarantee consistent access and we don't want to mislead people into thinking that.
+     *
+     * @param int $mode One of cache_store::MODE_*
+     * @param string $component The component this definition relates to.
+     * @param string $area The area this definition relates to.
+     * @param string $overrideclass The class to use as the loader.
+     * @param bool $persistent If this cache should be persistent.
+     * @return cache_application|cache_session|cache_request
+     */
+    public static function load_adhoc($mode, $component, $area, $overrideclass = null, $persistent = false) {
+        $id = 'adhoc/'.$component.'_'.$area;
+        $definition = array(
+            'mode' => $mode,
+            'component' => $component,
+            'area' => $area,
+            'persistent' => $persistent
+        );
+        if (!is_null($overrideclass)) {
+            $definition['overrideclass'] = $overrideclass;
+        }
+        return self::load($id, $definition, null);
+    }
+
+    /**
+     * Returns the cache loader class that should be used for this definition.
+     * @return string
+     */
+    public function get_cache_class() {
+        if (!is_null($this->overrideclass)) {
+            return $this->overrideclass;
+        }
+        return cache_helper::get_class_for_mode($this->mode);
+    }
+
+    /**
+     * Returns the id of this definition.
+     * @return string
+     */
+    public function get_id() {
+        return $this->id;
+    }
+
+    /**
+     * Returns the name for this definition
+     * @return string
+     */
+    public function get_name() {
+        $identifier = 'cachedef_'.clean_param($this->area, PARAM_STRINGID);
+        $component = $this->component;
+        if ($component === 'core') {
+            $component = 'cache';
+        }
+        return new lang_string($identifier, $component);
+    }
+
+    /**
+     * Returns the mode of this definition
+     * @return int One more cache_store::MODE_
+     */
+    public function get_mode() {
+        return $this->mode;
+    }
+
+    /**
+     * Returns the area this definition is associated with.
+     * @return string
+     */
+    public function get_area() {
+        return $this->area;
+    }
+
+    /**
+     * Returns the component this definition is associated with.
+     * @return string
+     */
+    public function get_component() {
+        return $this->component;
+    }
+
+    /**
+     * Returns the identifiers that are being used for this definition.
+     * @return array
+     */
+    public function get_identifiers() {
+        return $this->identifiers;
+    }
+
+    /**
+     * Returns the ttl in seconds for this definition if there is one, or null if not.
+     * @return int|null
+     */
+    public function get_ttl() {
+        return $this->ttl;
+    }
+
+    /**
+     * Returns the maximum number of items allowed in this cache.
+     * @return int
+     */
+    public function get_maxsize() {
+        return $this->maxsize;
+    }
+
+    /**
+     * Returns true if this definition should only be used with mappings.
+     * @return bool
+     */
+    public function is_for_mappings_only() {
+        return $this->mappingsonly;
+    }
+
+    /**
+     * Returns true if this definition requires a data guarantee from the cache stores being used.
+     * @return bool
+     */
+    public function require_data_guarantee() {
+        return $this->requiredataguarantee;
+    }
+
+    /**
+     * Returns true if this definition requires that the cache stores support multiple identifiers
+     * @return bool
+     */
+    public function require_multiple_identifiers() {
+        return $this->requiremultipleidentifiers;
+    }
+
+    /**
+     * Returns true if this definition requires locking functionality. Either read or write locking.
+     * @return bool
+     */
+    public function require_locking() {
+        return $this->requirelocking;
+    }
+
+    /**
+     * Returns true if this definition requires read locking.
+     * @return bool
+     */
+    public function require_locking_read() {
+        return $this->requirelockingread;
+    }
+
+    /**
+     * Returns true if this definition requires write locking.
+     * @return bool
+     */
+    public function require_locking_write() {
+        return $this->requirelockingwrite;
+    }
+
+    /**
+     * Returns true if this definition has an associated data source.
+     * @return bool
+     */
+    public function has_data_source() {
+        return !is_null($this->datasource);
+    }
+
+    /**
+     * Returns an instance of the data source class used for this definition.
+     *
+     * @return cache_data_source
+     * @throws coding_exception
+     */
+    public function get_data_source() {
+        if (!$this->has_data_source()) {
+            throw new coding_exception('This cache does not use a datasource.');
+        }
+        return forward_static_call(array($this->datasource, 'get_instance_for_cache'), $this);
+    }
+
+    /**
+     * Sets the identifiers for this definition, or updates them if they have already been set.
+     *
+     * @param array $identifiers
+     * @throws coding_exception
+     */
+    public function set_identifiers(array $identifiers = array()) {
+        foreach ($this->requireidentifiers as $identifier) {
+            if (!array_key_exists($identifier, $identifiers)) {
+                throw new coding_exception('Identifier required for cache has not been provided: '.$identifier);
+            }
+        }
+        foreach ($identifiers as $name => $value) {
+            $this->identifiers[$name] = (string)$value;
+        }
+        // Reset the key prefix's they need updating now.
+        $this->keyprefixsingle = null;
+        $this->keyprefixmulti = null;
+    }
+
+    /**
+     * Returns the requirements of this definition as a binary flag.
+     * @return int
+     */
+    public function get_requirements_bin() {
+        $requires = 0;
+        if ($this->require_data_guarantee()) {
+            $requires += cache_store::SUPPORTS_DATA_GUARANTEE;
+        }
+        if ($this->require_multiple_identifiers()) {
+            $requires += cache_store::SUPPORTS_MULTIPLE_IDENTIFIERS;
+        }
+        return $requires;
+    }
+
+    /**
+     * Returns true if this definitions cache should be made persistent.
+     * @return bool
+     */
+    public function should_be_persistent() {
+        return $this->persistent;
+    }
+
+    /**
+     * Returns the max size for the persistent item array in the cache.
+     * @return int
+     */
+    public function get_persistent_max_size() {
+        return $this->persistentmaxsize;
+    }
+
+    /**
+     * Generates a hash of this definition and returns it.
+     * @return string
+     */
+    public function generate_definition_hash() {
+        if ($this->definitionhash === null) {
+            $this->definitionhash = md5("{$this->mode} {$this->component} {$this->area}");
+        }
+        return $this->definitionhash;
+    }
+
+    /**
+     * Generates a single key prefix for this definition
+     *
+     * @return string
+     */
+    public function generate_single_key_prefix() {
+        if ($this->keyprefixsingle === null) {
+            $this->keyprefixsingle = $this->mode.'/'.$this->mode;
+            $identifiers = $this->get_identifiers();
+            if ($identifiers) {
+                foreach ($identifiers as $key => $value) {
+                    $this->keyprefixsingle .= '/'.$key.'='.$value;
+                }
+            }
+            $this->keyprefixsingle = md5($this->keyprefixsingle);
+        }
+        return $this->keyprefixsingle;
+    }
+
+    /**
+     * Generates a multi key prefix for this definition
+     *
+     * @return array
+     */
+    public function generate_multi_key_parts() {
+        if ($this->keyprefixmulti === null) {
+            $this->keyprefixmulti = array(
+                'mode' => $this->mode,
+                'component' => $this->component,
+                'area' => $this->area,
+            );
+            if (!empty($this->identifiers)) {
+                $identifiers = array();
+                foreach ($this->identifiers as $key => $value) {
+                    $identifiers[] = htmlentities($key).'='.htmlentities($value);
+                }
+                $this->keyprefixmulti['identifiers'] = join('&', $identifiers);
+            }
+        }
+        return $this->keyprefixmulti;
+    }
+
+    /**
+     * Check if this definition should invalidate on the given event.
+     *
+     * @param string $event
+     * @return bool True if the definition should invalidate on the event. False otherwise.
+     */
+    public function invalidates_on_event($event) {
+        return (in_array($event, $this->invalidationevents));
+    }
+
+    /**
+     * Check if the definition has any invalidation events.
+     *
+     * @return bool True if it does, false otherwise
+     */
+    public function has_invalidation_events() {
+        return !empty($this->invalidationevents);
+    }
+
+    /**
+     * Returns all of the invalidation events for this definition.
+     *
+     * @return array
+     */
+    public function get_invalidation_events() {
+        return $this->invalidationevents;
+    }
+}
\ No newline at end of file
diff --git a/cache/classes/dummystore.php b/cache/classes/dummystore.php
new file mode 100644 (file)
index 0000000..870d960
--- /dev/null
@@ -0,0 +1,277 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Cache dummy store.
+ *
+ * This dummy store is used when a load has no other stores that it can make use of.
+ * This shouldn't happen in normal operation... I think.
+ *
+ * This file is part of Moodle's cache API, affectionately called MUC.
+ * It contains the components that are requried in order to use caching.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The cache dummy store.
+ *
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cachestore_dummy implements cache_store {
+
+    /**
+     * The name of this store.
+     * @var string
+     */
+    protected $name;
+
+    /**
+     * Gets set to true if this store is going to persist data.
+     * This happens when the definition doesn't require it as the loader will not be persisting information and something has to.
+     * @var bool
+     */
+    protected $persist = false;
+
+    /**
+     * The persistent store array
+     * @var array
+     */
+    protected $store = array();
+
+    /**
+     * Constructs a dummy store instance.
+     * @param string $name
+     * @param array $configuration
+     */
+    public function __construct($name = 'Dummy store', array $configuration = array()) {
+        $this->name = $name;
+    }
+
+    /**
+     * Returns true if this store plugin is usable.
+     * @return bool
+     */
+    public static function are_requirements_met() {
+        return true;
+    }
+
+    /**
+     * Returns true if the user can add an instance.
+     * @return bool
+     */
+    public static function can_add_instance() {
+        return false;
+    }
+
+    /**
+     * Returns the supported features.
+     * @param array $configuration
+     * @return int
+     */
+    public static function get_supported_features(array $configuration = array()) {
+        return self::SUPPORTS_NATIVE_TTL;
+    }
+
+    /**
+     * Returns the supported mode.
+     * @param array $configuration
+     * @return int
+     */
+    public static function get_supported_modes(array $configuration = array()) {
+        return self::MODE_APPLICATION + self::MODE_REQUEST + self::MODE_SESSION;
+    }
+
+    /**
+     * Initialises the store instance for a definition.
+     * @param cache_definition $definition
+     */
+    public function initialise(cache_definition $definition) {
+        // If the definition isn't persistent then we need to be persistent here.
+        $this->persist = !$definition->should_be_persistent();
+    }
+
+    /**
+     * Returns true if this has been initialised.
+     * @return bool
+     */
+    public function is_initialised() {
+        return (!empty($this->definition));
+    }
+
+    /**
+     * Returns true if this is ready.
+     * @return bool
+     */
+    public function is_ready() {
+        return true;
+    }
+
+    /**
+     * Returns true the given mode is supported.
+     * @param int $mode
+     * @return bool
+     */
+    public static function is_supported_mode($mode) {
+        return true;
+    }
+
+    /**
+     * Returns true if this store supports data guarantee.
+     * @return bool
+     */
+    public function supports_data_guarantee() {
+        return false;
+    }
+
+    /**
+     * Returns true if this store supports multiple identifiers.
+     * @return bool
+     */
+    public function supports_multiple_indentifiers() {
+        return false;
+    }
+
+    /**
+     * Returns true if this store supports a native ttl.
+     * @return bool
+     */
+    public function supports_native_ttl() {
+        return true;
+    }
+
+    /**
+     * Returns the data for the given key
+     * @param string $key
+     * @return string|false
+     */
+    public function get($key) {
+        if ($this->persist && array_key_exists($key, $this->store)) {
+            return $this->store[$key];
+        }
+        return false;
+    }
+
+    /**
+     * Gets' the values for many keys
+     * @param array $keys
+     * @return bool
+     */
+    public function get_many($keys) {
+        $return = array();
+        foreach ($keys as $key) {
+            if ($this->persist && array_key_exists($key, $this->store)) {
+                $return[$key] = $this->store[$key];
+            } else {
+                $return[$key] = false;
+            }
+        }
+        return $return;
+    }
+
+    /**
+     * Sets an item in the cache
+     * @param string $key
+     * @param mixed $data
+     * @return bool
+     */
+    public function set($key, $data) {
+        if ($this->persist) {
+            $this->store[$key] = $data;
+        }
+        return true;
+    }
+
+    /**
+     * Sets many items in the cache
+     * @param array $keyvaluearray
+     * @return int
+     */
+    public function set_many(array $keyvaluearray) {
+        if ($this->persist) {
+            foreach ($keyvaluearray as $pair) {
+                $this->store[$pair['key']] = $pair['value'];
+            }
+            return count($keyvaluearray);
+        }
+        return 0;
+    }
+
+    /**
+     * Deletes an item from the cache
+     * @param string $key
+     * @return bool
+     */
+    public function delete($key) {
+        unset($this->store[$key]);
+        return true;
+    }
+    /**
+     * Deletes many items from the cache
+     * @param array $keys
+     * @return bool
+     */
+    public function delete_many(array $keys) {
+        if ($this->persist) {
+            foreach ($keys as $key) {
+                unset($this->store[$key]);
+            }
+        }
+        return count($keys);
+    }
+
+    /**
+     * Deletes all of the items from the cache.
+     * @return bool
+     */
+    public function purge() {
+        $this->store = array();
+        return true;
+    }
+
+    /**
+     * Performs any necessary clean up when the store instance is being deleted.
+     */
+    public function cleanup() {
+        $this->purge();
+    }
+
+    /**
+     * Generates an instance of the cache store that can be used for testing.
+     *
+     * @param cache_definition $definition
+     * @return false
+     */
+    public static function initialise_test_instance(cache_definition $definition) {
+        $cache = new cachestore_dummy('Dummy store test');
+        $cache->initialise($definition);
+        return $cache;;
+    }
+
+    /**
+     * Returns the name of this instance.
+     * @return string
+     */
+    public function my_name() {
+        return $this->name;
+    }
+}
\ No newline at end of file
diff --git a/cache/classes/factory.php b/cache/classes/factory.php
new file mode 100644 (file)
index 0000000..729a46b
--- /dev/null
@@ -0,0 +1,340 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the cache factory class.
+ *
+ * This file is part of Moodle's cache API, affectionately called MUC.
+ * It contains the components that are requried in order to use caching.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The cache factory class.
+ *
+ * This factory class is important because it stores instances of objects used by the cache API and returns them upon requests.
+ * This allows us to both reuse objects saving on overhead, and gives us an easy place to "reset" the cache API in situations that
+ * we need such as unit testing.
+ *
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cache_factory {
+
+    /**
+     * An instance of the cache_factory class created upon the first request.
+     * @var cache_factory
+     */
+    protected static $instance;
+
+    /**
+     * An array containing caches created for definitions
+     * @var array
+     */
+    protected $cachesfromdefinitions = array();
+
+    /**
+     * Array of caches created by parameters, ad-hoc definitions will have been used.
+     * @var array
+     */
+    protected $cachesfromparams = array();
+
+    /**
+     * An array of instantiated stores.
+     * @var array
+     */
+    protected $stores = array();
+
+    /**
+     * An array of configuration instances
+     * @var array
+     */
+    protected $configs = array();
+
+    /**
+     * An array of initialised definitions
+     * @var array
+     */
+    protected $definitions = array();
+
+    /**
+     * An array of lock plugins.
+     * @var array
+     */
+    protected $lockplugins = null;
+
+    /**
+     * Returns an instance of the cache_factor method.
+     *
+     * @param bool $forcereload If set to true a new cache_factory instance will be created and used.
+     * @return cache_factory
+     */
+    public static function instance($forcereload = false) {
+        if ($forcereload || self::$instance === null) {
+            self::$instance = new cache_factory();
+        }
+        return self::$instance;
+    }
+
+    /**
+     * Protected constructor, please use the static instance method.
+     */
+    protected function __construct() {
+        // Nothing to do here.
+    }
+
+    /**
+     * Resets the arrays containing instantiated caches, stores, and config instances.
+     */
+    public static function reset() {
+        $factory = self::instance();
+        $factory->cachesfromdefinitions = array();
+        $factory->cachesfromparams = array();
+        $factory->stores = array();
+        $factory->configs = array();
+        $factory->definitions = array();
+        $factory->lockplugins = null; // MUST be null in order to force its regeneration.
+    }
+
+    /**
+     * Creates a cache object given the parameters for a definition.
+     *
+     * If a cache has already been created for the given definition then that cache instance will be returned.
+     *
+     * @param string $component
+     * @param string $area
+     * @param array $identifiers
+     * @param string $aggregate
+     * @return cache_application|cache_session|cache_request
+     */
+    public function create_cache_from_definition($component, $area, array $identifiers = array(), $aggregate = null) {
+        $definitionname = $component.'/'.$area;
+        if (array_key_exists($definitionname, $this->cachesfromdefinitions)) {
+            $cache = $this->cachesfromdefinitions[$definitionname];
+            $cache->set_identifiers($identifiers);
+            return $cache;
+        }
+        $definition = $this->create_definition($component, $area, $aggregate);
+        $definition->set_identifiers($identifiers);
+        $cache = $this->create_cache($definition, $identifiers);
+        if ($definition->should_be_persistent()) {
+            $this->cachesfromdefinitions[$definitionname] = $cache;
+        }
+        return $cache;
+    }
+
+    /**
+     * Creates an ad-hoc cache from the given param.
+     *
+     * If a cache has already been created using the same params then that cache instance will be returned.
+     *
+     * @param int $mode
+     * @param string $component
+     * @param string $area
+     * @param array $identifiers
+     * @param bool $persistent
+     * @return cache_application|cache_session|cache_request
+     */
+    public function create_cache_from_params($mode, $component, $area, array $identifiers = array(), $persistent = false) {
+        $key = "{$mode}_{$component}_{$area}";
+        if (array_key_exists($key, $this->cachesfromparams)) {
+            return $this->cachesfromparams[$key];
+        }
+        // Get the class. Note this is a late static binding so we need to use get_called_class.
+        $definition = cache_definition::load_adhoc($mode, $component, $area, null, $persistent);
+        $definition->set_identifiers($identifiers);
+        $cache = $this->create_cache($definition, $identifiers);
+        if ($definition->should_be_persistent()) {
+            $cache->persist = true;
+            $cache->persistcache = array();
+            $this->cachesfromparams[$key] = $cache;
+        }
+        return $cache;
+    }
+
+    /**
+     * Common public method to create a cache instance given a definition.
+     *
+     * This is used by the static make methods.
+     *
+     * @param cache_definition $definition
+     * @return cache_application|cache_session|cache_store
+     * @throws coding_exception
+     */
+    public function create_cache(cache_definition $definition) {
+        $class = $definition->get_cache_class();
+        $stores = cache_helper::get_cache_stores($definition);
+        if (count($stores) === 0) {
+            // Hmm no stores, better provide a dummy store to mimick functionality. The dev will be none the wiser.
+            $stores[] = $this->create_dummy_store($definition);
+        }
+        $loader = null;
+        if ($definition->has_data_source()) {
+            $loader = $definition->get_data_source();
+        }
+        while (($store = array_pop($stores)) !== null) {
+            $loader = new $class($definition, $store, $loader);
+        }
+        return $loader;
+    }
+
+    /**
+     * Creates a store instance given its name and configuration.
+     *
+     * If the store has already been instantiated then the original objetc will be returned. (reused)
+     *
+     * @param string $name The name of the store (must be unique remember)
+     * @param array $details
+     * @param cache_definition $definition The definition to instantiate it for.
+     * @return boolean
+     */
+    public function create_store_from_config($name, array $details, cache_definition $definition) {
+        if (!array_key_exists($name, $this->stores)) {
+            // Properties: name, plugin, configuration, class.
+            $class = $details['class'];
+            $store = new $class($details['name'], $details['configuration']);
+            $this->stores[$name] = $store;
+        }
+        $store = $this->stores[$name];
+        if (!$store->is_ready() || !$store->is_supported_mode($definition->get_mode())) {
+            return false;
+        }
+        $store = clone($this->stores[$name]);
+        $store->initialise($definition);
+        return $store;
+    }
+
+    /**
+     * Creates a cache config instance with the ability to write if required.
+     *
+     * @param bool $writer If set to true an instance that can update the configuration will be returned.
+     * @return cache_config|cache_config_writer
+     */
+    public function create_config_instance($writer = false) {
+        global $CFG;
+
+        // Check if we need to create a config file with defaults.
+        $needtocreate = !cache_config::config_file_exists();
+
+        // The class to use.
+        $class = 'cache_config';
+        if ($writer || $needtocreate) {
+            require_once($CFG->dirroot.'/cache/locallib.php');
+            $class .= '_writer';
+        }
+
+        // Check if this is a PHPUnit test and redirect to the phpunit config classes if it is.
+        if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) {
+            require_once($CFG->dirroot.'/cache/locallib.php');
+            require_once($CFG->dirroot.'/cache/tests/fixtures/lib.php');
+            // We have just a single class for PHP unit tests. We don't care enough about its
+            // performance to do otherwise and having a single method allows us to inject things into it
+            // while testing.
+            $class = 'cache_config_phpunittest';
+        }
+
+        if ($needtocreate) {
+            // Create the default configuration.
+            $class::create_default_configuration();
+        }
+
+        if (!array_key_exists($class, $this->configs)) {
+            // Create a new instance and call it to load it.
+            $this->configs[$class] = new $class;
+            $this->configs[$class]->load();
+        }
+
+        // Return the instance.
+        return $this->configs[$class];
+    }
+
+    /**
+     * Creates a definition instance or returns the existing one if it has already been created.
+     * @param string $component
+     * @param string $area
+     * @param string $aggregate
+     * @return cache_definition
+     */
+    public function create_definition($component, $area, $aggregate = null) {
+        $id = $component.'/'.$area;
+        if ($aggregate) {
+            $id .= '::'.$aggregate;
+        }
+        if (!array_key_exists($id, $this->definitions)) {
+            $instance = $this->create_config_instance();
+            $definition = $instance->get_definition_by_id($id);
+            if (!$definition) {
+                $this->reset();
+                $instance = $this->create_config_instance(true);
+                $instance->update_definitions();
+                $definition = $instance->get_definition_by_id($id);
+                if (!$definition) {
+                    throw new coding_exception('The requested cache definition does not exist.'. $id, $id);
+                } else {
+                    debugging('Cache definitions reparsed causing cache reset in order to locate definition.
+                        You should bump the version number to ensure definitions are reprocessed.', DEBUG_DEVELOPER);
+                }
+            }
+            $this->definitions[$id] = cache_definition::load($id, $definition, $aggregate);
+        }
+        return $this->definitions[$id];
+    }
+
+    /**
+     * Creates a dummy store object for use when a loader has no potential stores to use.
+     *
+     * @param cache_definition $definition
+     * @return cachestore_dummy
+     */
+    protected function create_dummy_store(cache_definition $definition) {
+        global $CFG;
+        require_once($CFG->dirroot.'/cache/classes/dummystore.php');
+        $store = new cachestore_dummy();
+        $store->initialise($definition);
+        return $store;
+    }
+
+    /**
+     * Returns a lock instance ready for use.
+     *
+     * @param array $config
+     * @return cache_lock_interface
+     */
+    public function create_lock_instance(array $config) {
+        if (!array_key_exists('name', $config) || !array_key_exists('type', $config)) {
+            throw new coding_exception('Invalid cache lock instance provided');
+        }
+        $name = $config['name'];
+        $type = $config['type'];
+        unset($config['name']);
+        unset($config['type']);
+
+        if ($this->lockplugins === null) {
+            $this->lockplugins = get_plugin_list_with_class('cachelock', '', 'lib.php');
+        }
+        if (!array_key_exists($type, $this->lockplugins)) {
+            throw new coding_exception('Invalid cache lock type.');
+        }
+        $class = $this->lockplugins[$type];
+        return new $class($name, $config);
+    }
+}
\ No newline at end of file
diff --git a/cache/classes/helper.php b/cache/classes/helper.php
new file mode 100644 (file)
index 0000000..dfb3346
--- /dev/null
@@ -0,0 +1,456 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Cache helper class
+ *
+ * This file is part of Moodle's cache API, affectionately called MUC.
+ * It contains the components that are requried in order to use caching.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The cache helper class.
+ *
+ * The cache helper class provides common functionality to the cache API and is useful to developers within to interact with
+ * the cache API in a general way.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cache_helper {
+
+    /**
+     * Statistics gathered by the cache API during its operation will be used here.
+     * @static
+     * @var array
+     */
+    protected static $stats = array();
+
+    /**
+     * The instance of the cache helper.
+     * @var cache_helper
+     */
+    protected static $instance;
+
+    /**
+     * Returns true if the cache API can be initialised before Moodle has finished initialising itself.
+     *
+     * This check is essential when trying to cache the likes of configuration information. It checks to make sure that the cache
+     * configuration file has been created which allows use to set up caching when ever is required.
+     *
+     * @return bool
+     */
+    public static function ready_for_early_init() {
+        return cache_config::config_file_exists();
+    }
+
+    /**
+     * Returns an instance of the cache_helper.
+     *
+     * This is designed for internal use only and acts as a static store.
+     * @staticvar null $instance
+     * @return cache_helper
+     */
+    protected static function instance() {
+        if (is_null(self::$instance)) {
+            self::$instance = new cache_helper();
+        }
+        return self::$instance;
+    }
+
+    /**
+     * Constructs an instance of the cache_helper class. Again for internal use only.
+     */
+    protected function __construct() {
+        // Nothing to do here, just making sure you can't get an instance of this.
+    }
+
+    /**
+     * Used as a data store for initialised definitions.
+     * @var array
+     */
+    protected $definitions = array();
+
+    /**
+     * Used as a data store for initialised cache stores
+     * We use this because we want to avoid establishing multiple instances of a single store.
+     * @var array
+     */
+    protected $stores = array();
+
+    /**
+     * Returns the class for use as a cache loader for the given mode.
+     *
+     * @param int $mode One of cache_store::MODE_
+     * @return string
+     * @throws coding_exception
+     */
+    public static function get_class_for_mode($mode) {
+        switch ($mode) {
+            case cache_store::MODE_APPLICATION :
+                return 'cache_application';
+            case cache_store::MODE_REQUEST :
+                return 'cache_request';
+            case cache_store::MODE_SESSION :
+                return 'cache_session';
+        }
+        throw new coding_exception('Unknown cache mode passed. Must be one of cache_store::MODE_*');
+    }
+
+    /**
+     * Returns the cache stores to be used with the given definition.
+     * @param cache_definition $definition
+     * @return array
+     */
+    public static function get_cache_stores(cache_definition $definition) {
+        $instance = cache_config::instance();
+        $stores = $instance->get_stores_for_definition($definition);
+        $stores = self::initialise_cachestore_instances($stores, $definition);
+        return $stores;
+    }
+
+    /**
+     * Internal function for initialising an array of stores against a given cache definition.
+     *
+     * @param array $stores
+     * @param cache_definition $definition
+     * @return array
+     */
+    protected static function initialise_cachestore_instances(array $stores, cache_definition $definition) {
+        $return = array();
+        $factory = cache_factory::instance();
+        foreach ($stores as $name => $details) {
+            $store = $factory->create_store_from_config($name, $details, $definition);
+            if ($store !== false) {
+                $return[] = $store;
+            }
+        }
+        return $return;
+    }
+
+    /**
+     * Returns a cache_lock instance suitable for use with the store.
+     *
+     * @param cache_store $store
+     * @return cache_lock_interface
+     */
+    public static function get_cachelock_for_store(cache_store $store) {
+        $instance = cache_config::instance();
+        $lockconf = $instance->get_lock_for_store($store->my_name());
+        $factory = cache_factory::instance();
+        return $factory->create_lock_instance($lockconf);
+    }
+
+    /**
+     * Returns an array of plugins without using core methods.
+     *
+     * This function explicitly does NOT use core functions as it will in some circumstances be called before Moodle has
+     * finished initialising. This happens when loading configuration for instance.
+     *
+     * @return string
+     */
+    public static function early_get_cache_plugins() {
+        global $CFG;
+        $result = array();
+        $ignored = array('CVS', '_vti_cnf', 'simpletest', 'db', 'yui', 'tests');
+        $fulldir = $CFG->dirroot.'/cache/stores';
+        $items = new DirectoryIterator($fulldir);
+        foreach ($items as $item) {
+            if ($item->isDot() or !$item->isDir()) {
+                continue;
+            }
+            $pluginname = $item->getFilename();
+            if (in_array($pluginname, $ignored)) {
+                continue;
+            }
+            $pluginname = clean_param($pluginname, PARAM_PLUGIN);
+            if (empty($pluginname)) {
+                // Better ignore plugins with problematic names here.
+                continue;
+            }
+            $result[$pluginname] = $fulldir.'/'.$pluginname;
+            unset($item);
+        }
+        unset($items);
+        return $result;
+    }
+
+    /**
+     * Invalidates a given set of keys from a given definition.
+     *
+     * @todo Invalidating by definition should also add to the event cache so that sessions can be invalidated (when required).
+     *
+     * @param string $component
+     * @param string $area
+     * @param array $identifiers
+     * @param array $keys
+     * @return boolean
+     */
+    public static function invalidate_by_definition($component, $area, array $identifiers = array(), $keys = array()) {
+        $cache = cache::make($component, $area, $identifiers);
+        if (is_array($keys)) {
+            $cache->delete_many($keys);
+        } else if (is_scalar($keys)) {
+            $cache->delete($keys);
+        } else {
+            throw new coding_exception('cache_helper::invalidate_by_definition only accepts $keys as array, or scalar.');
+        }
+        return true;
+    }
+
+    /**
+     * Invalidates a given set of keys by means of an event.
+     *
+     * @todo add support for identifiers to be supplied and utilised.
+     *
+     * @param string $event
+     * @param array $keys
+     */
+    public static function invalidate_by_event($event, array $keys) {
+        $instance = cache_config::instance();
+        $invalidationeventset = false;
+        $factory = cache_factory::instance();
+        foreach ($instance->get_definitions() as $name => $definitionarr) {
+            $definition = cache_definition::load($name, $definitionarr);
+            if ($definition->invalidates_on_event($event)) {
+                // OK at this point we know that the definition has information to invalidate on the event.
+                // There are two routes, either its an application cache in which case we can invalidate it now.
+                // or it is a session cache in which case we need to set something to the "Event invalidation" definition.
+                // No need to deal with request caches, we don't want to change data half way through a request.
+                if ($definition->get_mode() === cache_store::MODE_APPLICATION) {
+                    $cache = $factory->create_cache($definition);
+                    $cache->delete_many($keys);
+                }
+
+                // We need to flag the event in the "Event invalidation" cache if it hasn't already happened.
+                if ($invalidationeventset === false) {
+                    // Get the event invalidation cache.
+                    $cache = cache::make('core', 'eventinvalidation');
+                    // Get any existing invalidated keys for this cache.
+                    $data = $cache->get($event);
+                    if ($data === false) {
+                        // There are none.
+                        $data = array();
+                    }
+                    // Add our keys to them with the current cache timestamp.
+                    foreach ($keys as $key) {
+                        $data[$key] = cache::now();
+                    }
+                    // Set that data back to the cache.
+                    $cache->set($event, $data);
+                    // This only needs to occur once.
+                    $invalidationeventset = true;
+                }
+            }
+        }
+    }
+
+    /**
+     * Purges the cache for a specific definition.
+     *
+     * @param string $component
+     * @param string $area
+     * @param array $identifiers
+     * @return bool
+     */
+    public static function purge_by_definition($component, $area, array $identifiers = array()) {
+        // Create the cache.
+        $cache = cache::make($component, $area, $identifiers);
+        // Purge baby, purge.
+        $cache->purge();
+        return true;
+    }
+
+    /**
+     * Purges a cache of all information on a given event.
+     *
+     * @param string $event
+     */
+    public static function purge_by_event($event) {
+        $instance = cache_config::instance();
+        $invalidationeventset = false;
+        $factory = cache_factory::instance();
+        foreach ($instance->get_definitions() as $name => $definitionarr) {
+            $definition = cache_definition::load($name, $definitionarr);
+            if ($definition->invalidates_on_event($event)) {
+                // Purge the cache.
+                $cache = $factory->create_cache($definition);
+                $cache->purge();
+
+                // We need to flag the event in the "Event invalidation" cache if it hasn't already happened.
+                if ($invalidationeventset === false) {
+                    // Get the event invalidation cache.
+                    $cache = cache::make('core', 'eventinvalidation');
+                    // Create a key to invalidate all.
+                    $data = array(
+                        'purged' => cache::now()
+                    );
+                    // Set that data back to the cache.
+                    $cache->set($event, $data);
+                    // This only needs to occur once.
+                    $invalidationeventset = true;
+                }
+            }
+        }
+    }
+
+    /**
+     * Ensure that the stats array is ready to collect information for the given store and definition.
+     * @param string $store
+     * @param string $definition
+     */
+    protected static function ensure_ready_for_stats($store, $definition) {
+        if (!array_key_exists($definition, self::$stats)) {
+            self::$stats[$definition] = array(
+                $store => array(
+                    'hits' => 0,
+                    'misses' => 0,
+                    'sets' => 0,
+                )
+            );
+        } else if (!array_key_exists($store, self::$stats[$definition])) {
+            self::$stats[$definition][$store] = array(
+                'hits' => 0,
+                'misses' => 0,
+                'sets' => 0,
+            );
+        }
+    }
+
+    /**
+     * Record a cache hit in the stats for the given store and definition.
+     *
+     * @param string $store
+     * @param string $definition
+     */
+    public static function record_cache_hit($store, $definition) {
+        self::ensure_ready_for_stats($store, $definition);
+        self::$stats[$definition][$store]['hits']++;
+    }
+
+    /**
+     * Record a cache miss in the stats for the given store and definition.
+     *
+     * @param string $store
+     * @param string $definition
+     */
+    public static function record_cache_miss($store, $definition) {
+        self::ensure_ready_for_stats($store, $definition);
+        self::$stats[$definition][$store]['misses']++;
+    }
+
+    /**
+     * Record a cache set in the stats for the given store and definition.
+     *
+     * @param string $store
+     * @param string $definition
+     */
+    public static function record_cache_set($store, $definition) {
+        self::ensure_ready_for_stats($store, $definition);
+        self::$stats[$definition][$store]['sets']++;
+    }
+
+    /**
+     * Return the stats collected so far.
+     * @return array
+     */
+    public static function get_stats() {
+        return self::$stats;
+    }
+
+    /**
+     * Purge all of the cache stores of all of their data.
+     *
+     * Think twice before calling this method. It will purge **ALL** caches regardless of whether they have been used recently or
+     * anything. This will involve full setup of the cache + the purge operation. On a site using caching heavily this WILL be
+     * painful.
+     */
+    public static function purge_all() {
+        $config = cache_config::instance();
+        $stores = $config->get_all_stores();
+        $definition = cache_definition::load_adhoc(cache_store::MODE_REQUEST, 'core', 'cache_purge');
+        foreach ($stores as $store) {
+            $class = $store['class'];
+            $instance = new $class($store['name'], $store['configuration']);
+            if (!$instance->is_ready()) {
+                continue;
+            }
+            $instance->initialise($definition);
+            $instance->purge();
+        }
+    }
+
+    /**
+     * Purges a store given its name.
+     *
+     * @param string $storename
+     * @return bool
+     */
+    public static function purge_store($storename) {
+        $config = cache_config::instance();
+        foreach ($config->get_all_stores() as $store) {
+            if ($store['name'] !== $storename) {
+                continue;
+            }
+            $class = $store['class'];
+            $instance = new $class($store['name'], $store['configuration']);
+            if (!$instance->is_ready()) {
+                continue;
+            }
+            $definition = cache_definition::load_adhoc(cache_store::MODE_REQUEST, 'core', 'cache_purge');
+            $instance->initialise($definition);
+            $instance->purge();
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Returns the translated name of the definition.
+     *
+     * @param cache_definition $definition
+     * @return lang_string
+     */
+    public static function get_definition_name($definition) {
+        if ($definition instanceof cache_definition) {
+            return $definition->get_name();
+        }
+        $identifier = 'cachedef_'.clean_param($definition['area'], PARAM_STRINGID);
+        $component = $definition['component'];
+        if ($component === 'core') {
+            $component = 'cache';
+        }
+        return new lang_string($identifier, $component);
+    }
+
+    /**
+     * Hashes a descriptive key to make it shorter and stil unique.
+     * @param string $key
+     * @return string
+     */
+    public static function hash_key($key) {
+        return crc32($key);
+    }
+}
\ No newline at end of file
diff --git a/cache/classes/interfaces.php b/cache/classes/interfaces.php
new file mode 100644 (file)
index 0000000..1f96fa0
--- /dev/null
@@ -0,0 +1,692 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Cache API interfaces
+ *
+ * This file is part of Moodle's cache API, affectionately called MUC.
+ * It contains the components that are requried in order to use caching.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cache Loader.
+ *
+ * This cache loader interface provides the required structure for classes that wish to be interacted with as a
+ * means of accessing and interacting with a cache.
+ *
+ * Can be implemented by any class wishing to be a cache loader.
+ */
+interface cache_loader {
+
+    /**
+     * Retrieves the value for the given key from the cache.
+     *
+     * @param string|int $key The key for the data being requested.
+     * @param int $strictness One of IGNORE_MISSING or MUST_EXIST.
+     * @return mixed The data retrieved from the cache, or false if the key did not exist within the cache.
+     *      If MUST_EXIST was used then an exception will be thrown if the key does not exist within the cache.
+     */
+    public function get($key, $strictness = IGNORE_MISSING);
+
+    /**
+     * Retrieves an array of values for an array of keys.
+     *
+     * Using this function comes with potential performance implications.
+     * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call
+     * the equivalent singular method for each item provided.
+     * This should not deter you from using this function as there is a performance benefit in situations where the cache
+     * store does support it, but you should be aware of this fact.
+     *
+     * @param array $keys The keys of the data being requested.
+     * @param int $strictness One of IGNORE_MISSING or MUST_EXIST.
+     * @return array An array of key value pairs for the items that could be retrieved from the cache.
+     *      If MUST_EXIST was used and not all keys existed within the cache then an exception will be thrown.
+     *      Otherwise any key that did not exist will have a data value of false within the results.
+     */
+    public function get_many(array $keys, $strictness = IGNORE_MISSING);
+
+    /**
+     * Sends a key => value pair to the cache.
+     *
+     * <code>
+     * // This code will add four entries to the cache, one for each url.
+     * $cache->set('main', 'http://moodle.org');
+     * $cache->set('docs', 'http://docs.moodle.org');
+     * $cache->set('tracker', 'http://tracker.moodle.org');
+     * $cache->set('qa', 'http://qa.moodle.net');
+     * </code>
+     *
+     * @param string|int $key The key for the data being requested.
+     * @param mixed $data The data to set against the key.
+     * @return bool True on success, false otherwise.
+     */
+    public function set($key, $data);
+
+    /**
+     * Sends several key => value pairs to the cache.
+     *
+     * Using this function comes with potential performance implications.
+     * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call
+     * the equivalent singular method for each item provided.
+     * This should not deter you from using this function as there is a performance benefit in situations where the cache store
+     * does support it, but you should be aware of this fact.
+     *
+     * <code>
+     * // This code will add four entries to the cache, one for each url.
+     * $cache->set_many(array(
+     *     'main' => 'http://moodle.org',
+     *     'docs' => 'http://docs.moodle.org',
+     *     'tracker' => 'http://tracker.moodle.org',
+     *     'qa' => ''http://qa.moodle.net'
+     * ));
+     * </code>
+     *
+     * @param array $keyvaluearray An array of key => value pairs to send to the cache.
+     * @return int The number of items successfully set. It is up to the developer to check this matches the number of items.
+     *      ... if they care that is.
+     */
+    public function set_many(array $keyvaluearray);
+
+    /**
+     * Test is a cache has a key.
+     *
+     * The use of the has methods is strongly discouraged. In a high load environment the cache may well change between the
+     * test and any subsequent action (get, set, delete etc).
+     * Instead it is recommended to write your code in such a way they it performs the following steps:
+     * <ol>
+     * <li>Attempt to retrieve the information.</li>
+     * <li>Generate the information.</li>
+     * <li>Attempt to set the information</li>
+     * </ol>
+     *
+     * Its also worth mentioning that not all stores support key tests.
+     * For stores that don't support key tests this functionality is mimicked by using the equivalent get method.
+     * Just one more reason you should not use these methods unless you have a very good reason to do so.
+     *
+     * @param string|int $key
+     * @return bool True if the cache has the requested key, false otherwise.
+     */
+    public function has($key);
+
+    /**
+     * Test if a cache has at least one of the given keys.
+     *
+     * It is strongly recommended to avoid the use of this function if not absolutely required.
+     * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc).
+     *
+     * Its also worth mentioning that not all stores support key tests.
+     * For stores that don't support key tests this functionality is mimicked by using the equivalent get method.
+     * Just one more reason you should not use these methods unless you have a very good reason to do so.
+     *
+     * @param array $keys
+     * @return bool True if the cache has at least one of the given keys
+     */
+    public function has_any(array $keys);
+
+    /**
+     * Test is a cache has all of the given keys.
+     *
+     * It is strongly recommended to avoid the use of this function if not absolutely required.
+     * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc).
+     *
+     * Its also worth mentioning that not all stores support key tests.
+     * For stores that don't support key tests this functionality is mimicked by using the equivalent get method.
+     * Just one more reason you should not use these methods unless you have a very good reason to do so.
+     *
+     * @param array $keys
+     * @return bool True if the cache has all of the given keys, false otherwise.
+     */
+    public function has_all(array $keys);
+
+    /**
+     * Delete the given key from the cache.
+     *
+     * @param string|int $key The key to delete.
+     * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores.
+     *     This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this.
+     * @return bool True of success, false otherwise.
+     */
+    public function delete($key, $recurse = true);
+
+    /**
+     * Delete all of the given keys from the cache.
+     *
+     * @param array $keys The key to delete.
+     * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores.
+     *     This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this.
+     * @return int The number of items successfully deleted.
+     */
+    public function delete_many(array $keys, $recurse = true);
+}
+
+/**
+ * Cache Loader supporting locking.
+ *
+ * This interface should be given to classes already implementing cache_loader that also wish to support locking.
+ * It outlines the required structure for utilising locking functionality when using a cache.
+ *
+ * Can be implemented by any class already implementing the cache_loader interface.
+ */
+interface cache_loader_with_locking {
+
+    /**
+     * Acquires a lock for the given key.
+     *
+     * Please note that this happens automatically if the cache definition requires locking.
+     * it is still made a public method so that adhoc caches can use it if they choose.
+     * However this doesn't guarantee consistent access. It will become the reponsiblity of the calling code to ensure locks
+     * are acquired, checked, and released.
+     *
+     * @param string|int $key
+     * @return bool True if the lock could be acquired, false otherwise.
+     */
+    public function acquire_lock($key);
+
+    /**
+     * Checks if the cache loader owns the lock for the given key.
+     *
+     * Please note that this happens automatically if the cache definition requires locking.
+     * it is still made a public method so that adhoc caches can use it if they choose.
+     * However this doesn't guarantee consistent access. It will become the reponsiblity of the calling code to ensure locks
+     * are acquired, checked, and released.
+     *
+     * @param string|int $key
+     * @return bool True if this code has the lock, false if there is a lock but this code doesn't have it,
+     *      null if there is no lock.
+     */
+    public function check_lock_state($key);
+
+    /**
+     * Releases the lock for the given key.
+     *
+     * Please note that this happens automatically if the cache definition requires locking.
+     * it is still made a public method so that adhoc caches can use it if they choose.
+     * However this doesn't guarantee consistent access. It will become the reponsiblity of the calling code to ensure locks
+     * are acquired, checked, and released.
+     *
+     * @param string|int $key
+     * @return bool True if the lock has been released, false if there was a problem releasing the lock.
+     */
+    public function release_lock($key);
+}
+
+/**
+ * Cache store.
+ *
+ * This interface outlines the requirements for a cache store plugin.
+ * It must be implemented by all such plugins and provides a reference to interacting with cache stores.
+ *
+ * Must be implemented by all cache store plugins.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface cache_store {
+
+    /**#@+
+     * Constants for features a cache store can support
+     */
+    /**
+     * Supports multi-part keys
+     */
+    const SUPPORTS_MULTIPLE_IDENTIFIERS = 1;
+    /**
+     * Ensures data remains in the cache once set.
+     */
+    const SUPPORTS_DATA_GUARANTEE = 2;
+    /**
+     * Supports a native ttl system.
+     */
+    const SUPPORTS_NATIVE_TTL = 4;
+    /**#@-*/
+
+    /**#@+
+     * Constants for the modes of a cache store
+     */
+    /**
+     * Application caches. These are shared caches.
+     */
+    const MODE_APPLICATION = 1;
+    /**
+     * Session caches. Just access to the PHP session.
+     */
+    const MODE_SESSION = 2;
+    /**
+     * Request caches. Static caches really.
+     */
+    const MODE_REQUEST = 4;
+    /**#@-*/
+
+    /**
+     * Static method to check if the store requirements are met.
+     *
+     * @return bool True if the stores software/hardware requirements have been met and it can be used. False otherwise.
+     */
+    public static function are_requirements_met();
+
+    /**
+     * Static method to check if a store is usable with the given mode.
+     *
+     * @param int $mode One of cache_store::MODE_*
+     */
+    public static function is_supported_mode($mode);
+
+    /**
+     * Returns the supported features as a binary flag.
+     *
+     * @param array $configuration The configuration of a store to consider specifically.
+     * @return int The supported features.
+     */
+    public static function get_supported_features(array $configuration = array());
+
+    /**
+     * Returns the supported modes as a binary flag.
+     *
+     * @param array $configuration The configuration of a store to consider specifically.
+     * @return int The supported modes.
+     */
+    public static function get_supported_modes(array $configuration = array());
+
+    /**
+     * Returns true if this cache store instance supports multiple identifiers.
+     *
+     * @return bool
+     */
+    public function supports_multiple_indentifiers();
+
+    /**
+     * Returns true if this cache store instance promotes data guarantee.
+     *
+     * @return bool
+     */
+    public function supports_data_guarantee();
+
+    /**
+     * Returns true if this cache store instance supports ttl natively.
+     *
+     * @return bool
+     */
+    public function supports_native_ttl();
+
+    /**
+     * Used to control the ability to add an instance of this store through the admin interfaces.
+     *
+     * @return bool True if the user can add an instance, false otherwise.
+     */
+    public static function can_add_instance();
+
+    /**
+     * Constructs an instance of the cache store.
+     *
+     * This method should not create connections or perform and processing, it should be used
+     *
+     * @param string $name The name of the cache store
+     * @param array $configuration The configuration for this store instance.
+     */
+    public function __construct($name, array $configuration = array());
+
+    /**
+     * Returns the name of this store instance.
+     * @return string
+     */
+    public function my_name();
+
+    /**
+     * Initialises a new instance of the cache store given the definition the instance is to be used for.
+     *
+     * This function should prepare any given connections etc.
+     *
+     * @param cache_definition $definition
+     */
+    public function initialise(cache_definition $definition);
+
+    /**
+     * Returns true if this cache store instance has been initialised.
+     * @return bool
+     */
+    public function is_initialised();
+
+    /**
+     * Returns true if this cache store instance is ready to use.
+     * @return bool
+     */
+    public function is_ready();
+
+    /**
+     * Retrieves an item from the cache store given its key.
+     *
+     * @param string $key The key to retrieve
+     * @return mixed The data that was associated with the key, or false if the key did not exist.
+     */
+    public function get($key);
+
+    /**
+     * Retrieves several items from the cache store in a single transaction.
+     *
+     * If not all of the items are available in the cache then the data value for those that are missing will be set to false.
+     *
+     * @param array $keys The array of keys to retrieve
+     * @return array An array of items from the cache. There will be an item for each key, those that were not in the store will
+     *      be set to false.
+     */
+    public function get_many($keys);
+
+    /**
+     * Sets an item in the cache given its key and data value.
+     *
+     * @param string $key The key to use.
+     * @param mixed $data The data to set.
+     * @return bool True if the operation was a success false otherwise.
+     */
+    public function set($key, $data);
+
+    /**
+     * Sets many items in the cache in a single transaction.
+     *
+     * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
+     *      keys, 'key' and 'value'.
+     * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
+     *      sent ... if they care that is.
+     */
+    public function set_many(array $keyvaluearray);
+
+    /**
+     * Deletes an item from the cache store.
+     *
+     * @param string $key The key to delete.
+     * @return bool Returns true if the operation was a success, false otherwise.
+     */
+    public function delete($key);
+
+    /**
+     * Deletes several keys from the cache in a single action.
+     *
+     * @param array $keys The keys to delete
+     * @return int The number of items successfully deleted.
+     */
+    public function delete_many(array $keys);
+
+    /**
+     * Purges the cache deleting all items within it.
+     *
+     * @return boolean True on success. False otherwise.
+     */
+    public function purge();
+
+    /**
+     * Performs any necessary clean up when the store instance is being deleted.
+     */
+    public function cleanup();
+
+    /**
+     * Generates an instance of the cache store that can be used for testing.
+     *
+     * Returns an instance of the cache store, or false if one cannot be created.
+     *
+     * @param cache_definition $definition
+     * @return cache_store|false
+     */
+    public static function initialise_test_instance(cache_definition $definition);
+}
+
+/**
+ * Cache store feature: locking
+ *
+ * This is a feature that cache stores can implement if they wish to support locking themselves rather
+ * than having the cache loader handle it for them.
+ *
+ * Can be implemented by classes already implementing cache_store.
+ */
+interface cache_is_lockable {
+
+    /**
+     * Acquires a lock on the given key for the given identifier.
+     *
+     * @param string $key The key we are locking.
+     * @param string $ownerid The identifier so we can check if we have the lock or if it is someone else.
+     *      The use of this property is entirely optional and implementations can act as they like upon it.
+     * @return bool True if the lock could be acquired, false otherwise.
+     */
+    public function acquire_lock($key, $ownerid);
+
+    /**
+     * Test if there is already a lock for the given key and if there is whether it belongs to the calling code.
+     *
+     * @param string $key The key we are locking.
+     * @param string $ownerid The identifier so we can check if we have the lock or if it is someone else.
+     * @return bool True if this code has the lock, false if there is a lock but this code doesn't have it, null if there
+     *      is no lock.
+     */
+    public function check_lock_state($key, $ownerid);
+
+    /**
+     * Releases the lock on the given key.
+     *
+     * @param string $key The key we are locking.
+     * @param string $ownerid The identifier so we can check if we have the lock or if it is someone else.
+     *      The use of this property is entirely optional and implementations can act as they like upon it.
+     * @return bool True if the lock has been released, false if there was a problem releasing the lock.
+     */
+    public function release_lock($key, $ownerid);
+}
+
+/**
+ * Cache store feature: key awareness.
+ *
+ * This is a feature that cache stores and cache loaders can both choose to implement.
+ * If a cache store implements this then it will be made responsible for tests for items within the cache.
+ * If the cache store being used doesn't implement this then it will be the responsibility of the cache loader to use the
+ * equivalent get methods to mimick the functionality of these tests.
+ *
+ * Cache stores should only override these methods if they natively support such features or if they have a better performing
+ * means of performing these tests than the handling that would otherwise take place in the cache_loader.
+ *
+ * Can be implemented by classes already implementing cache_store.
+ */
+interface cache_is_key_aware {
+
+    /**
+     * Test is a cache has a key.
+     *
+     * The use of the has methods is strongly discouraged. In a high load environment the cache may well change between the
+     * test and any subsequent action (get, set, delete etc).
+     * Instead it is recommended to write your code in such a way they it performs the following steps:
+     * <ol>
+     * <li>Attempt to retrieve the information.</li>
+     * <li>Generate the information.</li>
+     * <li>Attempt to set the information</li>
+     * </ol>
+     *
+     * Its also worth mentioning that not all stores support key tests.
+     * For stores that don't support key tests this functionality is mimicked by using the equivalent get method.
+     * Just one more reason you should not use these methods unless you have a very good reason to do so.
+     *
+     * @param string|int $key
+     * @return bool True if the cache has the requested key, false otherwise.
+     */
+    public function has($key);
+
+    /**
+     * Test if a cache has at least one of the given keys.
+     *
+     * It is strongly recommended to avoid the use of this function if not absolutely required.
+     * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc).
+     *
+     * Its also worth mentioning that not all stores support key tests.
+     * For stores that don't support key tests this functionality is mimicked by using the equivalent get method.
+     * Just one more reason you should not use these methods unless you have a very good reason to do so.
+     *
+     * @param array $keys
+     * @return bool True if the cache has at least one of the given keys
+     */
+    public function has_any(array $keys);
+
+    /**
+     * Test is a cache has all of the given keys.
+     *
+     * It is strongly recommended to avoid the use of this function if not absolutely required.
+     * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc).
+     *
+     * Its also worth mentioning that not all stores support key tests.
+     * For stores that don't support key tests this functionality is mimicked by using the equivalent get method.
+     * Just one more reason you should not use these methods unless you have a very good reason to do so.
+     *
+     * @param array $keys
+     * @return bool True if the cache has all of the given keys, false otherwise.
+     */
+    public function has_all(array $keys);
+}
+
+/**
+ * Cache Data Source.
+ *
+ * The cache data source interface can be implemented by any class within Moodle.
+ * If implemented then the class can be reference in a cache definition and will be used to load information that cannot be
+ * retrieved from the cache. As part of its retrieval that information will also be loaded into the cache.
+ *
+ * This allows developers to created a complete cache solution that can be used through code ensuring consistent cache
+ * interaction and loading. Allowing them in turn to centralise code and help keeps things more easily maintainable.
+ *
+ * Can be implemented by any class.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface cache_data_source {
+
+    /**
+     * Returns an instance of the data source class that the cache can use for loading data using the other methods
+     * specified by this interface.
+     *
+     * @param cache_definition $definition
+     * @return object
+     */
+    public static function get_instance_for_cache(cache_definition $definition);
+
+    /**
+     * Loads the data for the key provided ready formatted for caching.
+     *
+     * @param string|int $key The key to load.
+     * @return mixed What ever data should be returned, or false if it can't be loaded.
+     */
+    public function load_for_cache($key);
+
+    /**
+     * Loads several keys for the cache.
+     *
+     * @param array $keys An array of keys each of which will be string|int.
+     * @return array An array of matching data items.
+     */
+    public function load_many_for_cache(array $keys);
+}
+
+/**
+ * Cacheable object.
+ *
+ * This interface can be implemented by any class that is going to be passed into a cache and allows it to take control of the
+ * structure and the information about to be cached, as well as how to deal with it when it is retrieved from a cache.
+ * Think of it like serialisation and the __sleep and __wakeup methods.
+ * This is used because cache stores are responsible for how they interact with data and what they do when storing it. This
+ * interface ensures there is always a guaranteed action.
+ */
+interface cacheable_object {
+
+    /**
+     * Prepares the object for caching. Works like the __sleep method.
+     *
+     * @return mixed The data to cache, can be anything except a class that implements the cacheable_object... that would
+     *      be dumb.
+     */
+    public function prepare_to_cache();
+
+    /**
+     * Takes the data provided by prepare_to_cache and reinitialises an instance of the associated from it.
+     *
+     * @param mixed $data
+     * @return object The instance for the given data.
+     */
+    public static function wake_from_cache($data);
+}
+
+/**
+ * Cache lock interface
+ *
+ * This interface needs to be inherited by all cache lock plugins.
+ */
+interface cache_lock_interface {
+    /**
+     * Constructs an instance of the cache lock given its name and its configuration data
+     *
+     * @param string $name The unique name of the lock instance
+     * @param array $configuration
+     */
+    public function __construct($name, array $configuration = array());
+
+    /**
+     * Acquires a lock on a given key.
+     *
+     * @param string $key The key to acquire a lock for.
+     * @param string $ownerid An unique identifier for the owner of this lock. It is entirely optional for the cache lock plugin
+     *      to use this. Each implementation can decide for themselves.
+     * @param bool $block If set to true the application will wait until a lock can be acquired
+     * @return bool True if the lock can be acquired false otherwise.
+     */
+    public function lock($key, $ownerid, $block = false);
+
+    /**
+     * Releases the lock held on a certain key.
+     *
+     * @param string $key The key to release the lock for.
+     * @param string $ownerid An unique identifier for the owner of this lock. It is entirely optional for the cache lock plugin
+     *      to use this. Each implementation can decide for themselves.
+     * @param bool $forceunlock If set to true the lock will be removed if it exists regardless of whether or not we own it.
+     */
+    public function unlock($key, $ownerid, $forceunlock = false);
+
+    /**
+     * Checks the state of the given key.
+     *
+     * Returns true if the key is locked and belongs to the ownerid.
+     * Returns false if the key is locked but does not belong to the ownerid.
+     * Returns null if there is no lock
+     *
+     * @param string $key The key we are checking for.
+     * @param string $ownerid The identifier so we can check if we have the lock or if it is someone else.
+     * @return bool True if this code has the lock, false if there is a lock but this code doesn't have it, null if there
+     *      is no lock.
+     */
+    public function check_state($key, $ownerid);
+
+    /**
+     * Cleans up any left over locks.
+     *
+     * This function MUST clean up any locks that have been acquired and not released during processing.
+     * Although the situation of acquiring a lock and not releasing it should be insanely rare we need to deal with it.
+     * Things such as unfortunate timeouts etc could cause this situation.
+     */
+    public function __destruct();
+}
\ No newline at end of file
diff --git a/cache/classes/loaders.php b/cache/classes/loaders.php
new file mode 100644 (file)
index 0000000..8595808
--- /dev/null
@@ -0,0 +1,1454 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Cache loaders
+ *
+ * This file is part of Moodle's cache API, affectionately called MUC.
+ * It contains the components that are requried in order to use caching.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The main cache class.
+ *
+ * This class if the first class that any end developer will interact with.
+ * In order to create an instance of a cache that they can work with they must call one of the static make methods belonging
+ * to this class.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cache implements cache_loader, cache_is_key_aware {
+
+    /**
+     * We need a timestamp to use within the cache API.
+     * This stamp needs to be used for all ttl and time based operations to ensure that we don't end up with
+     * timing issues.
+     * @var int
+     */
+    protected static $now;
+
+    /**
+     * The definition used when loading this cache if there was one.
+     * @var cache_definition
+     */
+    private $definition = false;
+
+    /**
+     * The cache store that this loader will make use of.
+     * @var cache_store
+     */
+    private $store;
+
+    /**
+     * The next cache loader in the chain if there is one.
+     * If a cache request misses for the store belonging to this loader then the loader
+     * stored here will be checked next.
+     * If there is a loader here then $datasource must be false.
+     * @var cache_loader|false
+     */
+    private $loader = false;
+
+    /**
+     * The data source to use if we need to load data (because if doesn't exist in the cache store).
+     * If there is a data source here then $loader above must be false.
+     * @var cache_data_source|false
+     */
+    private $datasource = false;
+
+    /**
+     * Used to quickly check if the store supports key awareness.
+     * This is set when the cache is initialised and is used to speed up processing.
+     * @var bool
+     */
+    private $supportskeyawareness = null;
+
+    /**
+     * Used to quickly check if the store supports ttl natively.
+     * This is set when the cache is initialised and is used to speed up processing.
+     * @var bool
+     */
+    private $supportsnativettl = null;
+
+    /**
+     * Gets set to true if the cache is going to be using the build in static "persist" cache.
+     * The persist cache statically caches items used during the lifetime of the request. This greatly speeds up interaction
+     * with the cache in areas where it will be repetitively hit for the same information such as with strings.
+     * There are several other variables to control how this persist cache works.
+     * @var bool
+     */
+    private $persist = false;
+
+    /**
+     * The persist cache itself.
+     * Items will be stored in this cache as they were provided. This ensure there is no unnecessary processing taking place.
+     * @var array
+     */
+    private $persistcache = array();
+
+    /**
+     * The number of items in the persist cache. Avoids count calls like you wouldn't believe.
+     * @var int
+     */
+    private $persistcount = 0;
+
+    /**
+     * An array containing just the keys being used in the persist cache.
+     * This seems redundant perhaps but is used when managing the size of the persist cache.
+     * @var array
+     */
+    private $persistkeys = array();
+
+    /**
+     * The maximum size of the persist cache. If set to false there is no max size.
+     * Caches that make use of the persist cache should seriously consider setting this to something reasonably small, but
+     * still large enough to offset repetitive calls.
+     * @var int|false
+     */
+    private $persistmaxsize = false;
+
+    /**
+     * Gets set to true during initialisation if the definition is making use of a ttl.
+     * Used to speed up processing.
+     * @var bool
+     */
+    private $hasattl = false;
+
+    /**
+     * Gets set to the class name of the store during initialisation. This is used several times in the cache class internally
+     * and having it here helps speed up processing.
+     * @var strubg
+     */
+    private $storetype = 'unknown';
+
+    /**
+     * Gets set to true if we want to collect performance information about the cache API.
+     * @var bool
+     */
+    protected $perfdebug = false;
+
+    /**
+     * Determines if this loader is a sub loader, not the top of the chain.
+     * @var bool
+     */
+    protected $subloader = false;
+
+    /**
+     * Creates a new cache instance for a pre-defined definition.
+     *
+     * @param string $component The component for the definition
+     * @param string $area The area for the definition
+     * @param array $identifiers Any additional identifiers that should be provided to the definition.
+     * @param string $aggregate Super advanced feature. More docs later.
+     * @return cache_application|cache_session|cache_store
+     */
+    public static function make($component, $area, array $identifiers = array(), $aggregate = null) {
+        $factory = cache_factory::instance();
+        return $factory->create_cache_from_definition($component, $area, $identifiers, $aggregate);
+    }
+
+    /**
+     * Creates a new cache instance based upon the given params.
+     *
+     * @param int $mode One of cache_store::MODE_*
+     * @param string $component The component this cache relates to.
+     * @param string $area The area this cache relates to.
+     * @param array $identifiers Any additional identifiers that should be provided to the definition.
+     * @param bool $persistent If set to true the cache will persist construction requests.
+     * @return cache_application|cache_session|cache_store
+     */
+    public static function make_from_params($mode, $component, $area, array $identifiers = array(), $persistent = false) {
+        $factory = cache_factory::instance();
+        return $factory->create_cache_from_params($mode, $component, $area, $identifiers, $persistent);
+    }
+
+    /**
+     * Constructs a new cache instance.
+     *
+     * You should not call this method from your code, instead you should use the cache::make methods.
+     *
+     * This method is public so that the cache_factory is able to instantiate cache instances.
+     * Ideally we would make this method protected and expose its construction to the factory method internally somehow.
+     * The factory class is responsible for this in order to centralise the storage of instances once created. This way if needed
+     * we can force a reset of the cache API (used during unit testing).
+     *
+     * @param cache_definition $definition The definition for the cache instance.
+     * @param cache_store $store The store that cache should use.
+     * @param cache_loader|cache_data_source $loader The next loader in the chain or the data source if there is one and there
+     *      are no other cache_loaders in the chain.
+     */
+    public function __construct(cache_definition $definition, cache_store $store, $loader = null) {
+        global $CFG;
+        $this->definition = $definition;
+        $this->store = $store;
+        $this->storetype = get_class($store);
+        $this->perfdebug = !empty($CFG->perfdebug);
+        if ($loader instanceof cache_loader) {
+            $this->loader = $loader;
+            // Mark the loader as a sub (chained) loader.
+            $this->loader->set_is_sub_loader(true);
+        } else if ($loader instanceof cache_data_source) {
+            $this->datasource = $loader;
+        }
+        $this->definition->generate_definition_hash();
+        $this->persist = $this->definition->should_be_persistent();
+        if ($this->persist) {
+            $this->persistmaxsize = $this->definition->get_persistent_max_size();
+        }
+        $this->hasattl = ($this->definition->get_ttl() > 0);
+    }
+
+    /**
+     * Used to inform the loader of its state as a sub loader, or as the top of the chain.
+     *
+     * This is important as it ensures that we do not have more than one loader keeping persistent data.
+     * Subloaders need to be "pure" loaders in the sense that they are used to store and retrieve information from stores or the
+     * next loader/data source in the chain.
+     * Nothing fancy, nothing flash.
+     *
+     * @param bool $setting
+     */
+    protected function set_is_sub_loader($setting = true) {
+        if ($setting) {
+            $this->subloader = true;
+            // Subloaders should not keep persistent data.
+            $this->persist = false;
+            $this->persistmaxsize = false;
+        } else {
+            $this->subloader = true;
+            $this->persist = $this->definition->should_be_persistent();
+            if ($this->persist) {
+                $this->persistmaxsize = $this->definition->get_persistent_max_size();
+            }
+        }
+    }
+
+    /**
+     * Alters the identifiers that have been provided to the definition.
+     *
+     * This is an advanced method and should not be used unless really needed.
+     * It allows the developer to slightly alter the definition without having to re-establish the cache.
+     * It will cause more processing as the definition will need to clear and reprepare some of its properties.
+     *
+     * @param array $identifiers
+     */
+    public function set_identifiers(array $identifiers) {
+        $this->definition->set_identifiers($identifiers);
+    }
+
+    /**
+     * Retrieves the value for the given key from the cache.
+     *
+     * @param string|int $key The key for the data being requested.
+     *      It can be any structure although using a scalar string or int is recommended in the interests of performance.
+     *      In advanced cases an array may be useful such as in situations requiring the multi-key functionality.
+     * @param int $strictness One of IGNORE_MISSING | MUST_EXIST
+     * @return mixed|false The data from the cache or false if the key did not exist within the cache.
+     * @throws moodle_exception
+     */
+    public function get($key, $strictness = IGNORE_MISSING) {
+        // 1. Parse the key.
+        $parsedkey = $this->parse_key($key);
+        // 2. Get it from the persist cache if we can (only when persist is enabled and it has already been requested/set).
+        $result = $this->get_from_persist_cache($parsedkey);
+        if ($result !== false) {
+            if ($this->perfdebug) {
+                cache_helper::record_cache_hit('** static persist **', $this->definition->get_id());
+            }
+            if (!is_scalar($result)) {
+                // If data is an object it will be a reference.
+                // If data is an array if may contain references.
+                // We want to break references so that the cache cannot be modified outside of itself.
+                // Call the function to unreference it (in the best way possible).
+                $result = $this->unref($result);
+            }
+            return $result;
+        } else if ($this->perfdebug) {
+            cache_helper::record_cache_miss('** static persist **', $this->definition->get_id());
+        }
+        // 3. Get it from the store. Obviously wasn't in the persist cache.
+        $result = $this->store->get($parsedkey);
+        if ($result !== false) {
+            if ($result instanceof cache_ttl_wrapper) {
+                if ($result->has_expired()) {
+                    $this->store->delete($parsedkey);
+                    $result = false;
+                } else {
+                    $result = $result->data;
+                }
+            }
+            if ($result instanceof cache_cached_object) {
+                $result = $result->restore_object();
+            }
+            if ($this->is_using_persist_cache()) {
+                $this->set_in_persist_cache($parsedkey, $result);
+            }
+        }
+        // 4. Load if from the loader/datasource if we don't already have it.
+        $setaftervalidation = false;
+        if ($result === false) {
+            if ($this->perfdebug) {
+                cache_helper::record_cache_miss($this->storetype, $this->definition->get_id());
+            }
+            if ($this->loader !== false) {
+                $result = $this->loader->get($parsedkey);
+            } else if ($this->datasource !== false) {
+                $result = $this->datasource->load_for_cache($key);
+            }
+            $setaftervalidation = ($result !== false);
+        } else if ($this->perfdebug) {
+            cache_helper::record_cache_hit($this->storetype, $this->definition->get_id());
+        }
+        // 5. Validate strictness.
+        if ($strictness === MUST_EXIST && $result === false) {
+            throw new moodle_exception('Requested key did not exist in any cache stores and could not be loaded.');
+        }
+        // 6. Set it to the store if we got it from the loader/datasource.
+        if ($setaftervalidation) {
+            $this->set($key, $result);
+        }
+        // 7. Make sure we don't pass back anything that could be a reference.
+        //    We don't want people modifying the data in the cache.
+        if (!is_scalar($result)) {
+            // If data is an object it will be a reference.
+            // If data is an array if may contain references.
+            // We want to break references so that the cache cannot be modified outside of itself.
+            // Call the function to unreference it (in the best way possible).
+            $result = $this->unref($result);
+        }
+        return $result;
+    }
+
+    /**
+     * Retrieves an array of values for an array of keys.
+     *
+     * Using this function comes with potential performance implications.
+     * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call
+     * the equivalent singular method for each item provided.
+     * This should not deter you from using this function as there is a performance benefit in situations where the cache store
+     * does support it, but you should be aware of this fact.
+     *
+     * @param array $keys The keys of the data being requested.
+     *      Each key can be any structure although using a scalar string or int is recommended in the interests of performance.
+     *      In advanced cases an array may be useful such as in situations requiring the multi-key functionality.
+     * @param int $strictness One of IGNORE_MISSING or MUST_EXIST.
+     * @return array An array of key value pairs for the items that could be retrieved from the cache.
+     *      If MUST_EXIST was used and not all keys existed within the cache then an exception will be thrown.
+     *      Otherwise any key that did not exist will have a data value of false within the results.
+     * @throws moodle_exception
+     */
+    public function get_many(array $keys, $strictness = IGNORE_MISSING) {
+
+        $parsedkeys = array();
+        $resultpersist = array();
+        $resultstore = array();
+        $keystofind = array();
+
+        // First up check the persist cache for each key.
+        $isusingpersist = $this->is_using_persist_cache();
+        foreach ($keys as $key) {
+            $pkey = $this->parse_key($key);
+            $parsedkeys[$pkey] = $key;
+            $keystofind[$pkey] = $key;
+            if ($isusingpersist) {
+                $value = $this->get_from_persist_cache($pkey);
+                if ($value !== false) {
+                    $resultpersist[$pkey] = $value;
+                    unset($keystofind[$pkey]);
+                }
+            }
+        }
+
+        // Next assuming we didn't find all of the keys in the persist cache try loading them from the store.
+        if (count($keystofind)) {
+            $resultstore = $this->store->get_many(array_keys($keystofind));
+            // Process each item in the result to "unwrap" it.
+            foreach ($resultstore as $key => $value) {
+                if ($value instanceof cache_ttl_wrapper) {
+                    if ($value->has_expired()) {
+                        $value = false;
+                    } else {
+                        $value = $value->data;
+                    }
+                }
+                if ($value instanceof cache_cached_object) {
+                    $value = $value->restore_object();
+                }
+                $resultstore[$key] = $value;
+            }
+        }
+
+        // Merge the result from the persis cache with the results from the store load.
+        $result = $resultpersist + $resultstore;
+        unset($resultpersist);
+        unset($resultstore);
+
+        // Next we need to find any missing values and load them from the loader/datasource next in the chain.
+        $usingloader = ($this->loader !== false);
+        $usingsource = (!$usingloader && ($this->datasource !== false));
+        if ($usingloader || $usingsource) {
+            $missingkeys = array();
+            foreach ($result as $key => $value) {
+                if ($value === false) {
+                    $missingkeys[] = ($usingloader) ? $key : $parsedkeys[$key];
+                }
+            }
+            if (!empty($missingkeys)) {
+                if ($usingloader) {
+                    $resultmissing = $this->loader->get_many($missingkeys);
+                } else {
+                    $resultmissing = $this->datasource->load_many_for_cache($missingkeys);
+                }
+                foreach ($resultmissing as $key => $value) {
+                    $result[$key] = $value;
+                    if ($value !== false) {
+                        $this->set($parsedkeys[$key], $value);
+                    }
+                }
+                unset($resultmissing);
+            }
+            unset($missingkeys);
+        }
+
+        // Create an array with the original keys and the found values. This will be what we return.
+        $fullresult = array();
+        foreach ($result as $key => $value) {
+            $fullresult[$parsedkeys[$key]] = $value;
+        }
+        unset($result);
+
+        // Final step is to check strictness.
+        if ($strictness === MUST_EXIST) {
+            foreach ($keys as $key) {
+                if (!array_key_exists($key, $fullresult)) {
+                    throw new moodle_exception('Not all the requested keys existed within the cache stores.');
+                }
+            }
+        }
+
+        // Return the result. Phew!
+        return $fullresult;
+    }
+
+    /**
+     * Sends a key => value pair to the cache.
+     *
+     * <code>
+     * // This code will add four entries to the cache, one for each url.
+     * $cache->set('main', 'http://moodle.org');
+     * $cache->set('docs', 'http://docs.moodle.org');
+     * $cache->set('tracker', 'http://tracker.moodle.org');
+     * $cache->set('qa', 'http://qa.moodle.net');
+     * </code>
+     *
+     * @param string|int $key The key for the data being requested.
+     *      It can be any structure although using a scalar string or int is recommended in the interests of performance.
+     *      In advanced cases an array may be useful such as in situations requiring the multi-key functionality.
+     * @param mixed $data The data to set against the key.
+     * @return bool True on success, false otherwise.
+     */
+    public function set($key, $data) {
+        if ($this->perfdebug) {
+            cache_helper::record_cache_set($this->storetype, $this->definition->get_id());
+        }
+        if (is_object($data) && $data instanceof cacheable_object) {
+            $data = new cache_cached_object($data);
+        } else if (!is_scalar($data)) {
+            // If data is an object it will be a reference.
+            // If data is an array if may contain references.
+            // We want to break references so that the cache cannot be modified outside of itself.
+            // Call the function to unreference it (in the best way possible).
+            $data = $this->unref($data);
+        }
+        if ($this->has_a_ttl() && !$this->store_supports_native_ttl()) {
+            $data = new cache_ttl_wrapper($data, $this->definition->get_ttl());
+        }
+        $parsedkey = $this->parse_key($key);
+        if ($this->is_using_persist_cache()) {
+            $this->set_in_persist_cache($parsedkey, $data);
+        }
+        return $this->store->set($parsedkey, $data);
+    }
+
+    /**
+     * Removes references where required.
+     *
+     * @param stdClass|array $data
+     */
+    protected function unref($data) {
+        // Check if it requires serialisation in order to produce a reference free copy.
+        if ($this->requires_serialisation($data)) {
+            // Damn, its going to have to be serialise.
+            $data = serialize($data);
+            // We unserialise immediately so that we don't have to do it every time on get.
+            $data = unserialize($data);
+        } else if (!is_scalar($data)) {
+            // Its safe to clone, lets do it, its going to beat the pants of serialisation.
+            $data = $this->deep_clone($data);
+        }
+        return $data;
+    }
+
+    /**
+     * Checks to see if a var requires serialisation.
+     *
+     * @param mixed $value The value to check.
+     * @param int $depth Used to ensure we don't enter an endless loop (think recursion).
+     * @return bool Returns true if the value is going to require serialisation in order to ensure a reference free copy
+     *      or false if its safe to clone.
+     */
+    protected function requires_serialisation($value, $depth = 1) {
+        if (is_scalar($value)) {
+            return false;
+        } else if (is_array($value) || $value instanceof stdClass || $value instanceof Traversable) {
+            if ($depth > 5) {
+                // Skrew it, mega-deep object, developer you suck, we're just going to serialise.
+                return true;
+            }
+            foreach ($value as $key => $subvalue) {
+                if ($this->requires_serialisation($subvalue, $depth++)) {
+                    return true;
+                }
+            }
+        }
+        // Its not scalar, array, or stdClass so we'll need to serialise.
+        return true;
+    }
+
+    /**
+     * Creates a reference free clone of the given value.
+     *
+     * @param mixed $value
+     * @return mixed
+     */
+    protected function deep_clone($value) {
+        if (is_object($value)) {
+            // Objects must be cloned to begin with.
+            $value = clone $value;
+        }
+        if (is_array($value) || is_object($value)) {
+            foreach ($value as $key => $subvalue) {
+                $value[$key] = $this->deep_clone($subvalue);
+            }
+        }
+        return $value;
+    }
+
+    /**
+     * Sends several key => value pairs to the cache.
+     *
+     * Using this function comes with potential performance implications.
+     * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call
+     * the equivalent singular method for each item provided.
+     * This should not deter you from using this function as there is a performance benefit in situations where the cache store
+     * does support it, but you should be aware of this fact.
+     *
+     * <code>
+     * // This code will add four entries to the cache, one for each url.
+     * $cache->set_many(array(
+     *     'main' => 'http://moodle.org',
+     *     'docs' => 'http://docs.moodle.org',
+     *     'tracker' => 'http://tracker.moodle.org',
+     *     'qa' => ''http://qa.moodle.net'
+     * ));
+     * </code>
+     *
+     * @param array $keyvaluearray An array of key => value pairs to send to the cache.
+     * @return int The number of items successfully set. It is up to the developer to check this matches the number of items.
+     *      ... if they care that is.
+     */
+    public function set_many(array $keyvaluearray) {
+        $data = array();
+        $simulatettl = $this->has_a_ttl() && !$this->store_supports_native_ttl();
+        $usepersistcache = $this->is_using_persist_cache();
+        foreach ($keyvaluearray as $key => $value) {
+            if (is_object($value) && $value instanceof cacheable_object) {
+                $value = new cache_cached_object($value);
+            } else if (!is_scalar($value)) {
+                // If data is an object it will be a reference.
+                // If data is an array if may contain references.
+                // We want to break references so that the cache cannot be modified outside of itself.
+                // Call the function to unreference it (in the best way possible).
+                $value = $this->unref($value);
+            }
+            if ($simulatettl) {
+                $value = new cache_ttl_wrapper($value, $this->definition->get_ttl());
+            }
+            $data[$key] = array(
+                'key' => $this->parse_key($key),
+                'value' => $value
+            );
+            if ($usepersistcache) {
+                $this->set_in_persist_cache($data[$key]['key'], $value);
+            }
+        }
+        if ($this->perfdebug) {
+            cache_helper::record_cache_set($this->storetype, $this->definition->get_id());
+        }
+        return $this->store->set_many($data);
+    }
+
+    /**
+     * Test is a cache has a key.
+     *
+     * The use of the has methods is strongly discouraged. In a high load environment the cache may well change between the
+     * test and any subsequent action (get, set, delete etc).
+     * Instead it is recommended to write your code in such a way they it performs the following steps:
+     * <ol>
+     * <li>Attempt to retrieve the information.</li>
+     * <li>Generate the information.</li>
+     * <li>Attempt to set the information</li>
+     * </ol>
+     *
+     * Its also worth mentioning that not all stores support key tests.
+     * For stores that don't support key tests this functionality is mimicked by using the equivalent get method.
+     * Just one more reason you should not use these methods unless you have a very good reason to do so.
+     *
+     * @param string|int $key
+     * @param bool $tryloadifpossible If set to true, the cache doesn't contain the key, and there is another cache loader or
+     *      data source then the code will try load the key value from the next item in the chain.
+     * @return bool True if the cache has the requested key, false otherwise.
+     */
+    public function has($key, $tryloadifpossible = false) {
+        $parsedkey = $this->parse_key($key);
+        if ($this->is_in_persist_cache($parsedkey)) {
+            return true;
+        }
+        if (($this->has_a_ttl() && !$this->store_supports_native_ttl()) || !$this->store_supports_key_awareness()) {
+            if ($this->store_supports_key_awareness() && !$this->store->has($parsedkey)) {
+                return false;
+            }
+            $data = $this->store->get($parsedkey);
+            if (!$this->store_supports_native_ttl()) {
+                $has = ($data instanceof cache_ttl_wrapper && !$data->has_expired());
+            } else {
+                $has = ($data !== false);
+            }
+        } else {
+            $has = $this->store->has($parsedkey);
+        }
+        if (!$has && $tryloadifpossible) {
+            if ($this->loader !== false) {
+                $result = $this->loader->get($parsedkey);
+            } else if ($this->datasource !== null) {
+                $result = $this->datasource->load_for_cache($key);
+            }
+            $has = ($result !== null);
+            if ($has) {
+                $this->set($key, $result);
+            }
+        }
+        return $has;
+    }
+
+    /**
+     * Test is a cache has all of the given keys.
+     *
+     * It is strongly recommended to avoid the use of this function if not absolutely required.
+     * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc).
+     *
+     * Its also worth mentioning that not all stores support key tests.
+     * For stores that don't support key tests this functionality is mimicked by using the equivalent get method.
+     * Just one more reason you should not use these methods unless you have a very good reason to do so.
+     *
+     * @param array $keys
+     * @return bool True if the cache has all of the given keys, false otherwise.
+     */
+    public function has_all(array $keys) {
+        if (($this->has_a_ttl() && !$this->store_supports_native_ttl()) || !$this->store_supports_key_awareness()) {
+            foreach ($keys as $key) {
+                if (!$this->has($key)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+        $parsedkeys = array_map(array($this, 'parse_key'), $keys);
+        return $this->store->has_all($parsedkeys);
+    }
+
+    /**
+     * Test if a cache has at least one of the given keys.
+     *
+     * It is strongly recommended to avoid the use of this function if not absolutely required.
+     * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc).
+     *
+     * Its also worth mentioning that not all stores support key tests.
+     * For stores that don't support key tests this functionality is mimicked by using the equivalent get method.
+     * Just one more reason you should not use these methods unless you have a very good reason to do so.
+     *
+     * @param array $keys
+     * @return bool True if the cache has at least one of the given keys
+     */
+    public function has_any(array $keys) {
+        if (($this->has_a_ttl() && !$this->store_supports_native_ttl()) || !$this->store_supports_key_awareness()) {
+            foreach ($keys as $key) {
+                if ($this->has($key)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        if ($this->is_using_persist_cache()) {
+            $parsedkeys = array();
+            foreach ($keys as $id => $key) {
+                $parsedkey = $this->parse_key($key);
+                if ($this->is_in_persist_cache($parsedkey)) {
+                    return true;
+                }
+                $parsedkeys[] = $parsedkey;
+            }
+        } else {
+            $parsedkeys = array_map(array($this, 'parse_key'), $keys);
+        }
+        return $this->store->has_any($parsedkeys);
+    }
+
+    /**
+     * Delete the given key from the cache.
+     *
+     * @param string|int $key The key to delete.
+     * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores.
+     *     This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this.
+     * @return bool True of success, false otherwise.
+     */
+    public function delete($key, $recurse = true) {
+        $parsedkey = $this->parse_key($key);
+        $this->delete_from_persist_cache($parsedkey);
+        if ($recurse && !empty($this->loader)) {
+            // Delete from the bottom of the stack first.
+            $this->loader->delete($key, $recurse);
+        }
+        return $this->store->delete($parsedkey);
+    }
+
+    /**
+     * Delete all of the given keys from the cache.
+     *
+     * @param array $keys The key to delete.
+     * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores.
+     *     This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this.
+     * @return int The number of items successfully deleted.
+     */
+    public function delete_many(array $keys, $recurse = true) {
+        $parsedkeys = array_map(array($this, 'parse_key'), $keys);
+        if ($this->is_using_persist_cache()) {
+            foreach ($parsedkeys as $parsedkey) {
+                $this->delete_from_persist_cache($parsedkey);
+            }
+        }
+        if ($recurse && !empty($this->loader)) {
+            // Delete from the bottom of the stack first.
+            $this->loader->delete_many($keys, $recurse);
+        }
+        return $this->store->delete_many($parsedkeys);
+    }
+
+    /**
+     * Purges the cache store, and loader if there is one.
+     *
+     * @return bool True on success, false otherwise
+     */
+    public function purge() {
+        // 1. Purge the persist cache.
+        $this->persistcache = array();
+        // 2. Purge the store.
+        $this->store->purge();
+        // 3. Optionally pruge any stacked loaders.
+        if ($this->loader) {
+            $this->loader->purge();
+        }
+        return true;
+    }
+
+    /**
+     * Parses the key turning it into a string (or array is required) suitable to be passed to the cache store.
+     *
+     * @param string|int $key As passed to get|set|delete etc.
+     * @return string|array String unless the store supports multi-identifiers in which case an array if returned.
+     */
+    protected function parse_key($key) {
+        if ($this->store->supports_multiple_indentifiers()) {
+            $result = $this->definition->generate_multi_key_parts();
+            $result['key'] = $key;
+            return $result;
+        }
+        return cache_helper::hash_key($this->definition->generate_single_key_prefix().'-'.$key);
+    }
+
+    /**
+     * Returns true if the cache is making use of a ttl.
+     * @return bool
+     */
+    protected function has_a_ttl() {
+        return $this->hasattl;
+    }
+
+    /**
+     * Returns true if the cache store supports native ttl.
+     * @return bool
+     */
+    protected function store_supports_native_ttl() {
+        if ($this->supportsnativettl === null) {
+            $this->supportsnativettl = ($this->store->supports_native_ttl());
+        }
+        return $this->supportsnativettl;
+    }
+
+    /**
+     * Returns the cache definition.
+     *
+     * @return cache_definition
+     */
+    protected function get_definition() {
+        return $this->definition;
+    }
+
+    /**
+     * Returns the cache store
+     *
+     * @return cache_store
+     */
+    protected function get_store() {
+        return $this->store;
+    }
+
+    /**
+     * Returns true if the store supports key awareness.
+     *
+     * @return bool
+     */
+    protected function store_supports_key_awareness() {
+        if ($this->supportskeyawareness === null) {
+            $this->supportskeyawareness = ($this->store instanceof cache_is_key_aware);
+        }
+        return $this->supportskeyawareness;
+    }
+
+    /**
+     * Returns true if the store natively supports locking.
+     *
+     * @return bool
+     */
+    protected function store_supports_native_locking() {
+        if ($this->nativelocking === null) {
+            $this->nativelocking = ($this->store instanceof cache_is_lockable);
+        }
+        return $this->nativelocking;
+    }
+
+    /**
+     * Returns true if this cache is making use of the persist cache.
+     *
+     * @return bool
+     */
+    protected function is_using_persist_cache() {
+        return $this->persist;
+    }
+
+    /**
+     * Returns true if the requested key exists within the persist cache.
+     *
+     * @param string $key The parsed key
+     * @return bool
+     */
+    protected function is_in_persist_cache($key) {
+        if (is_array($key)) {
+            $key = $key['key'];
+        }
+        // This could be written as a single line, however it has been split because the ttl check is faster than the instanceof
+        // and has_expired calls.
+        if (!$this->persist || !array_key_exists($key, $this->persistcache)) {
+            return false;
+        }
+        if ($this->has_a_ttl() && $this->store_supports_native_ttl()) {
+             return !($this->persistcache[$key] instanceof cache_ttl_wrapper && $this->persistcache[$key]->has_expired());
+        }
+        return true;
+    }
+
+    /**
+     * Returns the item from the persist cache if it exists there.
+     *
+     * @param string $key The parsed key
+     * @return mixed|false The data from the persist cache or false if it wasn't there.
+     */
+    protected function get_from_persist_cache($key) {
+        if (is_array($key)) {
+            $key = $key['key'];
+        }
+        if (!$this->persist || !array_key_exists($key, $this->persistcache)) {
+            return false;
+        }
+        $data = $this->persistcache[$key];
+        if (!$this->has_a_ttl() || !$data instanceof cache_ttl_wrapper) {
+            if ($data instanceof cache_cached_object) {
+                $data = $data->restore_object();
+            }
+            return $data;
+        }
+        if ($data->has_expired()) {
+            $this->delete_from_persist_cache($key);
+            return false;
+        } else {
+            if ($data instanceof cache_cached_object) {
+                $data = $data->restore_object();
+            }
+            return $data->data;
+        }
+    }
+
+    /**
+     * Sets a key value pair into the persist cache.
+     *
+     * @param string $key The parsed key
+     * @param mixed $data
+     * @return bool
+     */
+    protected function set_in_persist_cache($key, $data) {
+        if (is_array($key)) {
+            $key = $key['key'];
+        }
+        $this->persistcache[$key] = $data;
+        if ($this->persistmaxsize !== false) {
+            $this->persistcount++;
+            if ($this->persistcount > $this->persistmaxsize) {
+                $dropkey = array_shift($this->persistkeys);
+                unset($this->persistcache[$dropkey]);
+                $this->persistcount--;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Deletes an item from the persist cache.
+     *
+     * @param string|int $key As given to get|set|delete
+     * @return bool True on success, false otherwise.
+     */
+    protected function delete_from_persist_cache($key) {
+        unset($this->persistcache[$key]);
+        if ($this->persistmaxsize !== false) {
+            $dropkey = array_search($key, $this->persistkeys);
+            if ($dropkey) {
+                unset($this->persistkeys[$dropkey]);
+                $this->persistcount--;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns the timestamp from the first request for the time from the cache API.
+     *
+     * This stamp needs to be used for all ttl and time based operations to ensure that we don't end up with
+     * timing issues.
+     *
+     * @return int
+     */
+    public static function now() {
+        if (self::$now === null) {
+            self::$now = time();
+        }
+        return self::$now;
+    }
+}
+
+/**
+ * An application cache.
+ *
+ * This class is used for application caches returned by the cache::make methods.
+ * On top of the standard functionality it also allows locking to be required and or manually operated.
+ *
+ * This cache class should never be interacted with directly. Instead you should always use the cache::make methods.
+ * It is technically possible to call those methods through this class however there is no guarantee that you will get an
+ * instance of this class back again.
+ *
+ * @internal don't use me directly.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cache_application extends cache implements cache_loader_with_locking {
+
+    /**
+     * Lock identifier.
+     * This is used to ensure the lock belongs to the cache instance + definition + user.
+     * @var string
+     */
+    protected $lockidentifier;
+
+    /**
+     * Gets set to true if the cache's primary store natively supports locking.
+     * If it does then we use that, otherwise we need to instantiate a second store to use for locking.
+     * @var cache_store
+     */
+    protected $nativelocking = null;
+
+    /**
+     * Gets set to true if the cache is going to be using locking.
+     * This isn't a requirement, it doesn't need to use locking (most won't) and this bool is used to quickly check things.
+     * If required then locking will be forced for the get|set|delete operation.
+     * @var bool
+     */
+    protected $requirelocking = false;
+
+    /**
+     * Gets set to true if the cache must use read locking (get|has).
+     * @var bool
+     */
+    protected $requirelockingread = false;
+
+    /**
+     * Gets set to true if the cache must use write locking (set|delete)
+     * @var bool
+     */
+    protected $requirelockingwrite = false;
+
+    /**
+     * Gets set to a cache_store to use for locking if the caches primary store doesn't support locking natively.
+     * @var cache_lock_interface
+     */
+    protected $cachelockinstance;
+
+    /**
+     * Overrides the cache construct method.
+     *
+     * You should not call this method from your code, instead you should use the cache::make methods.
+     *
+     * @param cache_definition $definition
+     * @param cache_store $store
+     * @param cache_loader|cache_data_source $loader
+     */
+    public function __construct(cache_definition $definition, cache_store $store, $loader = null) {
+        parent::__construct($definition, $store, $loader);
+        $this->nativelocking = $this->store_supports_native_locking();
+        if ($definition->require_locking()) {
+            $this->requirelocking = true;
+            $this->requirelockingread = $definition->require_locking_read();
+            $this->requirelockingwrite = $definition->require_locking_write();
+        }
+
+        if ($definition->has_invalidation_events()) {
+            $lastinvalidation = $this->get('lastinvalidation');
+            if ($lastinvalidation === false) {
+                // This is a new session, there won't be anything to invalidate. Set the time of the last invalidation and
+                // move on.
+                $this->set('lastinvalidation', cache::now());
+                return;
+            } else if ($lastinvalidation == cache::now()) {
+                // We've already invalidated during this request.
+                return;
+            }
+
+            // Get the event invalidation cache.
+            $cache = cache::make('core', 'eventinvalidation');
+            $events = $cache->get_many($definition->get_invalidation_events());
+            $todelete = array();
+            // Iterate the returned data for the events.
+            foreach ($events as $event => $keys) {
+                // Look at each key and check the timestamp.
+                foreach ($keys as $key => $timestamp) {
+                    // If the timestamp of the event is more than or equal to the last invalidation (happened between the last
+                    // invalidation and now)then we need to invaliate the key.
+                    if ($timestamp >= $lastinvalidation) {
+                        $todelete[] = $key;
+                    }
+                }
+            }
+            if (!empty($todelete)) {
+                $todelete = array_unique($todelete);
+                $this->delete_many($todelete);
+            }
+            // Set the time of the last invalidation.
+            $this->set('lastinvalidation', cache::now());
+        }
+    }
+
+    /**
+     * Returns the identifier to use
+     *
+     * @staticvar int $instances Counts the number of instances. Used as part of the lock identifier.
+     * @return string
+     */
+    public function get_identifier() {
+        static $instances = 0;
+        if ($this->lockidentifier === null) {
+            $this->lockidentifier = md5(
+                $this->get_definition()->generate_definition_hash() .
+                sesskey() .
+                $instances++ .
+                'cache_application'
+            );
+        }
+        return $this->lockidentifier;
+    }
+
+    /**
+     * Fixes the instance up after a clone.
+     */
+    public function __clone() {
+        // Force a new idenfitier.
+        $this->lockidentifier = null;
+    }
+
+    /**
+     * Acquires a lock on the given key.
+     *
+     * This is done automatically if the definition requires it.
+     * It is recommended to use a definition if you want to have locking although it is possible to do locking without having
+     * it required by the definition.
+     * The problem with such an approach is that you cannot ensure that code will consistently use locking. You will need to
+     * rely on the integrators review skills.
+     *
+     * @param string|int $key The key as given to get|set|delete
+     * @return bool Returns true if the lock could be acquired, false otherwise.
+     */
+    public function acquire_lock($key) {
+        $key = $this->parse_key($key);
+        if ($this->nativelocking) {
+            return $this->get_store()->acquire_lock($key, $this->get_identifier());
+        } else {
+            $this->ensure_cachelock_available();
+            return $this->cachelockinstance->lock($key, $this->get_identifier());
+        }
+    }
+
+    /**
+     * Checks if this cache has a lock on the given key.
+     *
+     * @param string|int $key The key as given to get|set|delete
+     * @return bool|null Returns true if there is a lock and this cache has it, null if no one has a lock on that key, false if
+     *      someone else has the lock.
+     */
+    public function check_lock_state($key) {
+        $key = $this->parse_key($key);
+        if ($this->nativelocking) {
+            return $this->get_store()->check_lock_state($key, $this->get_identifier());
+        } else {
+            $this->ensure_cachelock_available();
+            return $this->cachelockinstance->check_state($key, $this->get_identifier());
+        }
+    }
+
+    /**
+     * Releases the lock this cache has on the given key
+     *
+     * @param string|int $key
+     * @return bool True if the operation succeeded, false otherwise.
+     */
+    public function release_lock($key) {
+        $key = $this->parse_key($key);
+        if ($this->nativelocking) {
+            return $this->get_store()->release_lock($key, $this->get_identifier());
+        } else {
+            $this->ensure_cachelock_available();
+            return $this->cachelockinstance->unlock($key, $this->get_identifier());
+        }
+    }
+
+    /**
+     * Ensure that the dedicated lock store is ready to go.
+     *
+     * This should only happen if the cache store doesn't natively support it.
+     */
+    protected function ensure_cachelock_available() {
+        if ($this->cachelockinstance === null) {
+            $this->cachelockinstance = cache_helper::get_cachelock_for_store($this->get_store());
+        }
+    }
+
+    /**
+     * Sends a key => value pair to the cache.
+     *
+     * <code>
+     * // This code will add four entries to the cache, one for each url.
+     * $cache->set('main', 'http://moodle.org');
+     * $cache->set('docs', 'http://docs.moodle.org');
+     * $cache->set('tracker', 'http://tracker.moodle.org');
+     * $cache->set('qa', 'http://qa.moodle.net');
+     * </code>
+     *
+     * @param string|int $key The key for the data being requested.
+     * @param mixed $data The data to set against the key.
+     * @return bool True on success, false otherwise.
+     */
+    public function set($key, $data) {
+        if ($this->requirelockingwrite && !$this->acquire_lock($key)) {
+            return false;
+        }
+        $result = parent::set($key, $data);
+        if ($this->requirelockingwrite && !$this->release_lock($key)) {
+            debugging('Failed to release cache lock on set operation... this should not happen.', DEBUG_DEVELOPER);
+        }
+        return $result;
+    }
+
+    /**
+     * Sends several key => value pairs to the cache.
+     *
+     * Using this function comes with potential performance implications.
+     * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call
+     * the equivalent singular method for each item provided.
+     * This should not deter you from using this function as there is a performance benefit in situations where the cache store
+     * does support it, but you should be aware of this fact.
+     *
+     * <code>
+     * // This code will add four entries to the cache, one for each url.
+     * $cache->set_many(array(
+     *     'main' => 'http://moodle.org',
+     *     'docs' => 'http://docs.moodle.org',
+     *     'tracker' => 'http://tracker.moodle.org',
+     *     'qa' => ''http://qa.moodle.net'
+     * ));
+     * </code>
+     *
+     * @param array $keyvaluearray An array of key => value pairs to send to the cache.
+     * @return int The number of items successfully set. It is up to the developer to check this matches the number of items.
+     *      ... if they care that is.
+     */
+    public function set_many(array $keyvaluearray) {
+        if ($this->requirelockingwrite) {
+            $locks = array();
+            foreach ($keyvaluearray as $id => $pair) {
+                $key = $pair['key'];
+                if ($this->acquire_lock($key)) {
+                    $locks[] = $key;
+                } else {
+                    unset($keyvaluearray[$id]);
+                }
+            }
+        }
+        $result = parent::set_many($keyvaluearray);
+        if ($this->requirelockingwrite) {
+            foreach ($locks as $key) {
+                if ($this->release_lock($key)) {
+                    debugging('Failed to release cache lock on set_many operation... this should not happen.', DEBUG_DEVELOPER);
+                }
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * Retrieves the value for the given key from the cache.
+     *
+     * @param string|int $key The key for the data being requested.
+     * @param int $strictness One of IGNORE_MISSING | MUST_EXIST
+     * @return mixed|false The data from the cache or false if the key did not exist within the cache.
+     * @throws moodle_exception
+     */
+    public function get($key, $strictness = IGNORE_MISSING) {
+        if ($this->requirelockingread && $this->check_lock_state($key) === false) {
+            // Read locking required and someone else has the read lock.
+            return false;
+        }
+        return parent::get($key, $strictness);
+    }
+
+    /**
+     * Retrieves an array of values for an array of keys.
+     *
+     * Using this function comes with potential performance implications.
+     * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call
+     * the equivalent singular method for each item provided.
+     * This should not deter you from using this function as there is a performance benefit in situations where the cache store
+     * does support it, but you should be aware of this fact.
+     *
+     * @param array $keys The keys of the data being requested.
+     * @param int $strictness One of IGNORE_MISSING or MUST_EXIST.
+     * @return array An array of key value pairs for the items that could be retrieved from the cache.
+     *      If MUST_EXIST was used and not all keys existed within the cache then an exception will be thrown.
+     *      Otherwise any key that did not exist will have a data value of false within the results.
+     * @throws moodle_exception
+     */
+    public function get_many(array $keys, $strictness = IGNORE_MISSING) {
+        if ($this->requirelockingread) {
+            foreach ($keys as $id => $key) {
+                $lock =$this->acquire_lock($key);
+                if (!$lock) {
+                    if ($strictness === MUST_EXIST) {
+                        throw new coding_exception('Could not acquire read locks for all of the items being requested.');
+                    } else {
+                        // Can't return this as we couldn't get a read lock.
+                        unset($keys[$id]);
+                    }
+                }
+
+            }
+        }
+        return parent::get_many($keys, $strictness);
+    }
+
+    /**
+     * Delete the given key from the cache.
+     *
+     * @param string|int $key The key to delete.
+     * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores.
+     *     This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this.
+     * @return bool True of success, false otherwise.
+     */
+    public function delete($key, $recurse = true) {
+        if ($this->requirelockingwrite && !$this->acquire_lock($key)) {
+            return false;
+        }
+        $result = parent::delete($key, $recurse);
+        if ($this->requirelockingwrite && !$this->release_lock($key)) {
+            debugging('Failed to release cache lock on delete operation... this should not happen.', DEBUG_DEVELOPER);
+        }
+        return $result;
+    }
+
+    /**
+     * Delete all of the given keys from the cache.
+     *
+     * @param array $keys The key to delete.
+     * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores.
+     *     This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this.
+     * @return int The number of items successfully deleted.
+     */
+    public function delete_many(array $keys, $recurse = true) {
+        if ($this->requirelockingwrite) {
+            $locks = array();
+            foreach ($keys as $id => $key) {
+                if ($this->acquire_lock($key)) {
+                    $locks[] = $key;
+                } else {
+                    unset($keys[$id]);
+                }
+            }
+        }
+        $result = parent::delete_many($keys, $recurse);
+        if ($this->requirelockingwrite) {
+            foreach ($locks as $key) {
+                if ($this->release_lock($key)) {
+                    debugging('Failed to release cache lock on delete_many operation... this should not happen.', DEBUG_DEVELOPER);
+                }
+            }
+        }
+        return $result;
+    }
+}
+
+/**
+ * A session cache.
+ *
+ * This class is used for session caches returned by the cache::make methods.
+ *
+ * This cache class should never be interacted with directly. Instead you should always use the cache::make methods.
+ * It is technically possible to call those methods through this class however there is no guarantee that you will get an
+ * instance of this class back again.
+ *
+ * @todo we should support locking in the session as well. Should be pretty simple to set up.
+ *
+ * @internal don't use me directly.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cache_session extends cache {
+    /**
+     * Override the cache::construct method.
+     *
+     * This function gets overriden so that we can process any invalidation events if need be.
+     * If the definition doesn't have any invalidation events then this occurs exactly as it would for the cache class.
+     * Otherwise we look at the last invalidation time and then check the invalidation data for events that have occured
+     * between then now.
+     *
+     * You should not call this method from your code, instead you should use the cache::make methods.
+     *
+     * @param cache_definition $definition
+     * @param cache_store $store
+     * @param cache_loader|cache_data_source $loader
+     * @return void
+     */
+    public function __construct(cache_definition $definition, cache_store $store, $loader = null) {
+        parent::__construct($definition, $store, $loader);
+        if ($definition->has_invalidation_events()) {
+            $lastinvalidation = $this->get('lastsessioninvalidation');
+            if ($lastinvalidation === false) {
+                // This is a new session, there won't be anything to invalidate. Set the time of the last invalidation and
+                // move on.
+                $this->set('lastsessioninvalidation', cache::now());
+                return;
+            } else if ($lastinvalidation == cache::now()) {
+                // We've already invalidated during this request.
+                return;
+            }
+
+            // Get the event invalidation cache.
+            $cache = cache::make('core', 'eventinvalidation');
+            $events = $cache->get_many($definition->get_invalidation_events());
+            $todelete = array();
+            // Iterate the returned data for the events.
+            foreach ($events as $event => $keys) {
+                if ($keys === false) {
+                    // No data to be invalidated yet.
+                    continue;
+                }
+                // Look at each key and check the timestamp.
+                foreach ($keys as $key => $timestamp) {
+                    // If the timestamp of the event is more than or equal to the last invalidation (happened between the last
+                    // invalidation and now)then we need to invaliate the key.
+                    if ($timestamp >= $lastinvalidation) {
+                        $todelete[] = $key;
+                    }
+                }
+            }
+            if (!empty($todelete)) {
+                $todelete = array_unique($todelete);
+                $this->delete_many($todelete);
+            }
+            // Set the time of the last invalidation.
+            $this->set('lastsessioninvalidation', cache::now());
+        }
+    }
+}
+
+/**
+ * An request cache.
+ *
+ * This class is used for request caches returned by the cache::make methods.
+ *
+ * This cache class should never be interacted with directly. Instead you should always use the cache::make methods.
+ * It is technically possible to call those methods through this class however there is no guarantee that you will get an
+ * instance of this class back again.
+ *
+ * @internal don't use me directly.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cache_request extends cache {
+    // This comment appeases code pre-checker ;) !
+}
\ No newline at end of file
diff --git a/cache/forms.php b/cache/forms.php
new file mode 100644 (file)
index 0000000..533f1f8
--- /dev/null
@@ -0,0 +1,211 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Forms used for the administration and managemement of the cache setup.
+ *
+ * This file is part of Moodle's cache API, affectionately called MUC.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot.'/lib/formslib.php');
+
+/**
+ * Add store instance form.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cachestore_addinstance_form extends moodleform {
+
+    /**
+     * The definition of the add instance form
+     */
+    protected final function definition() {
+        $form = $this->_form;
+        $store = $this->_customdata['store'];
+        $plugin = $this->_customdata['plugin'];
+        $locks = $this->_customdata['locks'];
+
+        $form->addElement('hidden', 'plugin', $plugin);
+        $form->addElement('hidden', 'editing', !empty($this->_customdata['store']));
+
+        if (!$store) {
+            $form->addElement('text', 'name', get_string('storename', 'cache'));
+            $form->addHelpButton('name', 'storename', 'cache');
+            $form->addRule('name', get_string('required'), 'required');
+            $form->setType('name', PARAM_TEXT);
+        } else {
+            $form->addElement('hidden', 'name', $store);
+            $form->addElement('static', 'name-value', get_string('storename', 'cache'), $store);
+        }
+
+        if (is_array($locks)) {
+            $form->addElement('select', 'lock', get_string('lockmethod', 'cache'), $locks);
+            $form->addHelpButton('lock', 'lockmethod', 'cache');
+            $form->setType('lock', PARAM_TEXT);
+        } else {
+            $form->addElement('hidden', 'lock', '');
+            $form->addElement('static', 'lock-value', get_string('lockmethod', 'cache'),
+                    '<em>'.get_string('nativelocking', 'cache').'</em>');
+        }
+
+        if (method_exists($this, 'configuration_definition')) {
+            $form->addElement('header', 'storeconfiguration', get_string('storeconfiguration', 'cache'));
+            $this->configuration_definition();
+        }
+
+        $this->add_action_buttons();
+    }
+
+    /**
+     * Validates the add instance form data
+     *
+     * @param array $data
+     * @param array $files
+     * @return array
+     */
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+
+        if (!array_key_exists('name', $errors)) {
+            if (!preg_match('#^[a-zA-Z0-9\-_ ]+$#', $data['name'])) {
+                $errors['name'] = get_string('storenameinvalid', 'cache');
+            } else if (empty($this->_customdata['store'])) {
+                $stores = cache_administration_helper::get_store_instance_summaries();
+                if (array_key_exists($data['name'], $stores)) {
+                    $errors['name'] = get_string('storenamealreadyused', 'cache');
+                }
+            }
+        }
+
+        if (method_exists($this, 'configuration_validation')) {
+            $errors = $this->configuration_validation($data, $files);
+        }
+
+        return $errors;
+    }
+}
+
+/**
+ * Form to set definition mappings
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cache_definition_mappings_form extends moodleform {
+
+    /**
+     * The definition of the form
+     */
+    protected final function definition() {
+        $definition = $this->_customdata['definition'];
+        $form = $this->_form;
+
+        list($component, $area) = explode('/', $definition, 2);
+        list($currentstores, $storeoptions, $defaults) =
+                cache_administration_helper::get_definition_store_options($component, $area);
+
+        $form->addElement('hidden', 'definition', $definition);
+        $form->addElement('hidden', 'action', 'editdefinitionmapping');
+
+        $requiredoptions = max(3, count($currentstores)+1);
+        $requiredoptions = min($requiredoptions, count($storeoptions));
+
+        $options = array('' => get_string('none'));
+        foreach ($storeoptions as $option => $def) {
+            $options[$option] = $option;
+            if ($def['default']) {
+                $options[$option] .= ' '.get_string('mappingdefault', 'cache');
+            }
+        }
+
+        for ($i = 0; $i < $requiredoptions; $i++) {
+            $title = '...';
+            if ($i === 0) {
+                $title = get_string('mappingprimary', 'cache');
+            } else if ($i === $requiredoptions-1) {
+                $title = get_string('mappingfinal', 'cache');
+            }
+            $form->addElement('select', 'mappings['.$i.']', $title, $options);
+        }
+        $i = 0;
+        foreach ($currentstores as $store => $def) {
+            $form->setDefault('mappings['.$i.']', $store);
+            $i++;
+        }
+
+        if (!empty($defaults)) {
+            $form->addElement('static', 'defaults', get_string('defaultmappings', 'cache'),
+                    html_writer::tag('strong', join(', ', $defaults)));
+            $form->addHelpButton('defaults', 'defaultmappings', 'cache');
+        }
+
+        $this->add_action_buttons();
+    }
+}
+
+/**
+ * Form to set the mappings for a mode.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cache_mode_mappings_form extends moodleform {
+    /**
+     * The definition of the form
+     */
+    protected function definition() {
+        $form = $this->_form;
+        $stores = $this->_customdata;
+
+        $options = array(
+            cache_store::MODE_APPLICATION => array(),
+            cache_store::MODE_SESSION => array(),
+            cache_store::MODE_REQUEST => array()
+        );
+        foreach ($stores as $storename => $store) {
+            foreach ($store['modes'] as $mode => $enabled) {
+                if ($enabled) {
+                    if (empty($store['default'])) {
+                        $options[$mode][$storename] = $store['name'];
+                    } else {
+                        $options[$mode][$storename] = get_string('store_'.$store['name'], 'cache');
+                    }
+                }
+            }
+        }
+
+        $form->addElement('hidden', 'action', 'editmodemappings');
+        foreach ($options as $mode => $optionset) {
+            $form->addElement('select', 'mode_'.$mode, get_string('mode_'.$mode, 'cache'), $optionset);
+        }
+
+        $this->add_action_buttons();
+    }
+}
\ No newline at end of file
diff --git a/cache/lib.php b/cache/lib.php
new file mode 100644 (file)
index 0000000..b5e23ad
--- /dev/null
@@ -0,0 +1,150 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The core cache API.
+ *
+ * Pretty much just includes the mandatory classes and contains the misc classes that arn't worth separating into individual files.
+ *
+ * This file is part of Moodle's cache API, affectionately called MUC.
+ * It contains the components that are requried in order to use caching.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+// Include the required classes.
+require_once($CFG->dirroot.'/cache/classes/interfaces.php');
+require_once($CFG->dirroot.'/cache/classes/config.php');
+require_once($CFG->dirroot.'/cache/classes/helper.php');
+require_once($CFG->dirroot.'/cache/classes/factory.php');
+require_once($CFG->dirroot.'/cache/classes/loaders.php');
+require_once($CFG->dirroot.'/cache/classes/definition.php');
+
+/**
+ * A cached object wrapper.
+ *
+ * This class gets used when the data is an object that has implemented the cacheable_object interface.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cache_cached_object {
+
+    /**
+     * The class of the cacheable object
+     * @var string
+     */
+    protected $class;
+
+    /**
+     * The data returned by the cacheable_object prepare_to_cache method.
+     * @var mixed
+     */
+    protected $data;
+
+    /**
+     * Constructs a cached object wrapper.
+     * @param cacheable_object $obj
+     */
+    public function __construct(cacheable_object $obj) {
+        $this->class = get_class($obj);
+        $this->data = $obj->prepare_to_cache();
+    }
+
+    /**
+     * Restores the data as an instance of the cacheable_object class.
+     * @return object
+     */
+    public function restore_object() {
+        $class = $this->class;
+        return $class::wake_from_cache($this->data);
+    }
+}
+
+/**
+ * A wrapper class used to handle ttl when the cache store doesn't natively support it.
+ *
+ * This class is exactly why you should use event driving invalidation of cache data rather than relying on ttl.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cache_ttl_wrapper {
+
+    /**
+     * The data being stored.
+     * @var mixed
+     */
+    public $data;
+
+    /**
+     * When the cache data expires as a timestamp.
+     * @var int
+     */
+    public $expires;
+
+    /**
+     * Constructs a ttl cache wrapper.
+     *
+     * @param mixed $data
+     * @param int $ttl The time to live in seconds.
+     */
+    public function __construct($data, $ttl) {
+        $this->data = $data;
+        $this->expires = cache::now() + (int)$ttl;
+    }
+
+    /**
+     * Returns true if the data has expired.
+     * @return int
+     */
+    public function has_expired() {
+        return ($this->expires < cache::now());
+    }
+}
+
+/**
+ * A cache exception class. Just allows people to catch cache exceptions.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cache_exception extends moodle_exception {
+    /**
+     * Constructs a new exception
+     *
+     * @param string $errorcode
+     * @param string $module
+     * @param string $link
+     * @param mixed $a
+     * @param mixed $debuginfo
+     */
+    public function __construct($errorcode, $module = 'cache', $link = '', $a = null, $debuginfo = null) {
+        // This may appear like a useless override but you will notice that we have set a MUCH more useful default for $module.
+        parent::__construct($errorcode, $module, $link, $a, $debuginfo);
+    }
+}
\ No newline at end of file
diff --git a/cache/locallib.php b/cache/locallib.php
new file mode 100644 (file)
index 0000000..2bc3797
--- /dev/null
@@ -0,0 +1,942 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The supplementary cache API.
+ *
+ * This file is part of Moodle's cache API, affectionately called MUC.
+ * It contains elements of the API that are not required in order to use caching.
+ * Things in here are more in line with administration and management of the cache setup and configuration.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cache configuration writer.
+ *
+ * This class should only be used when you need to write to the config, all read operations exist within the cache_config.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cache_config_writer extends cache_config {
+
+    /**
+     * Returns an instance of the configuration writer.
+     *
+     * @return cache_config_writer
+     */
+    public static function instance() {
+        $factory = cache_factory::instance();
+        return $factory->create_config_instance(true);
+    }
+
+    /**
+     * Saves the current configuration.
+     */
+    protected function config_save() {
+        global $CFG;
+        $cachefile = self::get_config_file_path();
+        $directory = dirname($cachefile);
+        if ($directory !== $CFG->dataroot && !file_exists($directory)) {
+            $result = make_writable_directory($directory, false);
+            if (!$result) {
+                throw new cache_exception('ex_configcannotsave', 'cache', '', null, 'Cannot create config directory.');
+            }
+        }
+        if (!file_exists($directory) || !is_writable($directory)) {
+            throw new cache_exception('ex_configcannotsave', 'cache', '', null, 'Config directory is not writable.');
+        }
+
+        // Prepare a configuration array to store.
+        $configuration = array();
+        $configuration['stores'] = $this->configstores;
+        $configuration['modemappings'] = $this->configmodemappings;
+        $configuration['definitions'] = $this->configdefinitions;
+        $configuration['definitionmappings'] = $this->configdefinitionmappings;
+        $configuration['locks'] = $this->configlocks;
+
+        // Prepare the file content.
+        $content = "<?php defined('MOODLE_INTERNAL') || die();\n \$configuration = ".var_export($configuration, true).";";
+
+        // We need to create a temporary cache lock instance for use here. Remember we are generating the config file
+        // it doesn't exist and thus we can't use the normal API for this (it'll just try to use config).
+        $lockconf = reset($this->configlocks);
+        if ($lockconf === false) {
+            debugging('Your cache configuration file is out of date and needs to be refreshed.', DEBUG_DEVELOPER);
+            // Use the default
+            $lockconf = array(
+                'name' => 'cachelock_file_default',
+                'type' => 'cachelock_file',
+                'dir' => 'filelocks',
+                'default' => true
+            );
+        }
+        $factory = cache_factory::instance();
+        $locking = $factory->create_lock_instance($lockconf);
+        if ($locking->lock('configwrite', 'config', true)) {
+            // Its safe to use w mode here because we have already acquired the lock.
+            $handle = fopen($cachefile, 'w');
+            fwrite($handle, $content);
+            fflush($handle);
+            fclose($handle);
+            $locking->unlock('configwrite', 'config');
+        } else {
+            throw new cache_exception('ex_configcannotsave', 'cache', '', null, 'Unable to open the cache config file.');
+        }
+    }
+
+    /**
+     * Adds a plugin instance.
+     *
+     * This function also calls save so you should redirect immediately, or at least very shortly after
+     * calling this method.
+     *
+     * @param string $name The name for the instance (must be unique)
+     * @param string $plugin The name of the plugin.
+     * @param array $configuration The configuration data for the plugin instance.
+     * @return bool
+     * @throws cache_exception
+     */
+    public function add_store_instance($name, $plugin, array $configuration = array()) {
+        if (array_key_exists($name, $this->configstores)) {
+            throw new cache_exception('Duplicate name specificed for cache plugin instance. You must provide a unique name.');
+        }
+        $class = 'cachestore_'.$plugin;
+        if (!class_exists($class)) {
+            $plugins = get_plugin_list_with_file('cachestore', 'lib.php');
+            if (!array_key_exists($plugin, $plugins)) {
+                throw new cache_exception('Invalid plugin name specified. The plugin does not exist or is not valid.');
+            }
+            $file = $plugins[$plugin];
+            if (file_exists($file)) {
+                require_once($file);
+            }
+            if (!class_exists($class)) {
+                throw new cache_exception('Invalid cache plugin specified. The plugin does not contain the required class.');
+            }
+        }
+        if (!is_subclass_of($class, 'cache_store')) {
+            throw new cache_exception('Invalid cache plugin specified. The plugin does not extend the required class.');
+        }
+        if (!$class::are_requirements_met()) {
+            throw new cache_exception('Unable to add new cache plugin instance. The requested plugin type is not supported.');
+        }
+        $this->configstores[$name] = array(
+            'name' => $name,
+            'plugin' => $plugin,
+            'configuration' => $configuration,
+            'features' => $class::get_supported_features($configuration),
+            'modes' => $class::get_supported_modes($configuration),
+            'mappingsonly' => !empty($configuration['mappingsonly']),
+            'class' => $class,
+            'default' => false
+        );
+        if (array_key_exists('lock', $configuration)) {
+            $this->configstores[$name]['lock'] = $configuration['lock'];
+            unset($this->configstores[$name]['configuration']['lock']);
+        }
+        $this->config_save();
+        return true;
+    }
+
+    /**
+     * Sets the mode mappings.
+     *
+     * These determine the default caches for the different modes.
+     * This function also calls save so you should redirect immediately, or at least very shortly after
+     * calling this method.
+     *
+     * @param array $modemappings
+     * @return bool
+     * @throws cache_exception
+     */
+    public function set_mode_mappings(array $modemappings) {
+        $mappings = array(
+            cache_store::MODE_APPLICATION => array(),
+            cache_store::MODE_SESSION => array(),
+            cache_store::MODE_REQUEST => array(),
+        );
+        foreach ($modemappings as $mode => $stores) {
+            if (!array_key_exists($mode, $mappings)) {
+                throw new cache_exception('The cache mode for the new mapping does not exist');
+            }
+            $sort = 0;
+            foreach ($stores as $store) {
+                if (!array_key_exists($store, $this->configstores)) {
+                    throw new cache_exception('The instance name for the new mapping does not exist');
+                }
+                if (array_key_exists($store, $mappings[$mode])) {
+                    throw new cache_exception('This cache mapping already exists');
+                }
+                $mappings[$mode][] = array(
+                    'store' => $store,
+                    'mode' => $mode,
+                    'sort' => $sort++
+                );
+            }
+        }
+        $this->configmodemappings = array_merge(
+            $mappings[cache_store::MODE_APPLICATION],
+            $mappings[cache_store::MODE_SESSION],
+            $mappings[cache_store::MODE_REQUEST]
+        );
+
+        $this->config_save();
+        return true;
+    }
+
+    /**
+     * Edits a give plugin instance.
+     *
+     * The plugin instance is determined by its name, hence you cannot rename plugins.
+     * This function also calls save so you should redirect immediately, or at least very shortly after
+     * calling this method.
+     *
+     * @param string $name
+     * @param string $plugin
+     * @param array $configuration
+     * @return bool
+     * @throws cache_exception
+     */
+    public function edit_store_instance($name, $plugin, $configuration) {
+        if (!array_key_exists($name, $this->configstores)) {
+            throw new cache_exception('The requested instance does not exist.');
+        }
+        $plugins = get_plugin_list_with_file('cachestore', 'lib.php');
+        if (!array_key_exists($plugin, $plugins)) {
+            throw new cache_exception('Invalid plugin name specified. The plugin either does not exist or is not valid.');
+        }
+        $class = 'cachestore_'.$plugin;
+        $file = $plugins[$plugin];
+        if (!class_exists($class)) {
+            if (file_exists($file)) {
+                require_once($file);
+            }
+            if (!class_exists($class)) {
+                throw new cache_exception('Invalid cache plugin specified. The plugin does not contain the required class.'.$class);
+            }
+        }
+        $this->configstores[$name] = array(
+            'name' => $name,
+            'plugin' => $plugin,
+            'configuration' => $configuration,
+            'features' => $class::get_supported_features($configuration),
+            'modes' => $class::get_supported_modes($configuration),
+            'mappingsonly' => !empty($configuration['mappingsonly']),
+            'class' => $class,
+            'default' => $this->configstores[$name]['default'] // Can't change the default.
+        );
+        if (array_key_exists('lock', $configuration)) {
+            $this->configstores[$name]['lock'] = $configuration['lock'];
+            unset($this->configstores[$name]['configuration']['lock']);
+        }
+        $this->config_save();
+        return true;
+    }
+
+    /**
+     * Deletes a store instance.
+     *
+     * This function also calls save so you should redirect immediately, or at least very shortly after
+     * calling this method.
+     *
+     * @param string $name The name of the instance to delete.
+     * @return bool
+     * @throws cache_exception
+     */
+    public function delete_store_instance($name) {
+        if (!array_key_exists($name, $this->configstores)) {
+            throw new cache_exception('The requested store does not exist.');
+        }
+        if ($this->configstores[$name]['default']) {
+            throw new cache_exception('The can not delete the default stores.');
+        }
+        foreach ($this->configmodemappings as $mapping) {
+            if ($mapping['store'] === $name) {
+                throw new cache_exception('You cannot delete a cache store that has mode mappings.');
+            }
+        }
+        foreach ($this->configdefinitionmappings as $mapping) {
+            if ($mapping['store'] === $name) {
+                throw new cache_exception('You cannot delete a cache store that has definition mappings.');
+            }
+        }
+        unset($this->configstores[$name]);
+        $this->config_save();
+        return true;
+    }
+
+    /**
+     * Creates the default configuration and saves it.
+     *
+     * This function calls config_save, however it is safe to continue using it afterwards as this function should only ever
+     * be called when there is no configuration file already.
+     */
+    public static function create_default_configuration() {
+        global $CFG;
+
+        // HACK ALERT.
+        // We probably need to come up with a better way to create the default stores, or at least ensure 100% that the
+        // default store plugins are protected from deletion.
+        require_once($CFG->dirroot.'/cache/stores/file/lib.php');
+        require_once($CFG->dirroot.'/cache/stores/session/lib.php');
+        require_once($CFG->dirroot.'/cache/stores/static/lib.php');
+
+        $writer = new self;
+        $writer->configstores = array(
+            'default_application' => array(
+                'name' => 'default_application',
+                'plugin' => 'file',
+                'configuration' => array(),
+                'features' => cachestore_file::get_supported_features(),
+                'modes' => cache_store::MODE_APPLICATION,
+                'default' => true,
+            ),
+            'default_session' => array(
+                'name' => 'default_session',
+                'plugin' => 'session',
+                'configuration' => array(),
+                'features' => cachestore_session::get_supported_features(),
+                'modes' => cache_store::MODE_SESSION,
+                'default' => true,
+            ),
+            'default_request' => array(
+                'name' => 'default_request',
+                'plugin' => 'static',
+                'configuration' => array(),
+                'features' => cachestore_static::get_supported_features(),
+                'modes' => cache_store::MODE_REQUEST,
+                'default' => true,
+            )
+        );
+        $writer->configdefinitions = self::locate_definitions();
+        $writer->configmodemappings = array(
+            array(
+                'mode' => cache_store::MODE_APPLICATION,
+                'store' => 'default_application',
+                'sort' => -1
+            ),
+            array(
+                'mode' => cache_store::MODE_SESSION,
+                'store' => 'default_session',
+                'sort' => -1
+            ),
+            array(
+                'mode' => cache_store::MODE_REQUEST,
+                'store' => 'default_request',
+                'sort' => -1
+            )
+        );
+        $writer->configlocks = array(
+            'default_file_lock' => array(
+                'name' => 'cachelock_file_default',
+                'type' => 'cachelock_file',
+                'dir' => 'filelocks',
+                'default' => true
+            )
+        );
+        $writer->config_save();
+    }
+
+    /**
+     * Updates the definition in the configuration from those found in the cache files.
+     *
+     * Calls config_save further down, you should redirect immediately or asap after calling this method.
+     */
+    public static function update_definitions() {
+        $config = self::instance();
+        $config->write_definitions_to_cache(self::locate_definitions());
+    }
+
+    /**
+     * Locates all of the definition files.
+     *
+     * @return array
+     */
+    protected static function locate_definitions() {
+        global $CFG;
+
+        $files = array();
+        if (file_exists($CFG->dirroot.'/lib/db/caches.php')) {
+            $files['core'] = $CFG->dirroot.'/lib/db/caches.php';
+        }
+
+        $plugintypes = get_plugin_types();
+        foreach ($plugintypes as $type => $location) {
+            $plugins = get_plugin_list_with_file($type, 'db/caches.php');
+            foreach ($plugins as $plugin => $filepath) {
+                $component = clean_param($type.'_'.$plugin, PARAM_COMPONENT); // Standardised plugin name.
+                $files[$component] = $filepath;
+            }
+        }
+
+        $definitions = array();
+        foreach ($files as $component => $file) {
+            $filedefs = self::load_caches_file($file);
+            foreach ($filedefs as $area => $definition) {
+                $area = clean_param($area, PARAM_AREA);
+                $id = $component.'/'.$area;
+                $definition['component'] = $component;
+                $definition['area'] = $area;
+                if (array_key_exists($id, $definitions)) {
+                    debugging('Error: duplicate cache definition found with name '.$name, DEBUG_DEVELOPER);
+                    continue;
+                }
+                $definitions[$id] = $definition;
+            }
+        }
+
+        return $definitions;
+    }
+
+    /**
+     * Writes the updated definitions for the config file.
+     * @param array $definitions
+     */
+    private function write_definitions_to_cache(array $definitions) {
+        $this->configdefinitions = $definitions;
+        foreach ($this->configdefinitionmappings as $key => $mapping) {
+            if (!array_key_exists($mapping['definition'], $definitions)) {
+                unset($this->configdefinitionmappings[$key]);
+            }
+        }
+        $this->config_save();
+    }
+
+    /**
+     * Loads the caches file if it exists.
+     * @param string $file Absolute path to the file.
+     * @return array
+     */
+    private static function load_caches_file($file) {
+        if (!file_exists($file)) {
+            return array();
+        }
+        $definitions = array();
+        include($file);
+        return $definitions;
+    }
+
+    /**
+     * Sets the mappings for a given definition.
+     *
+     * @param string $definition
+     * @param array $mappings
+     * @throws coding_exception
+     */
+    public function set_definition_mappings($definition, $mappings) {
+        if (!array_key_exists($definition, $this->configdefinitions)) {
+            throw new coding_exception('Invalid definition name passed when updating mappings.');
+        }
+        foreach ($mappings as $store) {
+            if (!array_key_exists($store, $this->configstores)) {
+                throw new coding_exception('Invalid store name passed when updating definition mappings.');
+            }
+        }
+        foreach ($this->configdefinitionmappings as $key => $mapping) {
+            if ($mapping['definition'] == $definition) {
+                unset($this->configdefinitionmappings[$key]);
+            }
+        }
+        $sort = count($mappings);
+        foreach ($mappings as $store) {
+            $this->configdefinitionmappings[] = array(
+                'store' => $store,
+                'definition' => $definition,
+                'sort' => $sort
+            );
+            $sort--;
+        }
+
+        $this->config_save();
+    }
+
+}
+
+/**
+ * A cache helper for administration tasks
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class cache_administration_helper extends cache_helper {
+
+    /**
+     * Returns an array containing all of the information about stores a renderer needs.
+     * @return array
+     */
+    public static function get_store_instance_summaries() {
+        $return = array();
+        $default = array();
+        $instance = cache_config::instance();
+        $stores = $instance->get_all_stores();
+        foreach ($stores as $name => $details) {
+            $class = $details['class'];
+            $store = new $class($details['name'], $details['configuration']);
+            $record = array(
+                'name' => $name,
+                'plugin' => $details['plugin'],
+                'default' => $details['default'],
+                'isready' => $store->is_ready(),
+                'requirementsmet' => $store->are_requirements_met(),
+                'mappings' => 0,
+                'modes' => array(
+                    cache_store::MODE_APPLICATION =>
+                        ($store->get_supported_modes($return) & cache_store::MODE_APPLICATION) == cache_store::MODE_APPLICATION,
+                    cache_store::MODE_SESSION =>
+                        ($store->get_supported_modes($return) & cache_store::MODE_SESSION) == cache_store::MODE_SESSION,
+                    cache_store::MODE_REQUEST =>
+                        ($store->get_supported_modes($return) & cache_store::MODE_REQUEST) == cache_store::MODE_REQUEST,
+                ),
+                'supports' => array(
+                    'multipleidentifiers' => $store->supports_multiple_indentifiers(),
+                    'dataguarantee' => $store->supports_data_guarantee(),
+                    'nativettl' => $store->supports_native_ttl(),
+                    'nativelocking' => ($store instanceof cache_is_lockable),
+                    'keyawareness' => ($store instanceof cache_is_key_aware),
+                )
+            );
+            if (empty($details['default'])) {
+                $return[$name] = $record;
+            } else {
+                $default[$name] = $record;
+            }
+        }
+
+        ksort($return);
+        ksort($default);
+        $return = $return + $default;
+
+        foreach ($instance->get_definition_mappings() as $mapping) {
+            if (!array_key_exists($mapping['store'], $return)) {
+                continue;
+            }
+            $return[$mapping['store']]['mappings']++;
+        }
+
+        return $return;
+    }
+
+    /**
+     * Returns an array of information about plugins, everything a renderer needs.
+     * @return array
+     */
+    public static function get_store_plugin_summaries() {
+        $return = array();
+        $plugins = get_plugin_list_with_file('cachestore', 'lib.php', true);
+        foreach ($plugins as $plugin => $path) {
+            $class = 'cachestore_'.$plugin;
+            $return[$plugin] = array(
+                'name' => get_string('pluginname', 'cachestore_'.$plugin),
+                'requirementsmet' => $class::are_requirements_met(),
+                'instances' => 0,
+                'modes' => array(
+                    cache_store::MODE_APPLICATION => ($class::get_supported_modes() & cache_store::MODE_APPLICATION),
+                    cache_store::MODE_SESSION => ($class::get_supported_modes() & cache_store::MODE_SESSION),
+                    cache_store::MODE_REQUEST => ($class::get_supported_modes() & cache_store::MODE_REQUEST),
+                ),
+                'supports' => array(
+                    'multipleidentifiers' => ($class::get_supported_features() & cache_store::SUPPORTS_MULTIPLE_IDENTIFIERS),
+                    'dataguarantee' => ($class::get_supported_features() & cache_store::SUPPORTS_DATA_GUARANTEE),
+                    'nativettl' => ($class::get_supported_features() & cache_store::SUPPORTS_NATIVE_TTL),
+                    'nativelocking' => (in_array('cache_is_lockable', class_implements($class))),
+                    'keyawareness' => (array_key_exists('cache_is_key_aware', class_implements($class))),
+                ),
+                'canaddinstance' => ($class::can_add_instance())
+            );
+        }
+
+        $instance = cache_config::instance();
+        $stores = $instance->get_all_stores();
+        foreach ($stores as $store) {
+            $plugin = $store['plugin'];
+            if (array_key_exists($plugin, $return)) {
+                $return[$plugin]['instances']++;
+            }
+        }
+
+        return $return;
+    }
+
+    /**
+     * Returns an array about the definitions. All the information a renderer needs.
+     * @return array
+     */
+    public static function get_definition_summaries() {
+        $instance = cache_config::instance();
+        $definitions = $instance->get_definitions();
+
+        $storenames = array();
+        foreach ($instance->get_all_stores() as $key => $store) {
+            if (!empty($store['default'])) {
+                $storenames[$key] = new lang_string('store_'.$key, 'cache');
+            }
+        }
+
+        $modemappings = array();
+        foreach ($instance->get_mode_mappings() as $mapping) {
+            $mode = $mapping['mode'];
+            if (!array_key_exists($mode, $modemappings)) {
+                $modemappings[$mode] = array();
+            }
+            if (array_key_exists($mapping['store'], $storenames)) {
+                $modemappings[$mode][] = $storenames[$mapping['store']];
+            } else {
+                $modemappings[$mode][] = $mapping['store'];
+            }
+        }
+
+        $definitionmappings = array();
+        foreach ($instance->get_definition_mappings() as $mapping) {
+            $definition = $mapping['definition'];
+            if (!array_key_exists($definition, $definitionmappings)) {
+                $definitionmappings[$definition] = array();
+            }
+            if (array_key_exists($mapping['store'], $storenames)) {
+                $definitionmappings[$definition][] = $storenames[$mapping['store']];
+            } else {
+                $definitionmappings[$definition][] = $mapping['store'];
+            }
+        }
+
+        $return = array();
+
+        foreach ($definitions as $id => $definition) {
+
+            $mappings = array();
+            if (array_key_exists($id, $definitionmappings)) {
+                $mappings = $definitionmappings[$id];
+            } else if (empty($definition['mappingsonly'])) {
+                $mappings = $modemappings[$definition['mode']];
+            }
+
+            $return[$id] = array(
+                'id' => $id,
+                'name' => cache_helper::get_definition_name($definition),
+                'mode' => $definition['mode'],
+                'component' => $definition['component'],
+                'area' => $definition['area'],
+                'mappings' => $mappings
+            );
+        }
+        return $return;
+    }
+
+    /**
+     * Returns all of the actions that can be performed on a definition.
+     * @param context $context
+     * @return array
+     */
+    public static function get_definition_actions(context $context) {
+        if (has_capability('moodle/site:config', $context)) {
+            return array(
+                array(
+                    'text' => get_string('editmappings', 'cache'),
+                    'url' => new moodle_url('/cache/admin.php', array('action' => 'editdefinitionmapping', 'sesskey' => sesskey()))
+                )
+            );
+        }
+        return array();
+    }
+
+    /**
+     * Returns all of the actions that can be performed on a store.
+     *
+     * @param string $name The name of the store
+     * @param array $storedetails
+     * @return array
+     */
+    public static function get_store_instance_actions($name, array $storedetails) {
+        $actions = array();
+        if (has_capability('moodle/site:config', get_system_context())) {
+            $baseurl = new moodle_url('/cache/admin.php', array('store' => $name, 'sesskey' => sesskey()));
+            if (empty($storedetails['default'])) {
+                $actions[] = array(
+                    'text' => get_string('editstore', 'cache'),
+                    'url' => new moodle_url($baseurl, array('action' => 'editstore', 'plugin' => $storedetails['plugin']))
+                );
+                $actions[] = array(
+                    'text' => get_string('deletestore', 'cache'),
+                    'url' => new moodle_url($baseurl, array('action' => 'deletestore'))
+                );
+            }
+            $actions[] = array(
+                'text' => get_string('purge', 'cache'),
+                'url' => new moodle_url($baseurl, array('action' => 'purge'))
+            );
+        }
+        return $actions;
+    }
+
+
+    /**
+     * Returns all of the actions that can be performed on a plugin.
+     *
+     * @param string $name The name of the plugin
+     * @param array $plugindetails
+     * @return array
+     */
+    public static function get_store_plugin_actions($name, array $plugindetails) {
+        $actions = array();
+        if (has_capability('moodle/site:config', get_system_context())) {
+            if (!empty($plugindetails['canaddinstance'])) {
+                $url = new moodle_url('/cache/admin.php', array('action' => 'addstore', 'plugin' => $name, 'sesskey' => sesskey()));
+                $actions[] = array(
+                    'text' => get_string('addinstance', 'cache'),
+                    'url' => $url
+                );
+            }
+        }
+        return $actions;
+    }
+
+    /**
+     * Returns a form that can be used to add a store instance.
+     *
+     * @param string $plugin The plugin to add an instance of
+     * @return cachestore_addinstance_form
+     * @throws coding_exception
+     */
+    public static function get_add_store_form($plugin) {
+        global $CFG; // Needed for includes.
+        $plugins = get_plugin_list('cachestore');
+        if (!array_key_exists($plugin, $plugins)) {
+            throw new coding_exception('Invalid cache plugin used when trying to create an edit form.');
+        }
+        $plugindir = $plugins[$plugin];
+        $class = 'cachestore_addinstance_form';
+        if (file_exists($plugindir.'/addinstanceform.php')) {
+            require_once($plugindir.'/addinstanceform.php');
+            if (class_exists('cachestore_'.$plugin.'_addinstance_form')) {
+                $class = 'cachestore_'.$plugin.'_addinstance_form';
+                if (!array_key_exists('cachestore_addinstance_form', class_parents($class))) {
+                    throw new coding_exception('Cache plugin add instance forms must extend cachestore_addinstance_form');
+                }
+            }
+        }
+
+        $locks = self::get_possible_locks_for_stores($plugindir, $plugin);
+
+        $url = new moodle_url('/cache/admin.php', array('action' => 'addstore'));
+        return new $class($url, array('plugin' => $plugin, 'store' => null, 'locks' => $locks));
+    }
+
+    /**
+     * Returns a form that can be used to edit a store instance.
+     *
+     * @param string $plugin
+     * @param string $store
+     * @return cachestore_addinstance_form
+     * @throws coding_exception
+     */
+    public static function get_edit_store_form($plugin, $store) {
+        global $CFG; // Needed for includes.
+        $plugins = get_plugin_list('cachestore');
+        if (!array_key_exists($plugin, $plugins)) {
+            throw new coding_exception('Invalid cache plugin used when trying to create an edit form.');
+        }
+        $factory = cache_factory::instance();
+        $config = $factory->create_config_instance();
+        $stores = $config->get_all_stores();
+        if (!array_key_exists($store, $stores)) {
+            throw new coding_exception('Invalid store name given when trying to create an edit form.');
+        }
+        $plugindir = $plugins[$plugin];
+        $class = 'cachestore_addinstance_form';
+        if (file_exists($plugindir.'/addinstanceform.php')) {
+            require_once($plugindir.'/addinstanceform.php');
+            if (class_exists('cachestore_'.$plugin.'_addinstance_form')) {
+                $class = 'cachestore_'.$plugin.'_addinstance_form';
+                if (!array_key_exists('cachestore_addinstance_form', class_parents($class))) {
+                    throw new coding_exception('Cache plugin add instance forms must extend cachestore_addinstance_form');
+                }
+            }
+        }
+
+        $locks = self::get_possible_locks_for_stores($plugindir, $plugin);
+
+        $url = new moodle_url('/cache/admin.php', array('action' => 'editstore'));
+        return new $class($url, array('plugin' => $plugin, 'store' => $store, 'locks' => $locks));
+    }
+
+    /**
+     * Returns an array of suitable lock instances for use with this plugin, or false if the plugin handles locking itself.
+     *
+     * @param string $plugindir
+     * @param string $plugin
+     * @return array|false
+     */
+    protected static function get_possible_locks_for_stores($plugindir, $plugin) {
+        global $CFG; // Needed for includes.
+        $supportsnativelocking = false;
+        if (file_exists($plugindir.'/lib.php')) {
+            require_once($plugindir.'/lib.php');
+            $pluginclass = 'cachestore_'.$plugin;
+            if (class_exists($pluginclass)) {
+                $supportsnativelocking = array_key_exists('cache_is_lockable', class_implements($pluginclass));
+            }
+        }
+
+        if (!$supportsnativelocking) {
+            $config = cache_config::instance();
+            $locks = array();
+            foreach ($config->get_locks() as $lock => $conf) {
+                if (!empty($conf['default'])) {
+                    $name = get_string($lock, 'cache');
+                } else {
+                    $name = $lock;
+                }
+                $locks[$lock] = $name;
+            }
+        } else {
+            $locks = false;
+        }
+
+        return $locks;
+    }
+
+    /**
+     * Processes the results of the add/edit instance form data for a plugin returning an array of config information suitable to
+     * store in configuration.
+     *
+     * @param stdClass $data The mform data.
+     * @return array
+     * @throws coding_exception
+     */
+    public static function get_store_configuration_from_data(stdClass $data) {
+        global $CFG;
+        $file = $CFG->dirroot.'/cache/stores/'.$data->plugin.'/lib.php';
+        if (!file_exists($file)) {
+            throw new coding_exception('Invalid cache plugin provided. '.$file);
+        }
+        require_once($file);
+        $class = 'cachestore_'.$data->plugin;
+        $method = 'config_get_configuration_array';
+        if (!class_exists($class)) {
+            throw new coding_exception('Invalid cache plugin provided.');
+        }
+        if (method_exists($class, $method)) {
+            return call_user_func(array($class, $method), $data);
+        }
+        return array();
+    }
+
+    /**
+     * Get an array of stores that are suitable to be used for a given definition.
+     *
+     * @param string $component
+     * @param string $area
+     * @return array Array containing 3 elements
+     *      1. An array of currently used stores
+     *      2. An array of suitable stores
+     *      3. An array of default stores
+     */
+    public static function get_definition_store_options($component, $area) {
+        $factory = cache_factory::instance();
+        $definition = $factory->create_definition($component, $area);
+        $config = cache_config::instance();
+        $currentstores = $config->get_stores_for_definition($definition);
+        $possiblestores = $config->get_stores($definition->get_mode(), $definition->get_requirements_bin());
+
+        $defaults = array();
+        foreach ($currentstores as $key => $store) {
+            if (!empty($store['default'])) {
+                $defaults[] = $key;
+                unset($currentstores[$key]);
+            }
+        }
+        foreach ($possiblestores as $key => $store) {
+            if ($store['default']) {
+                unset($possiblestores[$key]);
+                $possiblestores[$key] = $store;
+            }
+        }
+        return array($currentstores, $possiblestores, $defaults);
+    }
+
+    /**
+     * Get the default stores for all modes.
+     *
+     * @return array An array containing sub-arrays, one for each mode.
+     */
+    public static function get_default_mode_stores() {
+        $instance = cache_config::instance();
+        $storenames = array();
+        foreach ($instance->get_all_stores() as $key => $store) {
+            if (!empty($store['default'])) {
+                $storenames[$key] = new lang_string('store_'.$key, 'cache');
+            }
+        }
+        $modemappings = array(
+            cache_store::MODE_APPLICATION => array(),
+            cache_store::MODE_SESSION => array(),
+            cache_store::MODE_REQUEST => array(),
+        );
+        foreach ($instance->get_mode_mappings() as $mapping) {
+            $mode = $mapping['mode'];
+            if (!array_key_exists($mode, $modemappings)) {
+                debugging('Unknown mode in cache store mode mappings', DEBUG_DEVELOPER);
+                continue;
+            }
+            if (array_key_exists($mapping['store'], $storenames)) {
+                $modemappings[$mode][$mapping['store']] = $storenames[$mapping['store']];
+            } else {
+                $modemappings[$mode][$mapping['store']] = $mapping['store'];
+            }
+        }
+        return $modemappings;
+    }
+
+    /**
+     * Returns an array summarising the locks available in the system
+     */
+    public static function get_lock_summaries() {
+        $locks = array();
+        $instance = cache_config::instance();
+        $stores = $instance->get_all_stores();
+        foreach ($instance->get_locks() as $lock) {
+            $default = !empty($lock['default']);
+            if ($default) {
+                $name = new lang_string($lock['name'], 'cache');
+            } else {
+                $name = $lock['name'];
+            }
+            $uses = 0;
+            foreach ($stores as $store) {
+                if (!empty($store['lock']) && $store['lock'] === $lock['name']) {
+                    $uses++;
+                }
+            }
+            $lockdata = array(
+                'name' => $name,
+                'default' => $default,
+                'uses' => $uses
+            );
+            $locks[] = $lockdata;
+        }
+        return $locks;
+    }
+}
\ No newline at end of file
diff --git a/cache/locks/file/lang/en/cachelock_file.php b/cache/locks/file/lang/en/cachelock_file.php
new file mode 100644 (file)
index 0000000..b57188c
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for the cache file locking plugin
+ *
+ * @package    cachelock_file
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['pluginname'] = 'File locking';
\ No newline at end of file
diff --git a/cache/locks/file/lib.php b/cache/locks/file/lib.php
new file mode 100644 (file)
index 0000000..b726cb7
--- /dev/null
@@ -0,0 +1,237 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * File locking for the Cache API
+ *
+ * @package    cachelock_file
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * File locking plugin
+ *
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cachelock_file implements cache_lock_interface {
+
+    /**
+     * The name of the cache lock instance
+     * @var string
+     */
+    protected $name;
+
+    /**
+     * The absolute directory in which lock files will be created and looked for.
+     * @var string
+     */
+    protected $cachedir;
+
+    /**
+     * The maximum life in seconds for a lock file. By default null for none.
+     * @var int|null
+     */
+    protected $maxlife = null;
+
+    /**
+     * The number of attempts to acquire a lock when blocking is required before throwing an exception.
+     * @var int
+     */
+    protected $blockattempts = 100;
+
+    /**
+     * An array containing the locks that have been acquired but not released so far.
+     * @var array Array of key => lock file path
+     */
+    protected $locks = array();
+
+    /**
+     * Initialises the cache lock instance.
+     *
+     * @param string $name The name of the cache lock
+     * @param array $configuration
+     */
+    public function __construct($name, array $configuration = array()) {
+        $this->name = $name;
+        if (!array_key_exists('dir', $configuration)) {
+            $this->cachedir = make_cache_directory(md5($name));
+        } else {
+            $dir = $configuration['dir'];
+            if (strpos($dir, '/') !== false && strpos($dir, '.') !== 0) {
+                // This looks like an absolute path.
+                if (file_exists($dir) && is_dir($dir) && is_writable($dir)) {
+                    $this->cachedir = $dir;
+                }
+            }
+            if (empty($this->cachedir)) {
+                $dir = preg_replace('#[^a-zA-Z0-9_]#', '_', $dir);
+                $this->cachedir = make_cache_directory($dir);
+            }
+        }
+        if (array_key_exists('maxlife', $configuration) && is_number($configuration['maxlife'])) {
+            $maxlife = (int)$configuration['maxlife'];
+            // Minimum lock time is 60 seconds.
+            $this->maxlife = max($maxlife, 60);
+        }
+        if (array_key_exists('blockattempts', $configuration) && is_number($configuration['blockattempts'])) {
+            $this->blockattempts = (int)$configuration['blockattempts'];
+        }
+    }
+
+    /**
+     * Acquire a lock.
+     *
+     * If the lock can be acquired:
+     *      This function will return true.
+     *
+     * If the lock cannot be acquired the result of this method is determined by the block param:
+     *      $block = true (default)
+     *          The function will block any further execution unti the lock can be acquired.
+     *          This involves the function attempting to acquire the lock and the sleeping for a period of time. This process
+     *          will be repeated until the lock is required or until a limit is hit (100 by default) in which case a cache
+     *          exception will be thrown.
+     *      $block = false
+     *          The function will return false immediately.
+     *
+     * If a max life has been specified and the lock can not be acquired then the lock file will be checked against this time.
+     * In the case that the file exceeds that max time it will be forcefully deleted.
+     * Because this can obviously be a dangerous thing it is not used by default. If it is used it should be set high enough that
+     * we can be as sure as possible that the executing code has completed.
+     *
+     * @param string $key The key that we want to lock
+     * @param string $ownerid A unique identifier for the owner of this lock. Not used by default.
+     * @param bool $block True if we want the program block further execution until the lock has been acquired.
+     * @return bool
+     * @throws cache_exception If block is set to true and more than 100 attempts have been made to acquire a lock.
+     */
+    public function lock($key, $ownerid, $block = false) {
+        // Get the name of the lock file we want to use.
+        $lockfile = $this->get_lock_file($key);
+
+        // Attempt to create a handle to the lock file.
+        // Mode xb is the secret to this whole function.
+        //   x = Creates the file and opens it for writing. If the file already exists fopen returns false and a warning is thrown.
+        //   b = Forces binary mode.
+        $result = @fopen($lockfile, 'xb');
+
+        // Check if we could create the file or not.
+        if ($result === false) {
+            // Lock exists already.
+            if ($this->maxlife !== null && !array_key_exists($key, $this->locks)) {
+                $mtime = filemtime($lockfile);
+                if ($mtime < time() - $this->maxlife) {
+                    $this->unlock($key, true);
+                    $result = $this->lock($key, false);
+                    if ($result) {
+                        return true;
+                    }
+                }
+            }
+            if ($block) {
+                // OK we are blocking. We had better sleep and then retry to lock.
+                $iterations = 0;
+                $maxiterations = $this->blockattempts;
+                while (($result = $this->lock($key, false)) === false) {
+                    // Usleep causes the application to cleep to x microseconds.
+                    // Before anyone asks there are 1'000'000 microseconds to a second.
+                    usleep(rand(1000, 50000)); // Sleep between 1 and 50 milliseconds.
+                    $iterations++;
+                    if ($iterations > $maxiterations) {
+                        // BOOM! We've exceeded the maximum number of iterations we want to block for.
+                        throw new cache_exception('ex_unabletolock');
+                    }
+                }
+            }
+
+            return false;
+        } else {
+            // We have the lock.
+            fclose($result);
+            $this->locks[$key] = $lockfile;
+            return true;
+        }
+    }
+
+    /**
+     * Releases an acquired lock.
+     *
+     * For more details see {@link cache_lock::unlock()}
+     *
+     * @param string $key
+     * @param string $ownerid A unique identifier for the owner of this lock. Not used by default.
+     * @param bool $forceunlock If set to true the lock will be removed if it exists regardless of whether or not we own it.
+     * @return bool
+     */
+    public function unlock($key, $ownerid, $forceunlock = false) {
+        if (array_key_exists($key, $this->locks)) {
+            @unlink($this->locks[$key]);
+            unset($this->locks[$key]);
+            return true;
+        } else if ($forceunlock) {
+            $lockfile = $this->get_lock_file($key);
+            if (file_exists($lockfile)) {
+                @unlink($lockfile);
+            }
+            return true;
+        }
+        // You cannot unlock a file you didn't lock.
+        return false;
+    }
+
+    /**
+     * Checks if the given key is locked.
+     *
+     * @param string $key
+     * @param string $ownerid
+     */
+    public function check_state($key, $ownerid) {
+        if (key_exists($key, $this->locks)) {
+            // The key is locked and we own it.
+            return true;
+        }
+        $lockfile = $this->get_lock_file($key);
+        if (file_exists($lockfile)) {
+            // The key is locked and we don't own it.
+            return false;
+        }
+        return null;
+    }
+
+    /**
+     * Gets the name to use for a lock file.
+     *
+     * @param string $key
+     * @return string
+     */
+    protected function get_lock_file($key) {
+        return $this->cachedir.'/'. $key .'.lock';
+    }
+
+    /**
+     * Cleans up the instance what it is no longer needed.
+     */
+    public function __destruct() {
+        foreach ($this->locks as $lockfile) {
+            // Naught, naughty developers.
+            @unlink($lockfile);
+        }
+    }
+}
\ No newline at end of file
diff --git a/cache/renderer.php b/cache/renderer.php
new file mode 100644 (file)
index 0000000..2443a09
--- /dev/null
@@ -0,0 +1,339 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The Cache renderer.
+ *
+ * This file is part of Moodle's cache API, affectionately called MUC.
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The cache renderer (mainly admin interfaces).
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_cache_renderer extends plugin_renderer_base {
+
+    /**
+     * Displays store summaries.
+     *
+     * @param array $stores
+     * @param array $plugins
+     * @return string HTML
+     */
+    public function store_instance_summariers(array $stores, array $plugins) {
+        $table = new html_table();
+        $table->head = array(
+            get_string('storename', 'cache'),
+            get_string('plugin', 'cache'),
+            get_string('storeready', 'cache'),
+            get_string('mappings', 'cache'),
+            get_string('modes', 'cache'),
+            get_string('supports', 'cache'),
+            get_string('actions', 'cache'),
+        );
+        $table->colclasses = array(
+            'storename',
+            'plugin',
+            'storeready',
+            'mappings',
+            'modes',
+            'supports',
+            'actions'
+        );
+        $table->data = array();
+
+        $defaultstoreactions = get_string('defaultstoreactions', 'cache');
+
+        foreach ($stores as $name => $store) {
+            $actions = cache_administration_helper::get_store_instance_actions($name, $store);
+            $modes = array();
+            foreach ($store['modes'] as $mode => $enabled) {
+                if ($enabled) {
+                    $modes[] = get_string('mode_'.$mode, 'cache');
+                }
+            }
+
+            $supports = array();
+            foreach ($store['supports'] as $support => $enabled) {
+                if ($enabled) {
+                    $supports[] = get_string('supports_'.$support, 'cache');
+                }
+            }
+
+            $info = '';
+            if (!empty($store['default'])) {
+                $info = $this->output->pix_icon('i/info', $defaultstoreactions).' ';
+            }
+            $htmlactions = array();
+            foreach ($actions as $action) {
+                $htmlactions[] = $this->output->action_link($action['url'], $action['text']);
+            }
+
+            $storename = $store['name'];
+            if (!empty($store['default'])) {
+                $storename = get_string('store_'.$store['name'], 'cache');
+            }
+
+            $row = new html_table_row(array(
+                $storename,
+                get_string('pluginname', 'cachestore_'.$store['plugin']),
+                ($store['isready'] && $store['requirementsmet']) ? $this->output->pix_icon('i/tick_green_small', '1') : '',
+                $store['mappings'],
+                join(', ', $modes),
+                join(', ', $supports),
+                $info.join(', ', $htmlactions)
+            ));
+            $row->attributes['class'] = 'store-'.$name;
+            if ($store['default']) {
+                $row->attributes['class'] .= ' default-store';
+            }
+            $table->data[] = $row;
+        }
+
+        $html  = html_writer::start_tag('div', array('id' => 'core-cache-store-summaries'));
+        $html .= $this->output->heading(get_string('storesummaries', 'cache'), 3);
+        $html .= html_writer::table($table);
+        $html .= html_writer::end_tag('div');
+        return $html;
+    }
+
+    /**
+     * Displays plugin summaries
+     *
+     * @param array $plugins
+     * @return string HTML
+     */
+    public function store_plugin_summaries(array $plugins) {
+        $table = new html_table();
+        $table->head = array(
+            get_string('plugin', 'cache'),
+            get_string('storeready', 'cache'),
+            get_string('stores', 'cache'),
+            get_string('modes', 'cache'),
+            get_string('supports', 'cache'),
+            get_string('actions', 'cache'),
+        );
+        $table->colclasses = array(
+            'plugin',
+            'storeready',
+            'stores',
+            'modes',
+            'supports',
+            'actions'
+        );
+        $table->data = array();
+
+        foreach ($plugins as $name => $plugin) {
+            $actions = cache_administration_helper::get_store_plugin_actions($name, $plugin);
+
+            $modes = array();
+            foreach ($plugin['modes'] as $mode => $enabled) {
+                if ($enabled) {
+                    $modes[] = get_string('mode_'.$mode, 'cache');
+                }
+            }
+
+            $supports = array();
+            foreach ($plugin['supports'] as $support => $enabled) {
+                if ($enabled) {
+                    $supports[] = get_string('supports_'.$support, 'cache');
+                }
+            }
+
+            $htmlactions = array();
+            foreach ($actions as $action) {
+                $htmlactions[] = $this->output->action_link($action['url'], $action['text']);
+            }
+
+            $row = new html_table_row(array(
+                $plugin['name'],
+                ($plugin['requirementsmet']) ? $this->output->pix_icon('i/tick_green_small', '1') : '',
+                $plugin['instances'],
+                join(', ', $modes),
+                join(', ', $supports),
+                join(', ', $htmlactions)
+            ));
+
+            $row->attributes['class'] = 'plugin-'.$name;
+            $table->data[] = $row;
+        }
+
+        $html  = html_writer::start_tag('div', array('id' => 'core-cache-plugin-summaries'));
+        $html .= $this->output->heading(get_string('pluginsummaries', 'cache'), 3);
+        $html .= html_writer::table($table);
+        $html .= html_writer::end_tag('div');
+        return $html;
+    }
+
+    /**
+     * Displays definition summaries
+     *
+     * @param array $definitions
+     * @param array $actions
+     * @return string HTML
+     */
+    public function definition_summaries(array $definitions, array $actions) {
+        $table = new html_table();
+        $table->head = array(
+            get_string('definition', 'cache'),
+            get_string('mode', 'cache'),
+            get_string('component', 'cache'),
+            get_string('area', 'cache'),
+            get_string('mappings', 'cache'),
+            get_string('actions', 'cache'),
+        );
+        $table->colclasses = array(
+            'definition',
+            'mode',
+            'component',
+            'area',
+            'mappings',
+            'actions'
+        );
+        $table->data = array();
+
+        $none = new lang_string('none', 'cache');
+        foreach ($definitions as $id => $definition) {
+            $htmlactions = array();
+            foreach ($actions as $action) {
+                $action['url']->param('definition', $id);
+                $htmlactions[] = $this->output->action_link($action['url'], $action['text']);
+            }
+            if (!empty($definition['mappings'])) {
+                $mapping = join(', ', $definition['mappings']);
+            } else {
+                $mapping = '<em>'.$none.'</em>';
+            }
+
+            $row = new html_table_row(array(
+                $definition['name'],
+                get_string('mode_'.$definition['mode'], 'cache'),
+                $definition['component'],
+                $definition['area'],
+                $mapping,
+                join(', ', $htmlactions)
+            ));
+            $row->attributes['class'] = 'definition-'.$definition['component'].'-'.$definition['area'];
+            $table->data[] = $row;
+        }
+
+        $html  = html_writer::start_tag('div', array('id' => 'core-cache-definition-summaries'));
+        $html .= $this->output->heading(get_string('definitionsummaries', 'cache'), 3);
+        $html .= html_writer::table($table);
+
+        $url = new moodle_url('/cache/admin.php', array('action' => 'rescandefinitions', 'sesskey' => sesskey()));
+        $link = html_writer::link($url, get_string('rescandefinitions', 'cache'));
+        $html .= html_writer::tag('div', $link, array('id' => 'core-cache-rescan-definitions'));
+
+        $html .= html_writer::end_tag('div');
+        return $html;
+    }
+
+    /**
+     * Displays mode mappings
+     *
+     * @param string $applicationstore
+     * @param string $sessionstore
+     * @param string $requeststore
+     * @param moodle_url $editurl
+     * @return string HTML
+     */
+    public function mode_mappings($applicationstore, $sessionstore, $requeststore, moodle_url $editurl) {
+        $table = new html_table();
+        $table->colclasses = array(
+            'mode',
+            'mapping',
+        );
+        $table->rowclasses = array(
+            'mode_application',
+            'mode_session',
+            'mode_request'
+        );
+        $table->head = array(
+            get_string('mode', 'cache'),
+            get_string('mappings', 'cache'),
+        );
+        $table->data = array(
+            array(get_string('mode_'.cache_store::MODE_APPLICATION, 'cache'), $applicationstore),
+            array(get_string('mode_'.cache_store::MODE_SESSION, 'cache'), $sessionstore),
+            array(get_string('mode_'.cache_store::MODE_REQUEST, 'cache'), $requeststore)
+        );
+
+        $html = html_writer::start_tag('div', array('id' => 'core-cache-mode-mappings'));
+        $html .= $this->output->heading(get_string('defaultmappings', 'cache'), 3);
+        $html .= html_writer::table($table);
+        $link = html_writer::link($editurl, get_string('editmappings', 'cache'));
+        $html .= html_writer::tag('div', $link, array('class' => 'edit-link'));
+        $html .= html_writer::end_tag('div');
+        return $html;
+    }
+
+    /**
+     * Display basic information about lock instances.
+     *
+     * @todo Add some actions so that people can configure lock instances.
+     *
+     * @param array $locks
+     * @return string
+     */
+    public function lock_summaries(array $locks) {
+        $table = new html_table();
+        $table->colclasses = array(
+            'name',
+            'default',
+            'uses',
+            // Useful later: 'actions'.
+        );
+        $table->rowclasses = array(
+            'lock_name',
+            'lock_default',
+            'lock_uses',
+            // Useful later: 'lock_actions',.
+        );
+        $table->head = array(
+            get_string('lockname', 'cache'),
+            get_string('lockdefault', 'cache'),
+            get_string('lockuses', 'cache'),
+            // Useful later: get_string('actions', 'cache').
+        );
+        $table->data = array();
+        $tick = $this->output->pix_icon('i/tick_green_big', '');
+        foreach ($locks as $lock) {
+            $table->data[] = new html_table_row(array(
+                new html_table_cell($lock['name']),
+                new html_table_cell($lock['default'] ? $tick : ''),
+                new html_table_cell($lock['uses']),
+            ));
+        }
+
+        $html = html_writer::start_tag('div', array('id' => 'core-cache-lock-summary'));
+        $html .= $this->output->heading(get_string('locksummary', 'cache'), 3);
+        $html .= html_writer::table($table);
+        $html .= html_writer::end_tag('div');
+        return $html;
+    }
+}
\ No newline at end of file
diff --git a/cache/stores/file/addinstanceform.php b/cache/stores/file/addinstanceform.php
new file mode 100644 (file)
index 0000000..a5e3125
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The library file for the file cache store.
+ *
+ * This file is part of the file cache store, it contains the API for interacting with an instance of the store.
+ * This is used as a default cache store within the Cache API. It should never be deleted.
+ *
+ * @package    cachestore_file
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once($CFG->dirroot.'/cache/forms.php');
+require_once($CFG->dirroot.'/cache/stores/file/lib.php');
+
+/**
+ * Form for adding a file instance.
+ *
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cachestore_file_addinstance_form extends cachestore_addinstance_form {
+
+    /**
+     * Adds the desired form elements.
+     */
+    protected function configuration_definition() {
+        $form = $this->_form;
+
+        $form->addElement('text', 'path', get_string('path', 'cachestore_file'));
+        $form->setType('path', PARAM_SAFEPATH);
+        $form->addHelpButton('path', 'path', 'cachestore_file');
+
+        $form->addElement('checkbox', 'autocreate', get_string('autocreate', 'cachestore_file'));
+        $form->setType('autocreate', PARAM_BOOL);
+        $form->addHelpButton('autocreate', 'autocreate', 'cachestore_file');
+        $form->disabledIf('autocreate', 'path', 'eq', '');
+
+        $form->addElement('checkbox', 'prescan', get_string('prescan', 'cachestore_file'));
+        $form->setType('prescan', PARAM_BOOL);
+        $form->addHelpButton('prescan', 'prescan', 'cachestore_file');
+    }
+}
\ No newline at end of file
diff --git a/cache/stores/file/lang/en/cachestore_file.php b/cache/stores/file/lang/en/cachestore_file.php
new file mode 100644 (file)
index 0000000..8a17b71
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The library file for the file cache store.
+ *
+ * This file is part of the file cache store, it contains the API for interacting with an instance of the store.
+ * This is used as a default cache store within the Cache API. It should never be deleted.
+ *
+ * @package    cachestore_file
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['autocreate'] = 'Auto create directory';
+$string['autocreate_help'] = 'If enabled the directory specified in path will be automatically created if it does not already exist.';
+$string['path'] = 'Cache path';
+$string['path_help'] = 'The directory that should be used to store files for this cache store. If left blank (default) a directory will be automatically created in the moodledata directory. This can be used to point a file store towards a directory on a better performing drive (such as one in memory).';
+$string['pluginname'] = 'File cache';
+$string['prescan'] = 'Prescan directory';
+$string['prescan_help'] = 'If enabled the directory is scanned when the cache is first used and requests for files are first checked against the scan data. This can help if you have a slow file system and are finding that file operations are causing you a bottle neck.';
diff --git a/cache/stores/file/lib.php b/cache/stores/file/lib.php
new file mode 100644 (file)
index 0000000..3428d92
--- /dev/null
@@ -0,0 +1,570 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The library file for the file cache store.
+ *
+ * This file is part of the file cache store, it contains the API for interacting with an instance of the store.
+ * This is used as a default cache store within the Cache API. It should never be deleted.
+ *
+ * @package    cachestore_file
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * The file store class.
+ *
+ * Configuration options
+ *      path:           string: path to the cache directory, if left empty one will be created in the cache directory
+ *      autocreate:     true, false
+ *      prescan:        true, false
+ *
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cachestore_file implements cache_store, cache_is_key_aware {
+
+    /**
+     * The name of the store.
+     * @var string
+     */
+    protected $name;
+
+    /**
+     * The path to use for the file storage.
+     * @var string
+     */
+    protected $path = null;
+
+    /**
+     * Set to true when a prescan has been performed.
+     * @var bool
+     */
+    protected $prescan = false;
+
+    /**
+     * Set to true when the path should be automatically created if it does not yet exist.
+     * @var bool
+     */
+    protected $autocreate = false;
+
+    /**
+     * Set to true if a custom path is being used.
+     * @var bool
+     */
+    protected $custompath = false;
+
+    /**
+     * An array of keys we are sure about presently.
+     * @var array
+     */
+    protected $keys = array();
+
+    /**
+     * True when the store is ready to be initialised.
+     * @var bool
+     */
+    protected $isready = false;
+
+    /**
+     * The cache definition this instance has been initialised with.
+     * @var cache_definition
+     */
+    protected $definition;
+
+    /**
+     * Constructs the store instance.
+     *
+     * Noting that this function is not an initialisation. It is used to prepare the store for use.
+     * The store will be initialised when required and will be provided with a cache_definition at that time.
+     *
+     * @param string $name
+     * @param array $configuration
+     */
+    public function __construct($name, array $configuration = array()) {
+        $this->name = $name;
+        if (array_key_exists('path', $configuration) && $configuration['path'] !== '') {
+            $this->custompath = true;
+            $this->autocreate = !empty($configuration['autocreate']);
+            $path = (string)$configuration['path'];
+            if (!is_dir($path)) {
+                if ($this->autocreate) {
+                    if (!make_writable_directory($path, false)) {
+                        $path = false;
+                        debugging('Error trying to autocreate file store path. '.$path, DEBUG_DEVELOPER);
+                    }
+                } else {
+                    $path = false;
+                    debugging('The given file cache store path does not exist. '.$path, DEBUG_DEVELOPER);
+                }
+            }
+            if ($path !== false && !is_writable($path)) {
+                $path = false;
+                debugging('The given file cache store path is not writable. '.$path, DEBUG_DEVELOPER);
+            }
+        } else {
+            $path = make_cache_directory('cachestore_file/'.preg_replace('#[^a-zA-Z0-9\.\-_]+#', '', $name));
+        }
+        $this->isready = $path !== false;
+        $this->path = $path;
+        $this->prescan = array_key_exists('prescan', $configuration) ? (bool)$configuration['prescan'] : false;
+    }
+
+    /**
+     * Returns true if this store instance is ready to be used.
+     * @return bool
+     */
+    public function is_ready() {
+        return ($this->path !== null);
+    }
+
+    /**
+     * Returns true once this instance has been initialised.
+     *
+     * @return bool
+     */
+    public function is_initialised() {
+        return true;
+    }
+
+    /**
+     * Returns the supported features as a combined int.
+     *
+     * @param array $configuration
+     * @return int
+     */
+    public static function get_supported_features(array $configuration = array()) {
+        $supported = self::SUPPORTS_DATA_GUARANTEE +
+                     self::SUPPORTS_NATIVE_TTL;
+        return $supported;
+    }
+
+    /**
+     * Returns the supported modes as a combined int.
+     *
+     * @param array $configuration
+     * @return int
+     */
+    public static function get_supported_modes(array $configuration = array()) {
+        return self::MODE_APPLICATION + self::MODE_SESSION;
+    }
+
+    /**
+     * Returns true if the store requirements are met.
+     *
+     * @return bool
+     */
+    public static function are_requirements_met() {
+        return true;
+    }
+
+    /**
+     * Returns true if the given mode is supported by this store.
+     *
+     * @param int $mode One of cache_store::MODE_*
+     * @return bool
+     */
+    public static function is_supported_mode($mode) {
+        return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION);
+    }
+
+    /**
+     * Returns true if the store instance supports multiple identifiers.
+     *
+     * @return bool
+     */
+    public function supports_multiple_indentifiers() {
+        return false;
+    }
+
+    /**
+     * Returns true if the store instance guarantees data.
+     *
+     * @return bool
+     */
+    public function supports_data_guarantee() {
+        return true;
+    }
+
+    /**
+     * Returns true if the store instance supports native ttl.
+     *
+     * @return bool
+     */
+    public function supports_native_ttl() {
+        return true;
+    }
+
+    /**
+     * Initialises the cache.
+     *
+     * Once this has been done the cache is all set to be used.
+     *
+     * @param cache_definition $definition
+     */
+    public function initialise(cache_definition $definition) {
+        $this->definition = $definition;
+        $hash = preg_replace('#[^a-zA-Z0-9]+#', '_', $this->definition->get_id());
+        $this->path .= '/'.$hash;
+        make_writable_directory($this->path);
+        if ($this->prescan && $definition->get_mode() !== self::MODE_REQUEST) {
+            $this->prescan = false;
+        }
+        if ($this->prescan) {
+            $pattern = $this->path.'/*.cache';
+            foreach (glob($pattern, GLOB_MARK | GLOB_NOSORT) as $filename) {
+                $this->keys[basename($filename)] = filemtime($filename);
+            }
+        }
+    }
+
+    /**
+     * Retrieves an item from the cache store given its key.
+     *
+     * @param string $key The key to retrieve
+     * @return mixed The data that was associated with the key, or false if the key did not exist.
+     */
+    public function get($key) {
+        $filename = $key.'.cache';
+        $file = $this->path.'/'.$filename;
+        $ttl = $this->definition->get_ttl();
+        if ($ttl) {
+            $maxtime = cache::now() - $ttl;
+        }
+        $readfile = false;
+        if ($this->prescan && array_key_exists($key, $this->keys)) {
+            if (!$ttl || $this->keys[$filename] >= $maxtime && file_exists($file)) {
+                $readfile = true;
+            } else {
+                $this->delete($key);
+            }
+        } else if (file_exists($file) && (!$ttl || filemtime($file) >= $maxtime)) {
+            $readfile = true;
+        }
+        if (!$readfile) {
+            return false;
+        }
+        // Check the filesize first, likely not needed but important none the less.
+        $filesize = filesize($file);
+        if (!$filesize) {
+            return false;
+        }
+        // Open ensuring the file for writing, truncating it and setting the pointer to the start.
+        if (!$handle = fopen($file, 'rb')) {
+            return false;
+        }
+        // Lock it up!
+        // We don't care if this succeeds or not, on some systems it will, on some it won't, meah either way.
+        flock($handle, LOCK_SH);
+        // HACK ALERT
+        // There is a problem when reading from the file during PHPUNIT tests. For one reason or another the filesize is not correct
+        // Doesn't happen during normal operation, just during unit tests.
+        // Read it.
+        $data = fread($handle, $filesize+128);
+        // Unlock it.
+        flock($handle, LOCK_UN);
+        // Return it unserialised.
+        return $this->prep_data_after_read($data);
+    }
+
+    /**
+     * Retrieves several items from the cache store in a single transaction.
+     *
+     * If not all of the items are available in the cache then the data value for those that are missing will be set to false.
+     *
+     * @param array $keys The array of keys to retrieve
+     * @return array An array of items from the cache. There will be an item for each key, those that were not in the store will
+     *      be set to false.
+     */
+    public function get_many($keys) {
+        $result = array();
+        foreach ($keys as $key) {
+            $result[$key] = $this->get($key);
+        }
+        return $result;
+    }
+
+    /**
+     * Deletes an item from the cache store.
+     *
+     * @param string $key The key to delete.
+     * @return bool Returns true if the operation was a success, false otherwise.
+     */
+    public function delete($key) {
+        $filename = $key.'.cache';
+        $file = $this->path.'/'.$filename;
+        $result = @unlink($file);
+        unset($this->keys[$filename]);
+        return $result;
+    }
+
+    /**
+     * Deletes several keys from the cache in a single action.
+     *
+     * @param array $keys The keys to delete
+     * @return int The number of items successfully deleted.
+     */
+    public function delete_many(array $keys) {
+        $count = 0;
+        foreach ($keys as $key) {
+            if ($this->delete($key)) {
+                $count++;
+            }
+        }
+        return $count;
+    }
+
+    /**
+     * Sets an item in the cache given its key and data value.
+     *
+     * @param string $key The key to use.
+     * @param mixed $data The data to set.
+     * @return bool True if the operation was a success false otherwise.
+     */
+    public function set($key, $data) {
+        $this->ensure_path_exists();
+        $filename = $key.'.cache';
+        $file = $this->path.'/'.$filename;
+        $result = $this->write_file($file, $this->prep_data_before_save($data));
+        if (!$result) {
+            // Couldn't write the file.
+            return false;
+        }
+        // Record the key if required.
+        if ($this->prescan) {
+            $this->keys[$filename] = cache::now() + 1;
+        }
+        // Return true.. it all worked **miracles**.
+        return true;
+    }
+
+    /**
+     * Prepares data to be stored in a file.
+     *
+     * @param mixed $data
+     * @return string
+     */
+    protected function prep_data_before_save($data) {
+        return serialize($data);
+    }
+
+    /**
+     * Prepares the data it has been read from the cache. Undoing what was done in prep_data_before_save.
+     *
+     * @param string $data
+     * @return mixed
+     * @throws coding_exception
+     */
+    protected function prep_data_after_read($data) {
+        $result = @unserialize($data);
+        if ($result === false) {
+            throw new coding_exception('Failed to unserialise data from file. Either failed to read, or failed to write.');
+        }
+        return $result;
+    }
+
+    /**
+     * Sets many items in the cache in a single transaction.
+     *
+     * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
+     *      keys, 'key' and 'value'.
+     * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
+     *      sent ... if they care that is.
+     */
+    public function set_many(array $keyvaluearray) {
+        $count = 0;
+        foreach ($keyvaluearray as $pair) {
+            if ($this->set($pair['key'], $pair['value'])) {
+                $count++;
+            }
+        }
+        return $count;
+    }
+
+    /**
+     * Checks if the store has a record for the given key and returns true if so.
+     *
+     * @param string $key
+     * @return bool
+     */
+    public function has($key) {
+        $filename = $key.'.cache';
+        $file = $this->path.'/'.$key.'.cache';
+        $maxtime = cache::now() - $this->definition->get_ttl();
+        if ($this->prescan) {
+            return array_key_exists($filename, $this->keys) && $this->keys[$filename] >= $maxtime;
+        }
+        return (file_exists($file) && ($this->definition->get_ttl() == 0 || filemtime($file) >= $maxtime));
+    }
+
+    /**
+     * Returns true if the store contains records for all of the given keys.
+     *
+     * @param array $keys
+     * @return bool
+     */
+    public function has_all(array $keys) {
+        foreach ($keys as $key) {
+            if (!$this->has($key)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns true if the store contains records for any of the given keys.
+     *
+     * @param array $keys
+     * @return bool
+     */
+    public function has_any(array $keys) {
+        foreach ($keys as $key) {
+            if ($this->has($key)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Purges the cache deleting all items within it.
+     *
+     * @return boolean True on success. False otherwise.
+     */
+    public function purge() {
+        $pattern = $this->path.'/*.cache';
+        foreach (glob($pattern, GLOB_MARK | GLOB_NOSORT) as $filename) {
+            @unlink($filename);
+        }
+        $this->keys = array();
+        return true;
+    }
+
+    /**
+     * Checks to make sure that the path for the file cache exists.
+     *
+     * @return bool
+     * @throws coding_exception
+     */
+    protected function ensure_path_exists() {
+        if (!is_writable($this->path)) {
+            if ($this->custompath && !$this->autocreate) {
+                throw new coding_exception('File store path does not exist. It must exist and be writable by the web server.');
+            }
+            if (!make_writable_directory($this->path, false)) {
+                throw new coding_exception('File store path does not exist and can not be created.');
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns true if the user can add an instance of the store plugin.
+     *
+     * @return bool
+     */
+    public static function can_add_instance() {
+        return true;
+    }
+
+    /**
+     * Performs any necessary clean up when the store instance is being deleted.
+     *
+     * 1. Purges the cache directory.
+     * 2. Deletes the directory we created for this cache instances data.
+     */
+    public function cleanup() {
+        $this->purge();
+        @rmdir($this->path);
+    }
+
+    /**
+     * Generates an instance of the cache store that can be used for testing.
+     *
+     * Returns an instance of the cache store, or false if one cannot be created.
+     *
+     * @param cache_definition $definition
+     * @return cachestore_file
+     */
+    public static function initialise_test_instance(cache_definition $definition) {
+        $name = 'File test';
+        $path = make_cache_directory('cachestore_file_test');
+        $cache = new cachestore_file($name, array('path' => $path));
+        $cache->initialise($definition);
+        return $cache;
+    }
+
+    /**
+     * Writes your madness to a file.
+     *
+     * There are several things going on in this function to try to ensure what we don't end up with partial writes etc.
+     *   1. Files for writing are opened with the mode xb, the file must be created and can not already exist.
+     *   2. Renaming, data is written to a temporary file, where it can be verified using md5 and is then renamed.
+     *
+     * @param string $file Absolute file path
+     * @param string $content The content to write.
+     * @return bool
+     */
+    protected function write_file($file, $content) {
+        // Generate a temp file that is going to be unique. We'll rename it at the end to the desired file name.
+        // in this way we avoid partial writes.
+        $path = dirname($file);
+        while (true) {
+            $tempfile = $path.'/'.uniqid(sesskey().'.', true) . '.temp';
+            if (!file_exists($tempfile)) {
+                break;
+            }
+        }
+
+        // Open the file with mode=x. This acts to create and open the file for writing only.
+        // If the file already exists this will return false.
+        // We also force binary.
+        $handle = @fopen($tempfile, 'xb+');
+        if ($handle === false) {
+            // File already exists... lock already exists, return false.
+            return false;
+        }
+        fwrite($handle, $content);
+        fflush($handle);
+        // Close the handle, we're done.
+        fclose($handle);
+
+        if (md5_file($tempfile) !== md5($content)) {
+            // The md5 of the content of the file must match the md5 of the content given to be written.
+            @unlink($tempfile);
+            return false;
+        }
+
+        // Finally rename the temp file to the desired file, returning the true|false result.
+        $result = rename($tempfile, $file);
+        if (!$result) {
+            // Failed to rename, don't leave files lying around.
+            @unlink($tempfile);
+        }
+        return $result;
+    }
+
+    /**
+     * Returns the name of this instance.
+     * @return string
+     */
+    public function my_name() {
+        return $this->name;
+    }
+}
\ No newline at end of file
diff --git a/cache/stores/file/version.php b/cache/stores/file/version.php
new file mode 100644 (file)
index 0000000..8d00408
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Cache file store version information.
+ *
+ * This is used as a default cache store within the Cache API. It should never be deleted.
+ *
+ * @package    cachestore_file
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+$plugin->version = 2012091000;    // The current module version (Date: YYYYMMDDXX)
+$plugin->requires = 2012090700;    // Requires this Moodle version.
+$plugin->component = 'cachestore_file';  // Full name of the plugin.
\ No newline at end of file
diff --git a/cache/stores/memcache/addinstanceform.php b/cache/stores/memcache/addinstanceform.php
new file mode 100644 (file)
index 0000000..deb4c9b
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The library file for the memcache cache store.
+ *
+ * This file is part of the memcache cache store, it contains the API for interacting with an instance of the store.
+ *
+ * @package    cachestore_memcache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot.'/cache/forms.php');
+require_once($CFG->dirroot.'/cache/stores/memcached/lib.php');
+
+/**
+ * Form for adding a memcache instance.
+ *
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cachestore_memcache_addinstance_form extends cachestore_addinstance_form {
+
+    /**
+     * Add the desired form elements.
+     */
+    protected function configuration_definition() {
+        $form = $this->_form;
+        $form->addElement('textarea', 'servers', get_string('servers', 'cachestore_memcache'), array('cols' => 75, 'rows' => 5));
+        $form->addHelpButton('servers', 'servers', 'cachestore_memcache');
+        $form->addRule('servers', get_string('required'), 'required');
+        $form->setType('servers', PARAM_RAW);
+    }
+}
\ No newline at end of file
diff --git a/cache/stores/memcache/lang/en/cachestore_memcache.php b/cache/stores/memcache/lang/en/cachestore_memcache.php
new file mode 100644 (file)
index 0000000..585fd53
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The library file for the memcache cache store.
+ *
+ * This file is part of the memcache cache store, it contains the API for interacting with an instance of the store.
+ *
+ * @package    cachestore_memcache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['pluginname'] = 'Memcache';
+$string['servers'] = 'Servers';
+$string['servers_help'] = 'This sets the servers that should be utilised by this memcache adapter.
+Servers should be defined one per line and consist of a server address and optionally a port and weight.
+If no port is provided then the default port (11211) is used.
+
+For example:
+<pre>
+server.url.com
+ipaddress:port
+servername:port:weight
+</pre>';
+$string['testservers'] = 'Test servers';
+$string['testservers_desc'] = 'The test servers get used for unit tests and for performance tests. It is entirely optional to set up test servers. Servers should be defined one per line and consist of a server address and optionally a port and weight.
+If no port is provided then the default port (11211) is used.';
\ No newline at end of file
diff --git a/cache/stores/memcache/lib.php b/cache/stores/memcache/lib.php
new file mode 100644 (file)
index 0000000..6d4c33f
--- /dev/null
@@ -0,0 +1,376 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The library file for the memcache cache store.
+ *
+ * This file is part of the memcache cache store, it contains the API for interacting with an instance of the store.
+ *
+ * @package    cachestore_memcache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The memcache store class.
+ *
+ * (Not to be confused with memcached store)
+ *
+ * Configuration options:
+ *      servers:        string: host:port:weight , ...
+ *
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cachestore_memcache implements cache_store {
+
+    /**
+     * The name of the store
+     * @var store
+     */
+    protected $name;
+
+    /**
+     * The memcache connection once established.
+     * @var Memcache
+     */
+    protected $connection;
+
+    /**
+     * An array of servers to use in the connection args.
+     * @var array
+     */
+    protected $servers = array();
+
+    /**
+     * An array of options used when establishing the connection.
+     * @var array
+     */
+    protected $options = array();
+
+    /**
+     * Set to true when things are ready to be initialised.
+     * @var bool
+     */
+    protected $isready = false;
+
+    /**
+     * The cache definition this store was initialised for.
+     * @var cache_definition
+     */
+    protected $definition;
+
+    /**
+     * Constructs the store instance.
+     *
+     * Noting that this function is not an initialisation. It is used to prepare the store for use.
+     * The store will be initialised when required and will be provided with a cache_definition at that time.
+     *
+     * @param string $name
+     * @param array $configuration
+     */
+    public function __construct($name, array $configuration = array()) {
+        $this->name = $name;
+        if (!array_key_exists('servers', $configuration) || empty($configuration['servers'])) {
+            // Nothing configured.
+            return;
+        }
+        if (!is_array($configuration['servers'])) {
+            $configuration['servers'] = array($configuration['servers']);
+        }
+        foreach ($configuration['servers'] as $server) {
+            if (!is_array($server)) {
+                $server = explode(':', $server, 3);
+            }
+            if (!array_key_exists(1, $server)) {
+                $server[1] = 11211;
+                $server[2] = 100;
+            } else if (!array_key_exists(2, $server)) {
+                $server[2] = 100;
+            }
+            $this->servers[] = $server;
+        }
+
+        $this->isready = true;
+    }
+
+    /**
+     * Initialises the cache.
+     *
+     * Once this has been done the cache is all set to be used.
+     *
+     * @param cache_definition $definition
+     */
+    public function initialise(cache_definition $definition) {
+        if ($this->is_initialised()) {
+            throw new coding_exception('This memcache instance has already been initialised.');
+        }
+        $this->definition = $definition;
+        $this->connection = new Memcache;
+        foreach ($this->servers as $server) {
+            $this->connection->addServer($server[0], $server[1], true, $server[2]);
+        }
+    }
+
+    /**
+     * Returns true once this instance has been initialised.
+     *
+     * @return bool
+     */
+    public function is_initialised() {
+        return ($this->connection !== null);
+    }
+
+    /**
+     * Returns true if this store instance is ready to be used.
+     * @return bool
+     */
+    public function is_ready() {
+        return $this->isready;
+    }
+
+    /**
+     * Returns true if the store requirements are met.
+     *
+     * @return bool
+     */
+    public static function are_requirements_met() {
+        return class_exists('Memcache');
+    }
+
+    /**
+     * Returns true if the given mode is supported by this store.
+     *
+     * @param int $mode One of cache_store::MODE_*
+     * @return bool
+     */
+    public static function is_supported_mode($mode) {
+        return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION);
+    }
+
+    /**
+     * Returns the supported features as a combined int.
+     *
+     * @param array $configuration
+     * @return int
+     */
+    public static function get_supported_features(array $configuration = array()) {
+        return self::SUPPORTS_NATIVE_TTL;
+    }
+
+    /**
+     * Returns true if the store instance supports multiple identifiers.
+     *
+     * @return bool
+     */
+    public function supports_multiple_indentifiers() {
+        return false;
+    }
+
+    /**
+     * Returns true if the store instance guarantees data.
+     *
+     * @return bool
+     */
+    public function supports_data_guarantee() {
+        return false;
+    }
+
+    /**
+     * Returns true if the store instance supports native ttl.
+     *
+     * @return bool
+     */
+    public function supports_native_ttl() {
+        return true;
+    }
+
+    /**
+     * Returns the supported modes as a combined int.
+     *
+     * @param array $configuration
+     * @return int
+     */
+    public static function get_supported_modes(array $configuration = array()) {
+        return self::MODE_APPLICATION + self::MODE_SESSION;
+    }
+
+    /**
+     * Retrieves an item from the cache store given its key.
+     *
+     * @param string $key The key to retrieve
+     * @return mixed The data that was associated with the key, or false if the key did not exist.
+     */
+    public function get($key) {
+        return $this->connection->get($key);
+    }
+
+    /**
+     * Retrieves several items from the cache store in a single transaction.
+     *
+     * If not all of the items are available in the cache then the data value for those that are missing will be set to false.
+     *
+     * @param array $keys The array of keys to retrieve
+     * @return array An array of items from the cache. There will be an item for each key, those that were not in the store will
+     *      be set to false.
+     */
+    public function get_many($keys) {
+        $result = $this->connection->get($keys);
+        if (!is_array($result)) {
+            $result = array();
+        }
+        foreach ($keys as $key) {
+            if (!array_key_exists($key, $result)) {
+                $result[$key] = false;
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * Sets an item in the cache given its key and data value.
+     *
+     * @param string $key The key to use.
+     * @param mixed $data The data to set.
+     * @return bool True if the operation was a success false otherwise.
+     */
+    public function set($key, $data) {
+        return $this->connection->set($key, $data, MEMCACHE_COMPRESSED, $this->definition->get_ttl());
+    }
+
+    /**
+     * Sets many items in the cache in a single transaction.
+     *
+     * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
+     *      keys, 'key' and 'value'.
+     * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
+     *      sent ... if they care that is.
+     */
+    public function set_many(array $keyvaluearray) {
+        $count = 0;
+        foreach ($keyvaluearray as $pair) {
+            if ($this->connection->set($pair['key'], $pair['value'], MEMCACHE_COMPRESSED, $this->definition->get_ttl())) {
+                $count++;
+            }
+        }
+        return $count;
+    }
+
+    /**
+     * Deletes an item from the cache store.
+     *
+     * @param string $key The key to delete.
+     * @return bool Returns true if the operation was a success, false otherwise.
+     */
+    public function delete($key) {
+        return $this->connection->delete($key);
+    }
+
+    /**
+     * Deletes several keys from the cache in a single action.
+     *
+     * @param array $keys The keys to delete
+     * @return int The number of items successfully deleted.
+     */
+    public function delete_many(array $keys) {
+        $count = 0;
+        foreach ($keys as $key) {
+            if ($this->delete($key)) {
+                $count++;
+            }
+        }
+        return $count;
+    }
+
+    /**
+     * Purges the cache deleting all items within it.
+     *
+     * @return boolean True on success. False otherwise.
+     */
+    public function purge() {
+        $this->connection->flush();
+        return true;
+    }
+
+    /**
+     * Given the data from the add instance form this function creates a configuration array.
+     *
+     * @param stdClass $data
+     * @return array
+     */
+    public static function config_get_configuration_array($data) {
+        $lines = explode("\n", $data->servers);
+        $servers = array();
+        foreach ($lines as $line) {
+            $line = trim($line, ':');
+            $servers[] = explode(':', $line, 3);
+        }
+        return array(
+            'servers' => $servers,
+        );
+    }
+
+    /**
+     * Returns true if the user can add an instance of the store plugin.
+     *
+     * @return bool
+     */
+    public static function can_add_instance() {
+        return true;
+    }
+
+    /**
+     * Performs any necessary clean up when the store instance is being deleted.
+     */
+    public function cleanup() {
+        $this->purge();
+    }
+
+    /**
+     * Generates an instance of the cache store that can be used for testing.
+     *
+     * @param cache_definition $definition
+     * @return false
+     */
+    public static function initialise_test_instance(cache_definition $definition) {
+        if (!self::are_requirements_met()) {
+            return false;
+        }
+
+        $config = get_config('cachestore_memcache');
+        if (empty($config->testservers)) {
+            return false;
+        }
+
+        $configuration = array();
+        $configuration['servers'] = explode("\n", $config->testservers);
+
+        $store = new cachestore_memcache('Test memcache', $configuration);
+        $store->initialise($definition);
+
+        return $store;
+    }
+
+    /**
+     * Returns the name of this instance.
+     * @return string
+     */
+    public function my_name() {
+        return $this->name;
+    }
+}
\ No newline at end of file
diff --git a/cache/stores/memcache/settings.php b/cache/stores/memcache/settings.php
new file mode 100644 (file)
index 0000000..f8229c5
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The settings for the memcache store.
+ *
+ * This file is part of the memcache cache store, it contains the API for interacting with an instance of the store.
+ *
+ * @package    cachestore_memcache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+$settings->add(new admin_setting_configtextarea(
+        'cachestore_memcache/testservers',
+        new lang_string('testservers', 'cachestore_memcache'),
+        new lang_string('testservers_desc', 'cachestore_memcache'),
+        '', PARAM_RAW, 60, 3));
\ No newline at end of file
diff --git a/cache/stores/memcache/version.php b/cache/stores/memcache/version.php
new file mode 100644 (file)
index 0000000..fc2b1fc
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Cache memcache store version information.
+ *
+ * Not to be confused with the memcached plugin.
+ *
+ * @package    cachestore_memcache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+$plugin->version = 2012091000;    // The current module version (Date: YYYYMMDDXX)
+$plugin->requires = 2012090700;    // Requires this Moodle version.
+$plugin->component = 'cachestore_memcache';  // Full name of the plugin.
\ No newline at end of file
diff --git a/cache/stores/memcached/addinstanceform.php b/cache/stores/memcached/addinstanceform.php
new file mode 100644 (file)
index 0000000..a8e98bf
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The library file for the memcached cache store.
+ *
+ * This file is part of the memcached cache store, it contains the API for interacting with an instance of the store.
+ *
+ * @package    cachestore_memcached
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot.'/cache/forms.php');
+require_once($CFG->dirroot.'/cache/stores/memcached/lib.php');
+
+/**
+ * Form for adding a memcached instance.
+ *
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cachestore_memcached_addinstance_form extends cachestore_addinstance_form {
+
+    /**
+     * Adds the desired form elements.
+     */
+    protected function configuration_definition() {
+        $form = $this->_form;
+
+        $form->addElement('textarea', 'servers', get_string('servers', 'cachestore_memcached'), array('cols' => 75, 'rows' => 5));
+        $form->addHelpButton('servers', 'servers', 'cachestore_memcached');
+        $form->addRule('servers', get_string('required'), 'required');
+        $form->setType('servers', PARAM_RAW);
+
+        $form->addElement('selectyesno', 'compression', get_string('usecompression', 'cachestore_memcached'));
+        $form->addHelpButton('compression', 'usecompression', 'cachestore_memcached');
+        $form->setDefault('compression', 1);
+        $form->setType('compression', PARAM_BOOL);
+
+        $serialiseroptions = cachestore_memcached::config_get_serialiser_options();
+        $form->addElement('select', 'serialiser', get_string('useserialiser', 'cachestore_memcached'), $serialiseroptions);
+        $form->addHelpButton('serialiser', 'useserialiser', 'cachestore_memcached');
+        $form->setDefault('serialiser', Memcached::SERIALIZER_PHP);
+        $form->setType('serialiser', PARAM_NUMBER);
+
+        $form->addElement('text', 'prefix', get_string('prefix', 'cachestore_memcached'), array('size' => 16));
+        $form->setType('prefix', PARAM_ALPHANUM);
+        $form->addHelpButton('prefix', 'prefix', 'cachestore_memcached');
+
+        $hashoptions = cachestore_memcached::config_get_hash_options();
+        $form->addElement('select', 'hash', get_string('hash', 'cachestore_memcached'), $hashoptions);
+        $form->addHelpButton('hash', 'hash', 'cachestore_memcached');
+        $form->setDefault('serialiser', Memcached::HASH_DEFAULT);
+        $form->setType('serialiser', PARAM_INT);
+
+        $form->addElement('selectyesno', 'bufferwrites', get_string('bufferwrites', 'cachestore_memcached'));
+        $form->addHelpButton('bufferwrites', 'bufferwrites', 'cachestore_memcached');
+        $form->setDefault('bufferwrites', 0);
+        $form->setType('bufferwrites', PARAM_BOOL);
+    }
+}
\ No newline at end of file
diff --git a/cache/stores/memcached/lang/en/cachestore_memcached.php b/cache/stores/memcached/lang/en/cachestore_memcached.php
new file mode 100644 (file)
index 0000000..67c3cd2
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The library file for the memcached cache store.
+ *
+ * This file is part of the memcached cache store, it contains the API for interacting with an instance of the store.
+ *
+ * @package    cachestore_memcached
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['bufferwrites'] = 'Buffer writes';
+$string['bufferwrites_help'] = 'Enables or disables buffered I/O. Enabling buffered I/O causes storage commands to "buffer" instead of being sent. Any action that retrieves data causes this buffer to be sent to the remote connection. Quitting the connection or closing down the connection will also cause the buffered data to be pushed to the remote connection.';
+$string['hash'] = 'Hash method';
+$string['hash_help'] = 'Specifies the hashing algorithm used for the item keys. Each hash algorithm has its advantages and its disadvantages. Go with the default if you don\'t know or don\'t care.';
+$string['hash_default'] = 'Default (one-at-a-time)';
+$string['hash_md5'] = 'MD5';
+$string['hash_crc'] = 'CRC';
+$string['hash_fnv1_64'] = 'FNV1_64';
+$string['hash_fnv1a_64'] = 'FNV1A_64';
+$string['hash_fnv1_32'] = 'FNV1_32';
+$string['hash_fnv1a_32'] = 'FNV1A_32';
+$string['hash_hsieh'] = 'Hsieh';
+$string['hash_murmur'] = 'Murmur';
+$string['pluginname'] = 'Memcached';
+$string['prefix'] = 'Prefix key';
+$string['prefix_help'] = 'This can be used to create a "domain" for your item keys allowing you to create multiple memcached stores on a single memcached installation. It cannot be longer than 16 characters in order to ensure key length issues are not encountered.';
+$string['serialiser_igbinary'] = 'The igbinary serializer.';
+$string['serialiser_json'] = 'The JSON serializer.';
+$string['serialiser_php'] = 'The default PHP serializer.';
+$string['servers'] = 'Servers';
+$string['servers_help'] = 'This sets the servers that should be utilised by this memcached adapter.
+Servers should be defined one per line and consist of a server address and optionally a port and weight.
+If no port is provided then the default port (11211) is used.
+
+For example:
+<pre>
+server.url.com
+ipaddress:port
+servername:port:weight
+</pre>';
+$string['testservers'] = 'Test servers';
+$string['testservers_desc'] = 'The test servers get used for unit tests and for performance tests. It is entirely optional to set up test servers. Servers should be defined one per line and consist of a server address and optionally a port and weight.
+If no port is provided then the default port (11211) is used.';
+$string['usecompression'] = 'Use compression';
+$string['usecompression_help'] = 'Enables or disables payload compression. When enabled, item values longer than a certain threshold (currently 100 bytes) will be compressed during storage and decompressed during retrieval transparently.';
+$string['useserialiser'] = 'Use serialiser';
+$string['useserialiser_help'] = 'Specifies the serializer to use for serializing non-scalar values.
+The valid serializers are Memcached::SERIALIZER_PHP or Memcached::SERIALIZER_IGBINARY.
+The latter is supported only when memcached is configured with --enable-memcached-igbinary option and the igbinary extension is loaded.';
\ No newline at end of file
diff --git a/cache/stores/memcached/lib.php b/cache/stores/memcached/lib.php
new file mode 100644 (file)
index 0000000..af74c41
--- /dev/null
@@ -0,0 +1,460 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The library file for the memcached cache store.
+ *
+ * This file is part of the memcached cache store, it contains the API for interacting with an instance of the store.
+ *
+ * @package    cachestore_memcached
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The memcached store.
+ *
+ * (Not to be confused with the memcache store)
+ *
+ * Configuration options:
+ *      servers:        string: host:port:weight , ...
+ *      compression:    true, false
+ *      serialiser:     SERIALIZER_PHP, SERIALIZER_JSON, SERIALIZER_IGBINARY
+ *      prefix:         string: defaults to instance name
+ *      hashmethod:     HASH_DEFAULT, HASH_MD5, HASH_CRC, HASH_FNV1_64, HASH_FNV1A_64, HASH_FNV1_32,
+ *                      HASH_FNV1A_32, HASH_HSIEH, HASH_MURMUR
+ *      bufferwrites:   true, false
+ *
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cachestore_memcached implements cache_store {
+
+    /**
+     * The name of the store
+     * @var store
+     */
+    protected $name;
+
+    /**
+     * The memcached connection
+     * @var Memcached
+     */
+    protected $connection;
+
+    /**
+     * An array of servers to use during connection
+     * @var array
+     */
+    protected $servers = array();
+
+    /**
+     * The options used when establishing the connection
+     * @var array
+     */
+    protected $options = array();
+
+    /**
+     * True when this instance is ready to be initialised.
+     * @var bool
+     */
+    protected $isready = false;
+
+    /**
+     * The cache definition this store was initialised with.
+     * @var cache_definition
+     */
+    protected $definition;
+
+    /**
+     * Constructs the store instance.
+     *
+     * Noting that this function is not an initialisation. It is used to prepare the store for use.
+     * The store wi