Merge branch 'MDL-37802' of https://github.com/paulholden/moodle
authorAdrian Greeve <abgreeve@gmail.com>
Wed, 30 Sep 2020 05:52:26 +0000 (13:52 +0800)
committerAdrian Greeve <abgreeve@gmail.com>
Wed, 30 Sep 2020 05:52:26 +0000 (13:52 +0800)
56 files changed:
admin/settings/server.php
admin/tool/dataprivacy/classes/output/renderer.php
admin/tool/mobile/lang/en/deprecated.txt [new file with mode: 0644]
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/mobile/lib.php
badges/tests/badgeslib_test.php
cache/admin.php
cache/classes/administration_helper.php [new file with mode: 0644]
cache/classes/factory.php
cache/classes/helper.php
cache/classes/local/administration_display_helper.php [new file with mode: 0644]
cache/forms.php
cache/locallib.php
cache/renderer.php
cache/tests/administration_helper_test.php
cache/upgrade.txt
calendar/classes/external/export/token.php [new file with mode: 0644]
calendar/export.php
calendar/export_execute.php
calendar/lib.php
calendar/tests/lib_test.php
completion/completion_completion.php
config-dist.php
course/externallib.php
course/tests/externallib_test.php
course/upgrade.txt
install/lang/ar/error.php
install/lang/ar/moodle.php
lang/en/admin.php
lang/en/completion.php
lang/en/moodle.php
lib/classes/user.php
lib/db/messages.php
lib/db/services.php
lib/externallib.php
lib/moodlelib.php
lib/tests/completionlib_test.php
lib/tests/moodlelib_test.php
mod/folder/lib.php
question/type/essay/backup/moodle2/backup_qtype_essay_plugin.class.php
question/type/essay/db/install.xml
question/type/essay/db/upgrade.php
question/type/essay/edit_essay_form.php
question/type/essay/lang/en/qtype_essay.php
question/type/essay/question.php
question/type/essay/questiontype.php
question/type/essay/renderer.php
question/type/essay/styles.css
question/type/essay/tests/behat/max_file_size.feature [new file with mode: 0644]
question/type/essay/tests/helper.php
question/type/essay/version.php
theme/boost/scss/moodle/admin.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
version.php
webservice/lib.php

index 1c3ee18..cb586b4 100644 (file)
@@ -444,6 +444,17 @@ if ($hassiteconfig) {
         new lang_string('configallowedemaildomains', 'admin'),
         ''));
 
+    $temp->add(new admin_setting_heading('divertallemailsheading', new lang_string('divertallemails', 'admin'),
+        new lang_string('divertallemailsdetail', 'admin')));
+    $temp->add(new admin_setting_configtext('divertallemailsto',
+        new lang_string('divertallemailsto', 'admin'),
+        new lang_string('divertallemailsto_desc', 'admin'),
+        ''));
+    $temp->add(new admin_setting_configtextarea('divertallemailsexcept',
+        new lang_string('divertallemailsexcept', 'admin'),
+        new lang_string('divertallemailsexcept_desc', 'admin'),
+        '', PARAM_RAW, '50', '4'));
+
     $url = new moodle_url('/admin/testoutgoingmailconf.php');
     $link = html_writer::link($url, get_string('testoutgoingmailconf', 'admin'));
     $temp->add(new admin_setting_heading('testoutgoinmailc', new lang_string('testoutgoingmailconf', 'admin'),
index 341bc8a..8032906 100644 (file)
@@ -63,7 +63,6 @@ class renderer extends plugin_renderer_base {
         $params = [
             'data-action' => 'contactdpo',
             'data-replytoemail' => $replytoemail,
-            'class' => 'contactdpo'
         ];
         return html_writer::link('#', get_string('contactdataprotectionofficer', 'tool_dataprivacy'), $params);
     }
diff --git a/admin/tool/mobile/lang/en/deprecated.txt b/admin/tool/mobile/lang/en/deprecated.txt
new file mode 100644 (file)
index 0000000..0edc136
--- /dev/null
@@ -0,0 +1 @@
+mobileappconnected,tool_mobile
\ No newline at end of file
index 678a1b5..6fafc5f 100644 (file)
@@ -88,7 +88,6 @@ $string['managefiletypes'] = 'Manage file types';
 $string['minimumversion'] = 'If an app version is specified (3.8.0 or higher), any users using an older app version will be prompted to upgrade their app before being allowed access to the site.';
 $string['minimumversion_key'] = 'Minimum app version required';
 $string['mobileapp'] = 'Mobile app';
-$string['mobileappconnected'] = 'Mobile app connected';
 $string['mobileappenabled'] = 'This site has mobile app access enabled.<br /><a href="{$a}">Download the mobile app</a>.';
 $string['mobileappearance'] = 'Mobile appearance';
 $string['mobileappsubscription'] = 'Moodle app subscription';
@@ -144,3 +143,6 @@ $string['privacy:metadata:preference:tool_mobile_autologin_request_last'] = 'The
 $string['privacy:metadata:core_userkey'] = 'User\'s keys used to create auto-login key for the current user.';
 $string['responsivemainmenuitems'] = 'Responsive menu items';
 $string['viewqrcode'] = 'View QR code';
+
+// Deprecated since Moodle 3.10.
+$string['mobileappconnected'] = 'Mobile app connected';
index 43d6cc2..567af8b 100644 (file)
@@ -87,22 +87,34 @@ function tool_mobile_create_app_download_url() {
 }
 
 /**
- * Checks if the given user has a mobile token (has used recently the app).
+ * Return the user mobile app WebService access token.
  *
- * @param  int $userid the user to check
- * @return bool        true if the user has a token, false otherwise.
+ * @param  int $userid the user to return the token from
+ * @return stdClass|false the token or false if the token doesn't exists
+ * @since  3.10
  */
-function tool_mobile_user_has_token($userid) {
+function tool_mobile_get_token($userid) {
     global $DB;
 
-    $sql = "SELECT 1
+    $sql = "SELECT t.*
               FROM {external_tokens} t, {external_services} s
              WHERE t.externalserviceid = s.id
                AND s.enabled = 1
                AND s.shortname IN ('moodle_mobile_app', 'local_mobile')
                AND t.userid = ?";
 
-    return $DB->record_exists_sql($sql, [$userid]);
+    return $DB->get_record_sql($sql, [$userid], IGNORE_MULTIPLE);
+}
+
+/**
+ * Checks if the given user has a mobile token (has used recently the app).
+ *
+ * @param  int $userid the user to check
+ * @return bool true if the user has a token, false otherwise.
+ */
+function tool_mobile_user_has_token($userid) {
+
+    return !empty(tool_mobile_get_token($userid));
 }
 
 /**
@@ -162,17 +174,25 @@ function tool_mobile_myprofile_navigation(\core_user\output\myprofile\tree $tree
     }
 
     // Check if the user is using the app, encouraging him to use it otherwise.
-    $userhastoken = tool_mobile_user_has_token($user->id);
+    $usertoken = tool_mobile_get_token($user->id);
     $mobilestrconnected = null;
-
-    if ($userhastoken) {
-        $mobilestrconnected = get_string('mobileappconnected', 'tool_mobile');
+    $mobilelastaccess = null;
+
+    if ($usertoken) {
+        $mobilestrconnected = get_string('lastsiteaccess');
+        if ($usertoken->lastaccess) {
+            $mobilelastaccess = userdate($usertoken->lastaccess) . "&nbsp; (" . format_time(time() - $usertoken->lastaccess) . ")";
+        } else {
+            // We should not reach this point.
+            $mobilelastaccess = get_string("never");
+        }
     } else if ($url = tool_mobile_create_app_download_url()) {
          $mobilestrconnected = get_string('mobileappenabled', 'tool_mobile', $url->out());
     }
 
     if ($mobilestrconnected) {
-        $newnodes[] = new core_user\output\myprofile\node('mobile', 'mobileappnode', $mobilestrconnected, null);
+        $newnodes[] = new core_user\output\myprofile\node('mobile', 'mobileappnode', $mobilestrconnected, null, null,
+            $mobilelastaccess);
     }
 
     // Add nodes, if any.
index 596c67e..c4ec0e1 100644 (file)
@@ -570,9 +570,13 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
         $this->assertFalse($badge->is_issued($this->user->id));
 
         // Mark course as complete.
-        $sink = $this->redirectEmails();
+        $sink = $this->redirectMessages();
         $ccompletion->mark_complete();
-        $this->assertCount(1, $sink->get_messages());
+        // Two messages are generated: One for the course completed and the other one for the badge awarded.
+        $messages = $sink->get_messages();
+        $this->assertCount(2, $messages);
+        $this->assertEquals('badgerecipientnotice', $messages[0]->eventtype);
+        $this->assertEquals('coursecompleted', $messages[1]->eventtype);
         $sink->close();
 
         // Check if badge is awarded.
index f8931b6..86b730e 100644 (file)
@@ -42,263 +42,27 @@ if (empty($SESSION->cacheadminreparsedefinitions)) {
 $action = optional_param('action', null, PARAM_ALPHA);
 
 admin_externalpage_setup('cacheconfig');
-$context = context_system::instance();
+$adminhelper = cache_factory::instance()->get_administration_display_helper();
 
-$storeinstancesummaries = cache_administration_helper::get_store_instance_summaries();
-$storepluginsummaries = cache_administration_helper::get_store_plugin_summaries();
-$definitionsummaries = 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;
 $notifications = array();
-$notifysuccess = true;
+// Empty array to hold any form information returned from actions.
+$forminfo = [];
 
+// Handle page actions in admin helper class.
 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);
-            if (!$storepluginsummaries[$plugin]['canaddinstance']) {
-                print_error('ex_unmetstorerequirements', 'cache');
-            }
-            $mform = cache_administration_helper::get_add_store_form($plugin);
-            $title = get_string('addstore', 'cache', $storepluginsummaries[$plugin]['name']);
-            if ($mform->is_cancelled()) {
-                redirect($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', $storepluginsummaries[$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', $storepluginsummaries[$plugin]['name']);
-            if ($mform->is_cancelled()) {
-                redirect($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', $storepluginsummaries[$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, $storeinstancesummaries)) {
-                $notifysuccess = false;
-                $notifications[] = array(get_string('invalidstore', 'cache'), false);
-            } else if ($storeinstancesummaries[$store]['mappings'] > 0) {
-                $notifysuccess = false;
-                $notifications[] = array(get_string('deletestorehasmappings', 'cache'), false);
-            }
-
-            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', $storeinstancesummaries[$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_SAFEPATH);
-            if (!array_key_exists($definition, $definitionsummaries)) {
-                throw new cache_exception('Invalid cache definition requested');
-            }
-            $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 'editdefinitionsharing' :
-            $definition = required_param('definition', PARAM_SAFEPATH);
-            if (!array_key_exists($definition, $definitionsummaries)) {
-                throw new cache_exception('Invalid cache definition requested');
-            }
-            $title = get_string('editdefinitionsharing', 'cache', $definition);
-            $sharingoptions = $definitionsummaries[$definition]['sharingoptions'];
-            $customdata = array('definition' => $definition, 'sharingoptions' => $sharingoptions);
-            $mform = new cache_definition_sharing_form($PAGE->url, $customdata);
-            $mform->set_data(array(
-                'sharing' => $definitionsummaries[$definition]['selectedsharingoption'],
-                'userinputsharingkey' => $definitionsummaries[$definition]['userinputsharingkey']
-            ));
-            if ($mform->is_cancelled()) {
-                redirect($PAGE->url);
-            } else if ($data = $mform->get_data()) {
-                $component = $definitionsummaries[$definition]['component'];
-                $area = $definitionsummaries[$definition]['area'];
-                // Purge the stores removing stale data before we alter the sharing option.
-                cache_helper::purge_stores_used_by_definition($component, $area);
-                $writer = cache_config_writer::instance();
-                $sharing = array_sum(array_keys($data->sharing));
-                $userinputsharingkey = $data->userinputsharingkey;
-                $writer->set_definition_sharing($definition, $sharing, $userinputsharingkey);
-                redirect($PAGE->url);
-            }
-            break;
-        case 'editmodemappings': // Edit default mode mappings.
-            $mform = new cache_mode_mappings_form(null, $storeinstancesummaries);
-            $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 'purgedefinition': // Purge a specific definition.
-            $id = required_param('definition', PARAM_SAFEPATH);
-            list($component, $area) = explode('/', $id, 2);
-            $factory = cache_factory::instance();
-            $definition = $factory->create_definition($component, $area);
-            if ($definition->has_required_identifiers()) {
-                // We will have to purge the stores used by this definition.
-                cache_helper::purge_stores_used_by_definition($component, $area);
-            } else {
-                // Alrighty we can purge just the data belonging to this definition.
-                cache_helper::purge_by_definition($component, $area);
-            }
-
-            $message = get_string('purgexdefinitionsuccess', 'cache', [
-                        'name' => $definition->get_name(),
-                        'component' => $component,
-                        'area' => $area,
-                    ]);
-            $purgeagainlink = html_writer::link(new moodle_url('/cache/admin.php', [
-                    'action' => 'purgedefinition', 'sesskey' => sesskey(), 'definition' => $id]),
-                    get_string('purgeagain', 'cache'));
-            redirect($PAGE->url, $message . ' ' . $purgeagainlink, 5);
-            break;
-
-        case 'purgestore':
-        case 'purge': // Purge a store cache.
-            $store = required_param('store', PARAM_TEXT);
-            cache_helper::purge_store($store);
-            $message = get_string('purgexstoresuccess', 'cache', ['store' => $store]);
-            $purgeagainlink = html_writer::link(new moodle_url('/cache/admin.php', [
-                    'action' => 'purgestore', 'sesskey' => sesskey(), 'store' => $store]),
-                    get_string('purgeagain', 'cache'));
-            redirect($PAGE->url, $message . ' ' . $purgeagainlink, 5);
-            break;
-
-        case 'newlockinstance':
-            // Adds a new lock instance.
-            $lock = required_param('lock', PARAM_ALPHANUMEXT);
-            $mform = cache_administration_helper::get_add_lock_form($lock);
-            if ($mform->is_cancelled()) {
-                redirect($PAGE->url);
-            } else if ($data = $mform->get_data()) {
-                $factory = cache_factory::instance();
-                $config = $factory->create_config_instance(true);
-                $name = $data->name;
-                $data = cache_administration_helper::get_lock_configuration_from_data($lock, $data);
-                $config->add_lock_instance($name, $lock, $data);
-                redirect($PAGE->url, get_string('addlocksuccess', 'cache', $name), 5);
-            }
-            break;
-        case 'deletelock':
-            // Deletes a lock instance.
-            $lock = required_param('lock', PARAM_ALPHANUMEXT);
-            $confirm = optional_param('confirm', false, PARAM_BOOL);
-            if (!array_key_exists($lock, $locks)) {
-                $notifysuccess = false;
-                $notifications[] = array(get_string('invalidlock', 'cache'), false);
-            } else if ($locks[$lock]['uses'] > 0) {
-                $notifysuccess = false;
-                $notifications[] = array(get_string('deletelockhasuses', 'cache'), false);
-            }
-            if ($notifysuccess) {
-                if (!$confirm) {
-                    $title = get_string('confirmlockdeletion', 'cache');
-                    $params = array('lock' => $lock, 'confirm' => 1, 'action' => $action, 'sesskey' => sesskey());
-                    $url = new moodle_url($PAGE->url, $params);
-                    $button = new single_button($url, get_string('deletelock', 'cache'));
-
-                    $PAGE->set_title($title);
-                    $PAGE->set_heading($SITE->fullname);
-                    echo $OUTPUT->header();
-                    echo $OUTPUT->heading($title);
-                    $confirmation = get_string('deletelockconfirmation', 'cache', $lock);
-                    echo $OUTPUT->confirm($confirmation, $button, $PAGE->url);
-                    echo $OUTPUT->footer();
-                    exit;
-                } else {
-                    $writer = cache_config_writer::instance();
-                    $writer->delete_lock_instance($lock);
-                    redirect($PAGE->url, get_string('deletelocksuccess', 'cache'), 5);
-                }
-            }
-            break;
-    }
+    $forminfo = $adminhelper->perform_cache_actions($action, $forminfo);
 }
 
 // Add cache store warnings to the list of notifications.
 // Obviously as these are warnings they are show as failures.
-foreach (cache_helper::warnings($storeinstancesummaries) as $warning) {
+foreach (cache_helper::warnings(core_cache\administration_helper::get_store_instance_summaries()) as $warning) {
     $notifications[] = array($warning, false);
 }
 
+// Decide on display mode based on returned forminfo.
+$mform = array_key_exists('form', $forminfo) ? $forminfo['form'] : null;
+$title = array_key_exists('title', $forminfo) ? $forminfo['title'] : new lang_string('cacheadmin', 'cache');
+
 $PAGE->set_title($title);
 $PAGE->set_heading($SITE->fullname);
 /* @var core_cache_renderer $renderer */
@@ -311,16 +75,8 @@ echo $renderer->notifications($notifications);
 if ($mform instanceof moodleform) {
     $mform->display();
 } else {
-    echo $renderer->store_plugin_summaries($storepluginsummaries);
-    echo $renderer->store_instance_summariers($storeinstancesummaries, $storepluginsummaries);
-    echo $renderer->definition_summaries($definitionsummaries, $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);
+    // Handle main page definition in admin helper class.
+    echo $adminhelper->generate_admin_page($renderer);
 }
 
 echo $renderer->footer();
diff --git a/cache/classes/administration_helper.php b/cache/classes/administration_helper.php
new file mode 100644 (file)
index 0000000..551e62c
--- /dev/null
@@ -0,0 +1,389 @@
+<?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 administration helper.
+ *
+ * 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
+ * @author     Peter Burnett <peterburnett@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_cache;
+
+defined('MOODLE_INTERNAL') || die();
+use cache_helper, cache_store, cache_config, cache_factory, cache_definition;
+
+/**
+ * Administration helper base class.
+ *
+ * Defines abstract methods for a subclass to define the admin page.
+ *
+ * @package     core
+ * @category    cache
+ * @author      Peter Burnett <peterburnett@catalyst-au.net>
+ * @copyright   2020 Catalyst IT
+ * @copyright  2012 Sam Hemelryk
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class 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(): array {
+        $return = array();
+        $default = array();
+        $instance = \cache_config::instance();
+        $stores = $instance->get_all_stores();
+        $locks = $instance->get_locks();
+        foreach ($stores as $name => $details) {
+            $class = $details['class'];
+            $store = false;
+            if ($class::are_requirements_met()) {
+                $store = new $class($details['name'], $details['configuration']);
+            }
+            $lock = (isset($details['lock'])) ? $locks[$details['lock']] : $instance->get_default_lock();
+            $record = array(
+                'name' => $name,
+                'plugin' => $details['plugin'],
+                'default' => $details['default'],
+                'isready' => $store ? $store->is_ready() : false,
+                'requirementsmet' => $class::are_requirements_met(),
+                'mappings' => 0,
+                'lock' => $lock,
+                'modes' => array(
+                    cache_store::MODE_APPLICATION =>
+                        ($class::get_supported_modes($return) & cache_store::MODE_APPLICATION) == cache_store::MODE_APPLICATION,
+                    cache_store::MODE_SESSION =>
+                        ($class::get_supported_modes($return) & cache_store::MODE_SESSION) == cache_store::MODE_SESSION,
+                    cache_store::MODE_REQUEST =>
+                        ($class::get_supported_modes($return) & cache_store::MODE_REQUEST) == cache_store::MODE_REQUEST,
+                ),
+                'supports' => array(
+                    'multipleidentifiers' => $store ? $store->supports_multiple_identifiers() : false,
+                    'dataguarantee' => $store ? $store->supports_data_guarantee() : false,
+                    'nativettl' => $store ? $store->supports_native_ttl() : false,
+                    'nativelocking' => ($store instanceof \cache_is_lockable),
+                    'keyawareness' => ($store instanceof \cache_is_key_aware),
+                    'searchable' => ($store instanceof \cache_is_searchable)
+                ),
+                'warnings' => $store ? $store->get_warnings() : array()
+            );
+            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 for each store, an array containing various information about each store.
+     *     See the code below for details
+     */
+    public static function get_store_plugin_summaries(): array {
+        $return = array();
+        $plugins = \core_component::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() && $class::are_requirements_met())
+            );
+        }
+
+        $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 for each store, an array containing various information about each store.
+     *     See the code below for details
+     */
+    public static function get_definition_summaries(): array {
+        $factory = cache_factory::instance();
+        $config = $factory->create_config_instance();
+        $storenames = array();
+        foreach ($config->get_all_stores() as $key => $store) {
+            if (!empty($store['default'])) {
+                $storenames[$key] = new \lang_string('store_'.$key, 'cache');
+            } else {
+                $storenames[$store['name']] = $store['name'];
+            }
+        }
+        /* @var cache_definition[] $definitions */
+        $definitions = [];
+        $return = [];
+        foreach ($config->get_definitions() as $key => $definition) {
+            $definitions[$key] = cache_definition::load($definition['component'].'/'.$definition['area'], $definition);
+        }
+        foreach ($definitions as $id => $definition) {
+            $mappings = array();
+            foreach (cache_helper::get_stores_suitable_for_definition($definition) as $store) {
+                $mappings[] = $storenames[$store->my_name()];
+            }
+            $return[$id] = array(
+                'id' => $id,
+                'name' => $definition->get_name(),
+                'mode' => $definition->get_mode(),
+                'component' => $definition->get_component(),
+                'area' => $definition->get_area(),
+                'mappings' => $mappings,
+                'canuselocalstore' => $definition->can_use_localstore(),
+                'sharingoptions' => self::get_definition_sharing_options($definition->get_sharing_options(), false),
+                'selectedsharingoption' => self::get_definition_sharing_options($definition->get_selected_sharing_option(), true),
+                'userinputsharingkey' => $definition->get_user_input_sharing_key()
+            );
+        }
+        return $return;
+    }
+
+    /**
+     * 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(): array {
+        global $OUTPUT;
+        $instance = cache_config::instance();
+        $adequatestores = cache_helper::get_stores_suitable_for_mode_default();
+        $icon = new \pix_icon('i/warning', new \lang_string('inadequatestoreformapping', 'cache'));
+        $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'];
+            }
+            if (!array_key_exists($mapping['store'], $adequatestores)) {
+                $modemappings[$mode][$mapping['store']] = $modemappings[$mode][$mapping['store']].' '.$OUTPUT->render($icon);
+            }
+        }
+        return $modemappings;
+    }
+
+    /**
+     * Returns an array summarising the locks available in the system.
+     *
+     * @return array array of lock summaries.
+     */
+    public static function get_lock_summaries(): array {
+        $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,
+                'type' => get_string('pluginname', $lock['type'])
+            );
+            $locks[$lock['name']] = $lockdata;
+        }
+        return $locks;
+    }
+
+    /**
+     * Given a sharing option hash this function returns an array of strings that can be used to describe it.
+     *
+     * @param int $sharingoption The sharing option hash to get strings for.
+     * @param bool $isselectedoptions Set to true if the strings will be used to view the selected options.
+     * @return array An array of lang_string's.
+     */
+    public static function get_definition_sharing_options(int $sharingoption, bool $isselectedoptions = true): array {
+        $options = array();
+        $prefix = ($isselectedoptions) ? 'sharingselected' : 'sharing';
+        if ($sharingoption & cache_definition::SHARING_ALL) {
+            $options[cache_definition::SHARING_ALL] = new \lang_string($prefix.'_all', 'cache');
+        }
+        if ($sharingoption & cache_definition::SHARING_SITEID) {
+            $options[cache_definition::SHARING_SITEID] = new \lang_string($prefix.'_siteid', 'cache');
+        }
+        if ($sharingoption & cache_definition::SHARING_VERSION) {
+            $options[cache_definition::SHARING_VERSION] = new \lang_string($prefix.'_version', 'cache');
+        }
+        if ($sharingoption & cache_definition::SHARING_INPUT) {
+            $options[cache_definition::SHARING_INPUT] = new \lang_string($prefix.'_input', 'cache');
+        }
+        return $options;
+    }
+
+    /**
+     * 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(string $component, string $area): array {
+        $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);
+    }
+
+    /**
+     * This function must be implemented to display options for store plugins.
+     *
+     * @param string $name the name of the store plugin.
+     * @param array $plugindetails array of store plugin details.
+     * @return array array of actions.
+     */
+    public function get_store_plugin_actions(string $name, array $plugindetails): array {
+        return array();
+    }
+
+    /**
+     * This function must be implemented to display options for store instances.
+     *
+     * @param string $name the store instance name.
+     * @param array $storedetails array of store instance details.
+     * @return array array of actions.
+     */
+    public function get_store_instance_actions(string $name, array $storedetails): array {
+        return array();
+    }
+
+    /**
+     * This function must be implemented to display options for definition mappings.
+     *
+     * @param context $context the context for the definition.
+     * @param array $definitionsummary the definition summary.
+     * @return array array of actions.
+     */
+    public function get_definition_actions(\context $context, array $definitionsummary): array {
+        return array();
+    }
+
+    /**
+     * This function must be implemented to get addable locks.
+     *
+     * @return array array of locks that are addable.
+     */
+    public function get_addable_lock_options(): array {
+        return array();
+    }
+
+    /**
+     * This function must be implemented to perform any page actions by a child class.
+     *
+     * @param string $action the action to perform.
+     * @param array $forminfo empty array to be set by actions.
+     * @return array array of form info.
+     */
+    public abstract function perform_cache_actions(string $action, array $forminfo): array;
+
+    /**
+     * This function must be implemented to display the cache admin page.
+     *
+     * @param core_cache_renderer $renderer the renderer used to generate the page.
+     * @return string the HTML for the page.
+     */
+    public abstract function generate_admin_page(\core_cache_renderer $renderer): string;
+}
index a974377..9791c47 100644 (file)
@@ -112,7 +112,13 @@ class cache_factory {
     protected $state = 0;
 
     /**
-     * Returns an instance of the cache_factor method.
+     * The current cache display helper.
+     * @var core_cache\local\administration_display_helper
+     */
+    protected static $displayhelper = null;
+
+    /**
+     * Returns an instance of the cache_factory class.
      *
      * @param bool $forcereload If set to true a new cache_factory instance will be created and used.
      * @return cache_factory
@@ -134,6 +140,10 @@ class cache_factory {
                     // The cache stores have been disabled.
                     self::$instance->set_state(self::STATE_STORES_DISABLED);
                 }
+
+            } else if (!empty($CFG->alternative_cache_factory_class)) {
+                $factoryclass = $CFG->alternative_cache_factory_class;
+                self::$instance = new $factoryclass();
             } else {
                 // We're using the regular factory.
                 self::$instance = new cache_factory();
@@ -636,4 +646,16 @@ class cache_factory {
         $factory->reset_cache_instances();
         $factory->set_state(self::STATE_STORES_DISABLED);
     }
+
+    /**
+     * Returns an instance of the current display_helper.
+     *
+     * @return core_cache\administration_helper
+     */
+    public static function get_administration_display_helper() : core_cache\administration_helper {
+        if (is_null(self::$displayhelper)) {
+            self::$displayhelper = new \core_cache\local\administration_display_helper();
+        }
+        return self::$displayhelper;
+    }
 }
index dc4821b..50643fe 100644 (file)
@@ -829,7 +829,7 @@ class cache_helper {
         global $CFG;
         if ($stores === null) {
             require_once($CFG->dirroot.'/cache/locallib.php');
-            $stores = cache_administration_helper::get_store_instance_summaries();
+            $stores = core_cache\administration_helper::get_store_instance_summaries();
         }
         $warnings = array();
         foreach ($stores as $store) {
diff --git a/cache/classes/local/administration_display_helper.php b/cache/classes/local/administration_display_helper.php
new file mode 100644 (file)
index 0000000..1e2aff7
--- /dev/null
@@ -0,0 +1,795 @@
+<?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 display administration helper.
+ *
+ * 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
+ * @author     Peter Burnett <peterburnett@catalyst-au.net>
+ * @copyright  2020 Catalyst IT
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_cache\local;
+
+defined('MOODLE_INTERNAL') || die();
+use cache_store, cache_factory, cache_config_writer, cache_helper, core_cache_renderer;
+
+/**
+ * A cache helper for administration tasks
+ *
+ * @package    core
+ * @category   cache
+ * @copyright  2020 Peter Burnett <peterburnett@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class administration_display_helper extends \core_cache\administration_helper {
+
+    /**
+     * Please do not call constructor directly. Use cache_factory::get_administration_display_helper() instead.
+     */
+    public function __construct() {
+        // Nothing to do here.
+    }
+
+    /**
+     * Returns all of the actions that can be performed on a definition.
+     *
+     * @param context $context the system context.
+     * @param array $definitionsummary information about this cache, from the array returned by
+     *      core_cache\administration_helper::get_definition_summaries(). Currently only 'sharingoptions'
+     *      element is used.
+     * @return array of actions. Each action is an action_url.
+     */
+    public function get_definition_actions(\context $context, array $definitionsummary): array {
+        global $OUTPUT;
+        if (has_capability('moodle/site:config', $context)) {
+            $actions = array();
+            // Edit mappings.
+            $actions[] = $OUTPUT->action_link(
+                new \moodle_url('/cache/admin.php', array('action' => 'editdefinitionmapping',
+                    'definition' => $definitionsummary['id'], 'sesskey' => sesskey())),
+                get_string('editmappings', 'cache')
+            );
+            // Edit sharing.
+            if (count($definitionsummary['sharingoptions']) > 1) {
+                $actions[] = $OUTPUT->action_link(
+                    new \moodle_url('/cache/admin.php', array('action' => 'editdefinitionsharing',
+                        'definition' => $definitionsummary['id'], 'sesskey' => sesskey())),
+                    get_string('editsharing', 'cache')
+                );
+            }
+            // Purge.
+            $actions[] = $OUTPUT->action_link(
+                new \moodle_url('/cache/admin.php', array('action' => 'purgedefinition',
+                    'definition' => $definitionsummary['id'], 'sesskey' => sesskey())),
+                get_string('purge', 'cache')
+            );
+            return $actions;
+        }
+        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 information about this store, from the array returned by
+     *      core_cache\administration_helper::get_store_instance_summaries().
+     * @return array of actions. Each action is an action_url.
+     */
+    public function get_store_instance_actions(string $name, array $storedetails): array {
+        global $OUTPUT;
+        $actions = array();
+        if (has_capability('moodle/site:config', \context_system::instance())) {
+            $baseurl = new \moodle_url('/cache/admin.php', array('store' => $name, 'sesskey' => sesskey()));
+            if (empty($storedetails['default'])) {
+                $actions[] = $OUTPUT->action_link(
+                    new \moodle_url($baseurl, array('action' => 'editstore', 'plugin' => $storedetails['plugin'])),
+                    get_string('editstore', 'cache')
+                );
+
+                $actions[] = $OUTPUT->action_link(
+                    new \moodle_url($baseurl, array('action' => 'deletestore')),
+                    get_string('deletestore', 'cache')
+                );
+            }
+
+            $actions[] = $OUTPUT->action_link(
+                new \moodle_url($baseurl, array('action' => 'purgestore')),
+                get_string('purge', 'cache')
+            );
+        }
+        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 information about this store, from the array returned by
+     *      core_cache\administration_helper::get_store_plugin_summaries().
+     * @return array of actions. Each action is an action_url.
+     */
+    public function get_store_plugin_actions(string $name, array $plugindetails): array {
+        global $OUTPUT;
+        $actions = array();
+        if (has_capability('moodle/site:config', \context_system::instance())) {
+            if (!empty($plugindetails['canaddinstance'])) {
+                $url = new \moodle_url('/cache/admin.php',
+                    array('action' => 'addstore', 'plugin' => $name, 'sesskey' => sesskey()));
+                $actions[] = $OUTPUT->action_link(
+                    $url,
+                    get_string('addinstance', 'cache')
+                );
+            }
+        }
+        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 function get_add_store_form(string $plugin): \cachestore_addinstance_form {
+        global $CFG; // Needed for includes.
+        $plugins = \core_component::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 = $this->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 function get_edit_store_form(string $plugin, string $store): \cachestore_addinstance_form {
+        global $CFG; // Needed for includes.
+        $plugins = \core_component::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 = $this->get_possible_locks_for_stores($plugindir, $plugin);
+
+        $url = new \moodle_url('/cache/admin.php', array('action' => 'editstore', 'plugin' => $plugin, 'store' => $store));
+        $editform = new $class($url, array('plugin' => $plugin, 'store' => $store, 'locks' => $locks));
+        if (isset($stores[$store]['lock'])) {
+            $editform->set_data(array('lock' => $stores[$store]['lock']));
+        }
+        // See if the cachestore is going to want to load data for the form.
+        // If it has a customised add instance form then it is going to want to.
+        $storeclass = 'cachestore_'.$plugin;
+        $storedata = $stores[$store];
+        if (array_key_exists('configuration', $storedata) &&
+            array_key_exists('cache_is_configurable', class_implements($storeclass))) {
+            $storeclass::config_set_edit_form_data($editform, $storedata['configuration']);
+        }
+        return $editform;
+    }
+
+    /**
+     * 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 function get_possible_locks_for_stores(string $plugindir, string $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 function get_store_configuration_from_data(\stdClass $data): array {
+        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;
+        if (!class_exists($class)) {
+            throw new \coding_exception('Invalid cache plugin provided.');
+        }
+        if (array_key_exists('cache_is_configurable', class_implements($class))) {
+            return $class::config_get_configuration_array($data);
+        }
+        return array();
+    }
+
+    /**
+     * Returns an array of lock plugins for which we can add an instance.
+     *
+     * Suitable for use within an mform select element.
+     *
+     * @return array
+     */
+    public function get_addable_lock_options(): array {
+        $plugins = \core_component::get_plugin_list_with_class('cachelock', '', 'lib.php');
+        $options = array();
+        $len = strlen('cachelock_');
+        foreach ($plugins as $plugin => $class) {
+            $method = "$class::can_add_instance";
+            if (is_callable($method) && !call_user_func($method)) {
+                // Can't add an instance of this plugin.
+                continue;
+            }
+            $options[substr($plugin, $len)] = get_string('pluginname', $plugin);
+        }
+        return $options;
+    }
+
+    /**
+     * Gets the form to use when adding a lock instance.
+     *
+     * @param string $plugin
+     * @param array $lockplugin
+     * @return cache_lock_form
+     * @throws coding_exception
+     */
+    public function get_add_lock_form(string $plugin, array $lockplugin = null): \cache_lock_form {
+        global $CFG; // Needed for includes.
+        $plugins = \core_component::get_plugin_list('cachelock');
+        if (!array_key_exists($plugin, $plugins)) {
+            throw new \coding_exception('Invalid cache lock plugin requested when trying to create a form.');
+        }
+        $plugindir = $plugins[$plugin];
+        $class = 'cache_lock_form';
+        if (file_exists($plugindir.'/addinstanceform.php') && in_array('cache_is_configurable', class_implements($class))) {
+            require_once($plugindir.'/addinstanceform.php');
+            if (class_exists('cachelock_'.$plugin.'_addinstance_form')) {
+                $class = 'cachelock_'.$plugin.'_addinstance_form';
+                if (!array_key_exists('cache_lock_form', class_parents($class))) {
+                    throw new \coding_exception('Cache lock plugin add instance forms must extend cache_lock_form');
+                }
+            }
+        }
+        return new $class(null, array('lock' => $plugin));
+    }
+
+    /**
+     * Gets configuration data from a new lock instance form.
+     *
+     * @param string $plugin
+     * @param stdClass $data
+     * @return array
+     * @throws coding_exception
+     */
+    public function get_lock_configuration_from_data(string $plugin, \stdClass $data): array {
+        global $CFG;
+        $file = $CFG->dirroot.'/cache/locks/'.$plugin.'/lib.php';
+        if (!file_exists($file)) {
+            throw new \coding_exception('Invalid cache plugin provided. '.$file);
+        }
+        require_once($file);
+        $class = 'cachelock_'.$plugin;
+        if (!class_exists($class)) {
+            throw new \coding_exception('Invalid cache plugin provided.');
+        }
+        if (array_key_exists('cache_is_configurable', class_implements($class))) {
+            return $class::config_get_configuration_array($data);
+        }
+        return array();
+    }
+
+    /**
+     * Handles the page actions, based on the parameter.
+     *
+     * @param string $action the action to handle.
+     * @param array $forminfo an empty array to be overridden and set.
+     * @return array the empty or overridden forminfo array.
+     */
+    public function perform_cache_actions(string $action, array $forminfo): array {
+        switch ($action) {
+            case 'rescandefinitions' : // Rescan definitions.
+                $this->action_rescan_definition();
+                break;
+
+            case 'addstore' : // Add the requested store.
+                $forminfo = $this->action_addstore();
+                break;
+
+            case 'editstore' : // Edit the requested store.
+                $forminfo = $this->action_editstore();
+                break;
+
+            case 'deletestore' : // Delete a given store.
+                $this->action_deletestore($action);
+                break;
+
+            case 'editdefinitionmapping' : // Edit definition mappings.
+                $forminfo = $this->action_editdefinitionmapping();
+                break;
+
+            case 'editdefinitionsharing' : // Edit definition sharing.
+                $forminfo = $this->action_editdefinitionsharing();
+                break;
+
+            case 'editmodemappings': // Edit default mode mappings.
+                $forminfo = $this->action_editmodemappings();
+                break;
+
+            case 'purgedefinition': // Purge a specific definition.
+                $this->action_purgedefinition();
+                break;
+
+            case 'purgestore':
+            case 'purge': // Purge a store cache.
+                $this->action_purge();
+                break;
+
+            case 'newlockinstance':
+                $forminfo = $this->action_newlockinstance();
+                break;
+
+            case 'deletelock':
+                // Deletes a lock instance.
+                $this->action_deletelock($action);
+                break;
+        }
+
+        return $forminfo;
+    }
+
+    /**
+     * Performs the rescan definition action.
+     *
+     * @return void
+     */
+    public function action_rescan_definition() {
+        global $PAGE;
+
+        \cache_config_writer::update_definitions();
+        redirect($PAGE->url);
+    }
+
+    /**
+     * Performs the add store action.
+     *
+     * @return array an array of the form to display to the user, and the page title.
+     */
+    public function action_addstore() : array {
+        global $PAGE;
+        $storepluginsummaries = $this->get_store_plugin_summaries();
+
+        $plugin = required_param('plugin', PARAM_PLUGIN);
+        if (!$storepluginsummaries[$plugin]['canaddinstance']) {
+            print_error('ex_unmetstorerequirements', 'cache');
+        }
+        $mform = $this->get_add_store_form($plugin);
+        $title = get_string('addstore', 'cache', $storepluginsummaries[$plugin]['name']);
+        if ($mform->is_cancelled()) {
+            redirect($PAGE->url);
+        } else if ($data = $mform->get_data()) {
+            $config = $this->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', $storepluginsummaries[$plugin]['name']), 5);
+        }
+
+        return array('form' => $mform, 'title' => $title);
+    }
+
+    /**
+     * Performs the edit store action.
+     *
+     * @return array an array of the form to display, and the page title.
+     */
+    public function action_editstore(): array {
+        global $PAGE;
+        $storepluginsummaries = $this->get_store_plugin_summaries();
+
+        $plugin = required_param('plugin', PARAM_PLUGIN);
+        $store = required_param('store', PARAM_TEXT);
+        $mform = $this->get_edit_store_form($plugin, $store);
+        $title = get_string('addstore', 'cache', $storepluginsummaries[$plugin]['name']);
+        if ($mform->is_cancelled()) {
+            redirect($PAGE->url);
+        } else if ($data = $mform->get_data()) {
+            $config = $this->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', $storepluginsummaries[$plugin]['name']), 5);
+        }
+
+        return array('form' => $mform, 'title' => $title);
+    }
+
+    /**
+     * Performs the deletestore action.
+     *
+     * @param string $action the action calling to this function.
+     * @return void
+     */
+    public function action_deletestore(string $action) {
+        global $OUTPUT, $PAGE, $SITE;
+        $notifysuccess = true;
+        $storeinstancesummaries = $this->get_store_instance_summaries();
+
+        $store = required_param('store', PARAM_TEXT);
+        $confirm = optional_param('confirm', false, PARAM_BOOL);
+
+        if (!array_key_exists($store, $storeinstancesummaries)) {
+            $notifysuccess = false;
+            $notifications[] = array(get_string('invalidstore', 'cache'), false);
+        } else if ($storeinstancesummaries[$store]['mappings'] > 0) {
+            $notifysuccess = false;
+            $notifications[] = array(get_string('deletestorehasmappings', 'cache'), false);
+        }
+
+        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', $storeinstancesummaries[$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);
+            }
+        }
+    }
+
+    /**
+     * Performs the edit definition mapping action.
+     *
+     * @return array an array of the form to display, and the page title.
+     * @throws cache_exception
+     */
+    public function action_editdefinitionmapping(): array {
+        global $PAGE;
+        $definitionsummaries = $this->get_definition_summaries();
+
+        $definition = required_param('definition', PARAM_SAFEPATH);
+        if (!array_key_exists($definition, $definitionsummaries)) {
+            throw new \cache_exception('Invalid cache definition requested');
+        }
+        $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);
+        }
+
+        return array('form' => $mform, 'title' => $title);
+    }
+
+    /**
+     * Performs the edit definition sharing action.
+     *
+     * @return array an array of the edit definition sharing form, and the page title.
+     */
+    public function action_editdefinitionsharing(): array {
+        global $PAGE;
+        $definitionsummaries = $this->get_definition_summaries();
+
+        $definition = required_param('definition', PARAM_SAFEPATH);
+        if (!array_key_exists($definition, $definitionsummaries)) {
+            throw new \cache_exception('Invalid cache definition requested');
+        }
+        $title = get_string('editdefinitionsharing', 'cache', $definition);
+        $sharingoptions = $definitionsummaries[$definition]['sharingoptions'];
+        $customdata = array('definition' => $definition, 'sharingoptions' => $sharingoptions);
+        $mform = new \cache_definition_sharing_form($PAGE->url, $customdata);
+        $mform->set_data(array(
+            'sharing' => $definitionsummaries[$definition]['selectedsharingoption'],
+            'userinputsharingkey' => $definitionsummaries[$definition]['userinputsharingkey']
+        ));
+        if ($mform->is_cancelled()) {
+            redirect($PAGE->url);
+        } else if ($data = $mform->get_data()) {
+            $component = $definitionsummaries[$definition]['component'];
+            $area = $definitionsummaries[$definition]['area'];
+            // Purge the stores removing stale data before we alter the sharing option.
+            \cache_helper::purge_stores_used_by_definition($component, $area);
+            $writer = \cache_config_writer::instance();
+            $sharing = array_sum(array_keys($data->sharing));
+            $userinputsharingkey = $data->userinputsharingkey;
+            $writer->set_definition_sharing($definition, $sharing, $userinputsharingkey);
+            redirect($PAGE->url);
+        }
+
+        return array('form' => $mform, 'title' => $title);
+    }
+
+    /**
+     * Performs the edit mode mappings action.
+     *
+     * @return array an array of the edit mode mappings form.
+     */
+    public function action_editmodemappings(): array {
+        global $PAGE;
+        $storeinstancesummaries = $this->get_store_instance_summaries();
+        $defaultmodestores = $this->get_default_mode_stores();
+
+        $mform = new \cache_mode_mappings_form(null, $storeinstancesummaries);
+        $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);
+        }
+
+        return array('form' => $mform);
+    }
+
+    /**
+     * Performs the purge definition action.
+     *
+     * @return void
+     */
+    public function action_purgedefinition() {
+        global $PAGE;
+
+        $id = required_param('definition', PARAM_SAFEPATH);
+        list($component, $area) = explode('/', $id, 2);
+        $factory = cache_factory::instance();
+        $definition = $factory->create_definition($component, $area);
+        if ($definition->has_required_identifiers()) {
+            // We will have to purge the stores used by this definition.
+            cache_helper::purge_stores_used_by_definition($component, $area);
+        } else {
+            // Alrighty we can purge just the data belonging to this definition.
+            cache_helper::purge_by_definition($component, $area);
+        }
+
+        $message = get_string('purgexdefinitionsuccess', 'cache', [
+                    'name' => $definition->get_name(),
+                    'component' => $component,
+                    'area' => $area,
+                ]);
+        $purgeagainlink = \html_writer::link(new \moodle_url('/cache/admin.php', [
+                'action' => 'purgedefinition', 'sesskey' => sesskey(), 'definition' => $id]),
+                get_string('purgeagain', 'cache'));
+        redirect($PAGE->url, $message . ' ' . $purgeagainlink, 5);
+    }
+
+    /**
+     * Performs the purge action.
+     *
+     * @return void
+     */
+    public function action_purge() {
+        global $PAGE;
+
+        $store = required_param('store', PARAM_TEXT);
+        cache_helper::purge_store($store);
+        $message = get_string('purgexstoresuccess', 'cache', ['store' => $store]);
+        $purgeagainlink = \html_writer::link(new \moodle_url('/cache/admin.php', [
+                'action' => 'purgestore', 'sesskey' => sesskey(), 'store' => $store]),
+                get_string('purgeagain', 'cache'));
+        redirect($PAGE->url, $message . ' ' . $purgeagainlink, 5);
+    }
+
+    /**
+     * Performs the new lock instance action.
+     *
+     * @return array An array containing the new lock instance form.
+     */
+    public function action_newlockinstance(): array {
+        global $PAGE;
+
+        // Adds a new lock instance.
+        $lock = required_param('lock', PARAM_ALPHANUMEXT);
+        $mform = $this->get_add_lock_form($lock);
+        if ($mform->is_cancelled()) {
+            redirect($PAGE->url);
+        } else if ($data = $mform->get_data()) {
+            $factory = cache_factory::instance();
+            $config = $factory->create_config_instance(true);
+            $name = $data->name;
+            $data = $this->get_lock_configuration_from_data($lock, $data);
+            $config->add_lock_instance($name, $lock, $data);
+            redirect($PAGE->url, get_string('addlocksuccess', 'cache', $name), 5);
+        }
+
+        return array('form' => $mform);
+    }
+
+    /**
+     * Performs the delete lock action.
+     *
+     * @param string $action the action calling this function.
+     * @return void
+     */
+    public function action_deletelock(string $action) {
+        global $OUTPUT, $PAGE, $SITE;
+        $notifysuccess = true;
+        $locks = $this->get_lock_summaries();
+
+        $lock = required_param('lock', PARAM_ALPHANUMEXT);
+        $confirm = optional_param('confirm', false, PARAM_BOOL);
+        if (!array_key_exists($lock, $locks)) {
+            $notifysuccess = false;
+            $notifications[] = array(get_string('invalidlock', 'cache'), false);
+        } else if ($locks[$lock]['uses'] > 0) {
+            $notifysuccess = false;
+            $notifications[] = array(get_string('deletelockhasuses', 'cache'), false);
+        }
+        if ($notifysuccess) {
+            if (!$confirm) {
+                $title = get_string('confirmlockdeletion', 'cache');
+                $params = array('lock' => $lock, 'confirm' => 1, 'action' => $action, 'sesskey' => sesskey());
+                $url = new \moodle_url($PAGE->url, $params);
+                $button = new \single_button($url, get_string('deletelock', 'cache'));
+
+                $PAGE->set_title($title);
+                $PAGE->set_heading($SITE->fullname);
+                echo $OUTPUT->header();
+                echo $OUTPUT->heading($title);
+                $confirmation = get_string('deletelockconfirmation', 'cache', $lock);
+                echo $OUTPUT->confirm($confirmation, $button, $PAGE->url);
+                echo $OUTPUT->footer();
+                exit;
+            } else {
+                $writer = cache_config_writer::instance();
+                $writer->delete_lock_instance($lock);
+                redirect($PAGE->url, get_string('deletelocksuccess', 'cache'), 5);
+            }
+        }
+    }
+
+    /**
+     * Outputs the main admin page by generating it through the renderer.
+     *
+     * @param core_cache_renderer $renderer the renderer to use to generate the page.
+     * @return string the HTML for the admin page.
+     */
+    public function generate_admin_page(core_cache_renderer $renderer): string {
+        $context = \context_system::instance();
+        $html = '';
+
+        $storepluginsummaries = $this->get_store_plugin_summaries();
+        $storeinstancesummaries = $this->get_store_instance_summaries();
+        $definitionsummaries = $this->get_definition_summaries();
+        $defaultmodestores = $this->get_default_mode_stores();
+        $locks = $this->get_lock_summaries();
+
+        $html .= $renderer->store_plugin_summaries($storepluginsummaries);
+        $html .= $renderer->store_instance_summariers($storeinstancesummaries, $storepluginsummaries);
+        $html .= $renderer->definition_summaries($definitionsummaries, $context);
+        $html .= $renderer->lock_summaries($locks);
+        $html .= $renderer->additional_lock_actions();
+
+        $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()));
+        $html .= $renderer->mode_mappings($applicationstore, $sessionstore, $requeststore, $editurl);
+
+        return $html;
+    }
+}
\ No newline at end of file
index e482702..1643570 100644 (file)
@@ -97,7 +97,7 @@ class cachestore_addinstance_form extends moodleform {
             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();
+                $stores = core_cache\administration_helper::get_store_instance_summaries();
                 if (array_key_exists($data['name'], $stores)) {
                     $errors['name'] = get_string('storenamealreadyused', 'cache');
                 }
@@ -139,9 +139,9 @@ class cache_definition_mappings_form extends moodleform {
 
         list($component, $area) = explode('/', $definition, 2);
         list($currentstores, $storeoptions, $defaults) =
-                cache_administration_helper::get_definition_store_options($component, $area);
+                core_cache\administration_helper::get_definition_store_options($component, $area);
 
-        $storedata = cache_administration_helper::get_definition_summaries();
+        $storedata = core_cache\administration_helper::get_definition_summaries();
         if ($storedata[$definition]['mode'] != cache_store::MODE_REQUEST) {
             if (isset($storedata[$definition]['canuselocalstore']) && $storedata[$definition]['canuselocalstore']) {
                 $form->addElement('html', $OUTPUT->notification(get_string('localstorenotification', 'cache'), 'notifymessage'));
@@ -247,7 +247,7 @@ class cache_definition_sharing_form extends moodleform {
     public function set_data($data) {
         if (!isset($data['sharing'])) {
             // Set the default value here. mforms doesn't handle defaults very nicely.
-            $data['sharing'] = cache_administration_helper::get_definition_sharing_options(cache_definition::SHARING_DEFAULT);
+            $data['sharing'] = core_cache\administration_helper::get_definition_sharing_options(cache_definition::SHARING_DEFAULT);
         }
         parent::set_data($data);
     }
index 62ead74..b8509ce 100644 (file)
@@ -659,597 +659,4 @@ class cache_config_writer extends cache_config {
         }
         $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();
-        $locks = $instance->get_locks();
-        foreach ($stores as $name => $details) {
-            $class = $details['class'];
-            $store = false;
-            if ($class::are_requirements_met()) {
-                $store = new $class($details['name'], $details['configuration']);
-            }
-            $lock = (isset($details['lock'])) ? $locks[$details['lock']] : $instance->get_default_lock();
-            $record = array(
-                'name' => $name,
-                'plugin' => $details['plugin'],
-                'default' => $details['default'],
-                'isready' => $store ? $store->is_ready() : false,
-                'requirementsmet' => $class::are_requirements_met(),
-                'mappings' => 0,
-                'lock' => $lock,
-                'modes' => array(
-                    cache_store::MODE_APPLICATION =>
-                        ($class::get_supported_modes($return) & cache_store::MODE_APPLICATION) == cache_store::MODE_APPLICATION,
-                    cache_store::MODE_SESSION =>
-                        ($class::get_supported_modes($return) & cache_store::MODE_SESSION) == cache_store::MODE_SESSION,
-                    cache_store::MODE_REQUEST =>
-                        ($class::get_supported_modes($return) & cache_store::MODE_REQUEST) == cache_store::MODE_REQUEST,
-                ),
-                'supports' => array(
-                    'multipleidentifiers' => $store ? $store->supports_multiple_identifiers() : false,
-                    'dataguarantee' => $store ? $store->supports_data_guarantee() : false,
-                    'nativettl' => $store ? $store->supports_native_ttl() : false,
-                    'nativelocking' => ($store instanceof cache_is_lockable),
-                    'keyawareness' => ($store instanceof cache_is_key_aware),
-                    'searchable' => ($store instanceof cache_is_searchable)
-                ),
-                'warnings' => $store ? $store->get_warnings() : array()
-            );
-            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 for each store, an array containing various information about each store.
-     *     See the code below for details
-     */
-    public static function get_store_plugin_summaries() {
-        $return = array();
-        $plugins = core_component::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() && $class::are_requirements_met())
-            );
-        }
-
-        $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 for each store, an array containing various information about each store.
-     *     See the code below for details
-     */
-    public static function get_definition_summaries() {
-        $factory = cache_factory::instance();
-        $config = $factory->create_config_instance();
-        $storenames = array();
-        foreach ($config->get_all_stores() as $key => $store) {
-            if (!empty($store['default'])) {
-                $storenames[$key] = new lang_string('store_'.$key, 'cache');
-            } else {
-                $storenames[$store['name']] = $store['name'];
-            }
-        }
-        /* @var cache_definition[] $definitions */
-        $definitions = array();
-        foreach ($config->get_definitions() as $key => $definition) {
-            $definitions[$key] = cache_definition::load($definition['component'].'/'.$definition['area'], $definition);
-        }
-        foreach ($definitions as $id => $definition) {
-            $mappings = array();
-            foreach (cache_helper::get_stores_suitable_for_definition($definition) as $store) {
-                $mappings[] = $storenames[$store->my_name()];
-            }
-            $return[$id] = array(
-                'id' => $id,
-                'name' => $definition->get_name(),
-                'mode' => $definition->get_mode(),
-                'component' => $definition->get_component(),
-                'area' => $definition->get_area(),
-                'mappings' => $mappings,
-                'canuselocalstore' => $definition->can_use_localstore(),
-                'sharingoptions' => self::get_definition_sharing_options($definition->get_sharing_options(), false),
-                'selectedsharingoption' => self::get_definition_sharing_options($definition->get_selected_sharing_option(), true),
-                'userinputsharingkey' => $definition->get_user_input_sharing_key()
-            );
-        }
-        return $return;
-    }
-
-    /**
-     * Given a sharing option hash this function returns an array of strings that can be used to describe it.
-     *
-     * @param int $sharingoption The sharing option hash to get strings for.
-     * @param bool $isselectedoptions Set to true if the strings will be used to view the selected options.
-     * @return array An array of lang_string's.
-     */
-    public static function get_definition_sharing_options($sharingoption, $isselectedoptions = true) {
-        $options = array();
-        $prefix = ($isselectedoptions) ? 'sharingselected' : 'sharing';
-        if ($sharingoption & cache_definition::SHARING_ALL) {
-            $options[cache_definition::SHARING_ALL] = new lang_string($prefix.'_all', 'cache');
-        }
-        if ($sharingoption & cache_definition::SHARING_SITEID) {
-            $options[cache_definition::SHARING_SITEID] = new lang_string($prefix.'_siteid', 'cache');
-        }
-        if ($sharingoption & cache_definition::SHARING_VERSION) {
-            $options[cache_definition::SHARING_VERSION] = new lang_string($prefix.'_version', 'cache');
-        }
-        if ($sharingoption & cache_definition::SHARING_INPUT) {
-            $options[cache_definition::SHARING_INPUT] = new lang_string($prefix.'_input', 'cache');
-        }
-        return $options;
-    }
-
-    /**
-     * Returns all of the actions that can be performed on a definition.
-     *
-     * @param context $context the system context.
-     * @param array $definitionsummary information about this cache, from the array returned by
-     *      cache_administration_helper::get_definition_summaries(). Currently only 'sharingoptions'
-     *      element is used.
-     * @return array of actions. Each action is an array with two elements, 'text' and 'url'.
-     */
-    public static function get_definition_actions(context $context, array $definitionsummary) {
-        if (has_capability('moodle/site:config', $context)) {
-            $actions = array();
-            // Edit mappings.
-            $actions[] = array(
-                'text' => get_string('editmappings', 'cache'),
-                'url' => new moodle_url('/cache/admin.php', array('action' => 'editdefinitionmapping', 'sesskey' => sesskey()))
-            );
-            // Edit sharing.
-            if (count($definitionsummary['sharingoptions']) > 1) {
-                $actions[] = array(
-                    'text' => get_string('editsharing', 'cache'),
-                    'url' => new moodle_url('/cache/admin.php', array('action' => 'editdefinitionsharing', 'sesskey' => sesskey()))
-                );
-            }
-            // Purge.
-            $actions[] = array(
-                'text' => get_string('purge', 'cache'),
-                'url' => new moodle_url('/cache/admin.php', array('action' => 'purgedefinition', 'sesskey' => sesskey()))
-            );
-            return $actions;
-        }
-        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 information about this store, from the array returned by
-     *      cache_administration_helper::get_store_instance_summaries().
-     * @return array of actions. Each action is an array with two elements, 'text' and 'url'.
-     */
-    public static function get_store_instance_actions($name, array $storedetails) {
-        $actions = array();
-        if (has_capability('moodle/site:config', context_system::instance())) {
-            $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' => 'purgestore'))
-            );
-        }
-        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 information about this store, from the array returned by
-     *      cache_administration_helper::get_store_plugin_summaries().
-     * @param array $plugindetails
-     * @return array
-     */
-    public static function get_store_plugin_actions($name, array $plugindetails) {
-        $actions = array();
-        if (has_capability('moodle/site:config', context_system::instance())) {
-            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 = core_component::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 = core_component::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', 'plugin' => $plugin, 'store' => $store));
-        $editform = new $class($url, array('plugin' => $plugin, 'store' => $store, 'locks' => $locks));
-        if (isset($stores[$store]['lock'])) {
-            $editform->set_data(array('lock' => $stores[$store]['lock']));
-        }
-        // See if the cachestore is going to want to load data for the form.
-        // If it has a customised add instance form then it is going to want to.
-        $storeclass = 'cachestore_'.$plugin;
-        $storedata = $stores[$store];
-        if (array_key_exists('configuration', $storedata) && array_key_exists('cache_is_configurable', class_implements($storeclass))) {
-            $storeclass::config_set_edit_form_data($editform, $storedata['configuration']);
-        }
-        return $editform;
-    }
-
-    /**
-     * 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;
-        if (!class_exists($class)) {
-            throw new coding_exception('Invalid cache plugin provided.');
-        }
-        if (array_key_exists('cache_is_configurable', class_implements($class))) {
-            return $class::config_get_configuration_array($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() {
-        global $OUTPUT;
-        $instance = cache_config::instance();
-        $adequatestores = cache_helper::get_stores_suitable_for_mode_default();
-        $icon = new pix_icon('i/warning', new lang_string('inadequatestoreformapping', 'cache'));
-        $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'];
-            }
-            if (!array_key_exists($mapping['store'], $adequatestores)) {
-                $modemappings[$mode][$mapping['store']] = $modemappings[$mode][$mapping['store']].' '.$OUTPUT->render($icon);
-            }
-        }
-        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,
-                'type' => get_string('pluginname', $lock['type'])
-            );
-            $locks[$lock['name']] = $lockdata;
-        }
-        return $locks;
-    }
-
-    /**
-     * Returns an array of lock plugins for which we can add an instance.
-     *
-     * Suitable for use within an mform select element.
-     *
-     * @return array
-     */
-    public static function get_addable_lock_options() {
-        $plugins = core_component::get_plugin_list_with_class('cachelock', '', 'lib.php');
-        $options = array();
-        $len = strlen('cachelock_');
-        foreach ($plugins as $plugin => $class) {
-            $method = "$class::can_add_instance";
-            if (is_callable($method) && !call_user_func($method)) {
-                // Can't add an instance of this plugin.
-                continue;
-            }
-            $options[substr($plugin, $len)] = get_string('pluginname', $plugin);
-        }
-        return $options;
-    }
-
-    /**
-     * Gets the form to use when adding a lock instance.
-     *
-     * @param string $plugin
-     * @param array $lockplugin
-     * @return cache_lock_form
-     * @throws coding_exception
-     */
-    public static function get_add_lock_form($plugin, array $lockplugin = null) {
-        global $CFG; // Needed for includes.
-        $plugins = core_component::get_plugin_list('cachelock');
-        if (!array_key_exists($plugin, $plugins)) {
-            throw new coding_exception('Invalid cache lock plugin requested when trying to create a form.');
-        }
-        $plugindir = $plugins[$plugin];
-        $class = 'cache_lock_form';
-        if (file_exists($plugindir.'/addinstanceform.php') && in_array('cache_is_configurable', class_implements($class))) {
-            require_once($plugindir.'/addinstanceform.php');
-            if (class_exists('cachelock_'.$plugin.'_addinstance_form')) {
-                $class = 'cachelock_'.$plugin.'_addinstance_form';
-                if (!array_key_exists('cache_lock_form', class_parents($class))) {
-                    throw new coding_exception('Cache lock plugin add instance forms must extend cache_lock_form');
-                }
-            }
-        }
-        return new $class(null, array('lock' => $plugin));
-    }
-
-    /**
-     * Gets configuration data from a new lock instance form.
-     *
-     * @param string $plugin
-     * @param stdClass $data
-     * @return array
-     * @throws coding_exception
-     */
-    public static function get_lock_configuration_from_data($plugin, $data) {
-        global $CFG;
-        $file = $CFG->dirroot.'/cache/locks/'.$plugin.'/lib.php';
-        if (!file_exists($file)) {
-            throw new coding_exception('Invalid cache plugin provided. '.$file);
-        }
-        require_once($file);
-        $class = 'cachelock_'.$plugin;
-        if (!class_exists($class)) {
-            throw new coding_exception('Invalid cache plugin provided.');
-        }
-        if (array_key_exists('cache_is_configurable', class_implements($class))) {
-            return $class::config_get_configuration_array($data);
-        }
-        return array();
-    }
 }
index 38ef769..93b3655 100644 (file)
@@ -41,9 +41,9 @@ class core_cache_renderer extends plugin_renderer_base {
      * Displays store summaries.
      *
      * @param array $storeinstancesummaries information about each store instance,
-     *      as returned by cache_administration_helper::get_store_instance_summaries().
+     *      as returned by core_cache\administration_helper::get_store_instance_summaries().
      * @param array $storepluginsummaries information about each store plugin as
-     *      returned by cache_administration_helper::get_store_plugin_summaries().
+     *      returned by core_cache\administration_helper::get_store_plugin_summaries().
      * @return string HTML
      */
     public function store_instance_summariers(array $storeinstancesummaries, array $storepluginsummaries) {
@@ -73,7 +73,7 @@ class core_cache_renderer extends plugin_renderer_base {
         $defaultstoreactions = get_string('defaultstoreactions', 'cache');
 
         foreach ($storeinstancesummaries as $name => $storesummary) {
-            $actions = cache_administration_helper::get_store_instance_actions($name, $storesummary);
+            $htmlactions = cache_factory::get_administration_display_helper()->get_store_instance_actions($name, $storesummary);
             $modes = array();
             foreach ($storesummary['modes'] as $mode => $enabled) {
                 if ($enabled) {
@@ -92,10 +92,6 @@ class core_cache_renderer extends plugin_renderer_base {
             if (!empty($storesummary['default'])) {
                 $info = $this->output->pix_icon('i/info', $defaultstoreactions, '', array('class' => 'icon'));
             }
-            $htmlactions = array();
-            foreach ($actions as $action) {
-                $htmlactions[] = $this->output->action_link($action['url'], $action['text']);
-            }
 
             $isready = $storesummary['isready'] && $storesummary['requirementsmet'];
             $readycell = new html_table_cell;
@@ -145,7 +141,7 @@ class core_cache_renderer extends plugin_renderer_base {
      * Displays plugin summaries.
      *
      * @param array $storepluginsummaries information about each store plugin as
-     *      returned by cache_administration_helper::get_store_plugin_summaries().
+     *      returned by core_cache\administration_helper::get_store_plugin_summaries().
      * @return string HTML
      */
     public function store_plugin_summaries(array $storepluginsummaries) {
@@ -169,7 +165,7 @@ class core_cache_renderer extends plugin_renderer_base {
         $table->data = array();
 
         foreach ($storepluginsummaries as $name => $plugin) {
-            $actions = cache_administration_helper::get_store_plugin_actions($name, $plugin);
+            $htmlactions = cache_factory::get_administration_display_helper()->get_store_plugin_actions($name, $plugin);
 
             $modes = array();
             foreach ($plugin['modes'] as $mode => $enabled) {
@@ -185,11 +181,6 @@ class core_cache_renderer extends plugin_renderer_base {
                 }
             }
 
-            $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/valid', '1') : '',
@@ -214,7 +205,7 @@ class core_cache_renderer extends plugin_renderer_base {
      * Displays definition summaries.
      *
      * @param array $definitionsummaries information about each definition, as returned by
-     *      cache_administration_helper::get_definition_summaries().
+     *      core_cache\administration_helper::get_definition_summaries().
      * @param context $context the system context.
      *
      * @return string HTML.
@@ -247,12 +238,7 @@ class core_cache_renderer extends plugin_renderer_base {
 
         $none = new lang_string('none', 'cache');
         foreach ($definitionsummaries as $id => $definition) {
-            $actions = cache_administration_helper::get_definition_actions($context, $definition);
-            $htmlactions = array();
-            foreach ($actions as $action) {
-                $action['url']->param('definition', $id);
-                $htmlactions[] = $this->output->action_link($action['url'], $action['text']);
-            }
+            $htmlactions = cache_factory::get_administration_display_helper()->get_definition_actions($context, $definition);
             if (!empty($definition['mappings'])) {
                 $mapping = join(', ', $definition['mappings']);
             } else {
@@ -379,13 +365,24 @@ class core_cache_renderer extends plugin_renderer_base {
             ));
         }
 
-        $url = new moodle_url('/cache/admin.php', array('action' => 'newlockinstance', 'sesskey' => sesskey()));
-        $select = new single_select($url, 'lock', cache_administration_helper::get_addable_lock_options());
-        $select->label = get_string('addnewlockinstance', 'cache');
-
         $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;
+    }
+
+    /**
+     * Renders additional actions for locks, such as Add.
+     *
+     * @return string
+     */
+    public function additional_lock_actions() : string {
+        $url = new moodle_url('/cache/admin.php', array('action' => 'newlockinstance', 'sesskey' => sesskey()));
+        $select = new single_select($url, 'lock', cache_factory::get_administration_display_helper()->get_addable_lock_options());
+        $select->label = get_string('addnewlockinstance', 'cache');
+
+        $html = html_writer::start_tag('div', array('id' => 'core-cache-lock-additional-actions'));
         $html .= html_writer::tag('div', $this->output->render($select), array('class' => 'new-instance'));
         $html .= html_writer::end_tag('div');
         return $html;
index 95c70de..865539f 100644 (file)
@@ -35,7 +35,7 @@ require_once($CFG->dirroot.'/cache/tests/fixtures/lib.php');
 
 
 /**
- * PHPunit tests for the cache API and in particular the cache_administration_helper
+ * PHPunit tests for the cache API and in particular the core_cache\administration_helper
  *
  * @copyright  2012 Sam Hemelryk
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -73,7 +73,7 @@ class core_cache_administration_helper_testcase extends advanced_testcase {
             cache_store::MODE_REQUEST => array('default_request'),
         )));
 
-        $storesummaries = cache_administration_helper::get_store_instance_summaries();
+        $storesummaries = core_cache\administration_helper::get_store_instance_summaries();
         $this->assertInternalType('array', $storesummaries);
         $this->assertArrayHasKey('summariesstore', $storesummaries);
         $summary = $storesummaries['summariesstore'];
@@ -94,7 +94,7 @@ class core_cache_administration_helper_testcase extends advanced_testcase {
         $this->assertEquals(1, $summary['requirementsmet']);
         $this->assertEquals(1, $summary['mappings']);
 
-        $definitionsummaries = cache_administration_helper::get_definition_summaries();
+        $definitionsummaries = core_cache\administration_helper::get_definition_summaries();
         $this->assertInternalType('array', $definitionsummaries);
         $this->assertArrayHasKey('core/eventinvalidation', $definitionsummaries);
         $summary = $definitionsummaries['core/eventinvalidation'];
@@ -114,7 +114,7 @@ class core_cache_administration_helper_testcase extends advanced_testcase {
         $this->assertInternalType('array', $summary['mappings']);
         $this->assertContains('summariesstore', $summary['mappings']);
 
-        $pluginsummaries = cache_administration_helper::get_store_plugin_summaries();
+        $pluginsummaries = core_cache\administration_helper::get_store_plugin_summaries();
         $this->assertInternalType('array', $pluginsummaries);
         $this->assertArrayHasKey('file', $pluginsummaries);
         $summary = $pluginsummaries['file'];
@@ -126,18 +126,18 @@ class core_cache_administration_helper_testcase extends advanced_testcase {
         $this->assertArrayHasKey('supports', $summary);
         $this->assertArrayHasKey('canaddinstance', $summary);
 
-        $locksummaries = cache_administration_helper::get_lock_summaries();
+        $locksummaries = core_cache\administration_helper::get_lock_summaries();
         $this->assertInternalType('array', $locksummaries);
         $this->assertTrue(count($locksummaries) > 0);
 
-        $mappings = cache_administration_helper::get_default_mode_stores();
+        $mappings = core_cache\administration_helper::get_default_mode_stores();
         $this->assertInternalType('array', $mappings);
         $this->assertCount(3, $mappings);
         $this->assertArrayHasKey(cache_store::MODE_APPLICATION, $mappings);
         $this->assertInternalType('array', $mappings[cache_store::MODE_APPLICATION]);
         $this->assertContains('summariesstore', $mappings[cache_store::MODE_APPLICATION]);
 
-        $potentials = cache_administration_helper::get_definition_store_options('core', 'eventinvalidation');
+        $potentials = core_cache\administration_helper::get_definition_store_options('core', 'eventinvalidation');
         $this->assertInternalType('array', $potentials); // Currently used, suitable, default
         $this->assertCount(3, $potentials);
         $this->assertArrayHasKey('summariesstore', $potentials[0]);
@@ -149,11 +149,11 @@ class core_cache_administration_helper_testcase extends advanced_testcase {
      * Test instantiating an add store form.
      */
     public function test_get_add_store_form() {
-        $form = cache_administration_helper::get_add_store_form('file');
+        $form = cache_factory::get_administration_display_helper()->get_add_store_form('file');
         $this->assertInstanceOf('moodleform', $form);
 
         try {
-            $form = cache_administration_helper::get_add_store_form('somethingstupid');
+            $form = cache_factory::get_administration_display_helper()->get_add_store_form('somethingstupid');
             $this->fail('You should not be able to create an add form for a store plugin that does not exist.');
         } catch (moodle_exception $e) {
             $this->assertInstanceOf('coding_exception', $e, 'Needs to be: ' .get_class($e)." ::: ".$e->getMessage());
@@ -164,21 +164,23 @@ class core_cache_administration_helper_testcase extends advanced_testcase {
      * Test instantiating a form to edit a store instance.
      */
     public function test_get_edit_store_form() {
+        // Always instantiate a new core display helper here.
+        $administrationhelper = new core_cache\local\administration_display_helper;
         $config = cache_config_writer::instance();
         $this->assertTrue($config->add_store_instance('test_get_edit_store_form', 'file'));
 
-        $form = cache_administration_helper::get_edit_store_form('file', 'test_get_edit_store_form');
+        $form = $administrationhelper->get_edit_store_form('file', 'test_get_edit_store_form');
         $this->assertInstanceOf('moodleform', $form);
 
         try {
-            $form = cache_administration_helper::get_edit_store_form('somethingstupid', 'moron');
+            $form = $administrationhelper->get_edit_store_form('somethingstupid', 'moron');
             $this->fail('You should not be able to create an edit form for a store plugin that does not exist.');
         } catch (moodle_exception $e) {
             $this->assertInstanceOf('coding_exception', $e);
         }
 
         try {
-            $form = cache_administration_helper::get_edit_store_form('file', 'blisters');
+            $form = $administrationhelper->get_edit_store_form('file', 'blisters');
             $this->fail('You should not be able to create an edit form for a store plugin that does not exist.');
         } catch (moodle_exception $e) {
             $this->assertInstanceOf('coding_exception', $e);
index d92a97e..1890d30 100644 (file)
@@ -6,6 +6,8 @@ Information provided here is intended especially for developers.
 * The function extend_lock() from the lock_factory interface has been deprecated without replacement including the related
   implementations.
 * The function extend() from the lock class has been deprecated without replacement.
+* The cache_factory class can now be overridden by an alternative cache config class, which can
+  also now control the frontend display of the cache/admin.php page (see MDL-41492).
 
 === 3.9 ===
 * The record_cache_hit/miss/set methods now take a cache_store instead of a cache_definition object
diff --git a/calendar/classes/external/export/token.php b/calendar/classes/external/export/token.php
new file mode 100644 (file)
index 0000000..9c530a0
--- /dev/null
@@ -0,0 +1,99 @@
+<?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 is the external method for exporting a calendar token.
+ *
+ * @package    core_calendar
+ * @since      Moodle 3.10
+ * @copyright  2020 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_calendar\external\export;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/externallib.php');
+require_once($CFG->dirroot . '/calendar/lib.php');
+
+use context_system;
+use external_api;
+use external_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+use external_warnings;
+use moodle_exception;
+
+/**
+ * This is the external method for exporting a calendar token.
+ *
+ * @copyright  2020 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class token extends external_api {
+
+    /**
+     * Returns description of method parameters.
+     *
+     * @return external_function_parameters.
+     * @since  Moodle 3.10
+     */
+    public static function execute_parameters() {
+        return new external_function_parameters([]);
+    }
+
+    /**
+     * Return the auth token required for exporting a calendar.
+     *
+     * @return array The access information
+     * @throws moodle_exception
+     * @since  Moodle 3.10
+     */
+    public static function execute() {
+        global $CFG, $USER;
+
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        if (empty($CFG->enablecalendarexport)) {
+            throw new moodle_exception('Calendar export is disabled in this site.');
+        }
+
+        return [
+            'token' => calendar_get_export_token($USER),
+            'warnings' => [],
+        ];
+    }
+
+    /**
+     * Returns description of method result value.
+     *
+     * @return external_description.
+     * @since  Moodle 3.10
+     */
+    public static function execute_returns() {
+
+        return new external_single_structure(
+            [
+                'token' => new external_value(PARAM_RAW, 'The calendar permanent access token for calendar export.'),
+                'warnings' => new external_warnings(),
+            ]
+        );
+    }
+}
index 89ca1e0..687f9cf 100644 (file)
@@ -141,10 +141,9 @@ $formdata = array(
 $exportform = new core_calendar_export_form(null, $formdata);
 $calendarurl = '';
 if ($data = $exportform->get_data()) {
-    $password = $DB->get_record('user', array('id' => $USER->id), 'password');
     $params = array();
     $params['userid']      = $USER->id;
-    $params['authtoken']   = sha1($USER->id . (isset($password->password) ? $password->password : '') . $CFG->calendar_exportsalt);
+    $params['authtoken']   = calendar_get_export_token($USER);
     $params['preset_what'] = $data->events['exportevents'];
     $params['preset_time'] = $data->period['timeperiod'];
 
index a06f328..66c348e 100644 (file)
@@ -24,7 +24,7 @@ if (!$checkuserid && !$checkusername) {
 }
 
 //Check authentication token
-$authuserid = !empty($userid) && $authtoken == sha1($userid . $user->password . $CFG->calendar_exportsalt);
+$authuserid = !empty($userid) && $authtoken == calendar_get_export_token($user);
 //allowing for fallback check of old url - MDL-27542
 $authusername = !empty($username) && $authtoken == sha1($username . $user->password . $CFG->calendar_exportsalt);
 if (!$authuserid && !$authusername) {
@@ -44,7 +44,7 @@ $allowedwhat = ['all', 'user', 'groups', 'courses', 'categories'];
 $allowedtime = ['weeknow', 'weeknext', 'monthnow', 'monthnext', 'recentupcoming', 'custom'];
 
 if (!empty($generateurl)) {
-    $authtoken = sha1($user->id . $user->password . $CFG->calendar_exportsalt);
+    $authtoken = calendar_get_export_token($user);
     $params = array();
     $params['preset_what'] = $what;
     $params['preset_time'] = $time;
index 9755765..59fc796 100644 (file)
@@ -3667,11 +3667,10 @@ function calendar_get_timestamp($d, $m, $y, $time = 0) {
  * @return array The data for template and template name.
  */
 function calendar_get_footer_options($calendar) {
-    global $CFG, $USER, $DB, $PAGE;
+    global $CFG, $USER, $PAGE;
 
     // Generate hash for iCal link.
-    $rawhash = $USER->id . $DB->get_field('user', 'password', ['id' => $USER->id]) . $CFG->calendar_exportsalt;
-    $authtoken = sha1($rawhash);
+    $authtoken = calendar_get_export_token($USER);
 
     $renderer = $PAGE->get_renderer('core_calendar');
     $footer = new \core_calendar\external\footer_options_exporter($calendar, $USER->id, $authtoken);
@@ -3905,3 +3904,15 @@ function calendar_internal_update_course_and_group_permission(int $courseid, con
         }
     }
 }
+
+/**
+ * Get the auth token for exporting the given user calendar.
+ * @param stdClass $user The user to export the calendar for
+ *
+ * @return string The export token.
+ */
+function calendar_get_export_token(stdClass $user): string {
+    global $CFG, $DB;
+
+    return sha1($user->id . $DB->get_field('user', 'password', ['id' => $user->id]) . $CFG->calendar_exportsalt);
+}
index 94a3cb4..4467337 100644 (file)
@@ -963,4 +963,36 @@ class core_calendar_lib_testcase extends advanced_testcase {
         // Viewing as someone not enrolled in a course with guest access on.
         $this->assertTrue(calendar_view_event_allowed($caleventguest));
     }
+
+    /**
+     *  Test for calendar_get_export_token for current user.
+     */
+    public function test_calendar_get_export_token_for_current_user() {
+        global $USER, $DB, $CFG;
+
+        $this->setAdminUser();
+
+        // Get my token.
+        $authtoken = calendar_get_export_token($USER);
+        $expected = sha1($USER->id . $DB->get_field('user', 'password', ['id' => $USER->id]) . $CFG->calendar_exportsalt);
+
+        $this->assertEquals($expected, $authtoken);
+    }
+
+    /**
+     *  Test for calendar_get_export_token for another user.
+     */
+    public function test_calendar_get_export_token_for_another_user() {
+        global $CFG;
+
+        // Get any user token.
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+
+        // Get other user token.
+        $authtoken = calendar_get_export_token($user);
+        $expected = sha1($user->id . $user->password . $CFG->calendar_exportsalt);
+
+        $this->assertEquals($expected, $authtoken);
+    }
 }
index 04b05c4..4ddc81a 100644 (file)
@@ -173,6 +173,36 @@ class completion_completion extends data_object {
             \core\event\course_completed::create_from_completion($data)->trigger();
         }
 
+        // Notify user.
+        $course = get_course($data->course);
+        $messagesubject = get_string('coursecompleted', 'completion');
+        $a = [
+            'coursename' => get_course_display_name_for_list($course),
+            'courselink' => (string) new moodle_url('/course/view.php', array('id' => $course->id)),
+        ];
+        $messagebody = get_string('coursecompletedmessage', 'completion', $a);
+        $messageplaintext = html_to_text($messagebody);
+
+        $eventdata = new \core\message\message();
+        $eventdata->courseid          = $course->id;
+        $eventdata->component         = 'moodle';
+        $eventdata->name              = 'coursecompleted';
+        $eventdata->userfrom          = core_user::get_noreply_user();
+        $eventdata->userto            = $data->userid;
+        $eventdata->notification      = 1;
+        $eventdata->subject           = $messagesubject;
+        $eventdata->fullmessage       = $messageplaintext;
+        $eventdata->fullmessageformat = FORMAT_HTML;
+        $eventdata->fullmessagehtml   = $messagebody;
+        $eventdata->smallmessage      = $messageplaintext;
+
+        if ($courseimage = \core_course\external\course_summary_exporter::get_course_image($course)) {
+            $eventdata->customdata  = [
+                'notificationpictureurl' => $courseimage,
+            ];
+        }
+        message_send($eventdata);
+
         return $result;
     }
 
index ecd56b6..97fae22 100644 (file)
@@ -1057,6 +1057,18 @@ $CFG->admin = 'admin';
 //      $CFG->showcampaigncontent = true;
 //
 //=========================================================================
+// 16. ALTERNATIVE CACHE CONFIG SETTINGS
+//=========================================================================
+//
+// Alternative cache config.
+// Since 3.10 it is possible to override the cache_factory class with an alternative caching factory.
+// This overridden factory can provide alternative classes for caching such as cache_config,
+// cache_config_writer and core_cache\local\administration_display_helper.
+// The autoloaded factory class name can be specified to use.
+//
+//      $CFG->alternative_cache_factory_class = 'tool_alternativecache_cache_factory';
+//
+//=========================================================================
 // ALL DONE!  To continue installation, visit your main page with a browser
 //=========================================================================
 
index 5a896e7..5ac27f8 100644 (file)
@@ -261,6 +261,7 @@ class core_course_external extends external_api {
                         $module['id'] = $cm->id;
                         $module['name'] = external_format_string($cm->name, $modcontext->id);
                         $module['instance'] = $cm->instance;
+                        $module['contextid'] = $modcontext->id;
                         $module['modname'] = (string) $cm->modname;
                         $module['modplural'] = (string) $cm->modplural;
                         $module['modicon'] = $cm->get_icon_url()->out(false);
@@ -442,6 +443,7 @@ class core_course_external extends external_api {
                                     'url' => new external_value(PARAM_URL, 'activity url', VALUE_OPTIONAL),
                                     'name' => new external_value(PARAM_RAW, 'activity module name'),
                                     'instance' => new external_value(PARAM_INT, 'instance id', VALUE_OPTIONAL),
+                                    'contextid' => new external_value(PARAM_INT, 'Activity context id.', VALUE_OPTIONAL),
                                     'description' => new external_value(PARAM_RAW, 'activity description', VALUE_OPTIONAL),
                                     'visible' => new external_value(PARAM_INT, 'is the module visible', VALUE_OPTIONAL),
                                     'uservisible' => new external_value(PARAM_BOOL, 'Is the module visible for the user?',
index 57e6108..fcf938b 100644 (file)
@@ -1015,6 +1015,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
                     array('noclean' => true, 'para' => false, 'filter' => false));
                 $this->assertEquals($formattedtext, $module['description']);
                 $this->assertEquals($forumcm->instance, $module['instance']);
+                $this->assertEquals(context_module::instance($forumcm->id)->id, $module['contextid']);
                 $this->assertContains('1 unread post', $module['afterlink']);
                 $this->assertFalse($module['noviewlink']);
                 $this->assertNotEmpty($module['description']);  // Module showdescription is on.
@@ -1025,6 +1026,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
                     array('noclean' => true, 'para' => false, 'filter' => false));
                 $this->assertEquals($formattedtext, $module['description']);
                 $this->assertEquals($labelcm->instance, $module['instance']);
+                $this->assertEquals(context_module::instance($labelcm->id)->id, $module['contextid']);
                 $this->assertTrue($module['noviewlink']);
                 $this->assertNotEmpty($module['description']);  // Label always prints the description.
                 $testexecuted = $testexecuted + 1;
index 4af9f85..e56837d 100644 (file)
@@ -4,6 +4,7 @@ information provided here is intended especially for developers.
 === 3.10 ===
 
 * The function make_categories_options() has now been deprecated. Please use \core_course_category::make_categories_list() instead.
+* External function core_course_external::get_course_contents now returns a new field contextid with the module context id.
 
 === 3.9 ===
 
index ceb7522..22a7ea1 100644 (file)
@@ -35,7 +35,7 @@ $string['cannotcreatedboninstall'] = '<p> لا يمكن إنشاء قاعدة ا
 <p> المسؤول عن الموقع يجب أن يتحقق من إعدادات قاعدة بيانات. </p>';
 $string['cannotcreatelangdir'] = 'لا يمكن إنشاء مجلد اللغة';
 $string['cannotcreatetempdir'] = 'لا يمكن إنشاء المجلد المؤقت';
-$string['cannotdownloadcomponents'] = 'لم يتم تحميل العناصر';
+$string['cannotdownloadcomponents'] = 'تعذر تنزيل المُكونات';
 $string['cannotdownloadzipfile'] = 'لم يتم تحميل الملف المضغوط';
 $string['cannotfindcomponent'] = 'لم يتم العثور على المكون';
 $string['cannotsavemd5file'] = 'لم يتم حفظ ملف  md5';
index 7c0ccda..8207932 100644 (file)
@@ -34,4 +34,4 @@ $string['language'] = 'اللغة';
 $string['moodlelogo'] = 'شعار مودل';
 $string['next'] = 'التالي';
 $string['previous'] = 'السابق';
-$string['reload'] = 'إعادة تحميل';
+$string['reload'] = 'إعادة Ø§Ù\84تحÙ\85Ù\8aÙ\84';
index 6cbb336..455a2cd 100644 (file)
@@ -499,6 +499,15 @@ $string['disabled'] = 'Disabled';
 $string['disableuserimages'] = 'Disable user profile images';
 $string['displayerrorswarning'] = 'Enabling the PHP setting <em>display_errors</em> is not recommended on production sites because some error messages may reveal sensitive information about your server.';
 $string['displayloginfailures'] = 'Display login failures';
+$string['divertallemails'] = 'Email diverting';
+$string['divertallemailsdetail'] = 'Used as a safeguard in development environments when testing emails and should not be used in production.';
+$string['divertallemailsexcept'] = 'Email diversion exceptions';
+$string['divertallemailsexcept_desc'] = 'A list of email exception rules separated by either commas or new lines. Each rule is interpreted as a regular expression, eg<pre>simone@acme.com
+.*@acme.com
+fred(\\+.*)?@acme.com
+</pre>';
+$string['divertallemailsto'] = 'Divert all emails';
+$string['divertallemailsto_desc'] = 'If set then all emails will be diverted to this single email address instead.';
 $string['dndallowtextandlinks'] = 'Drag and drop upload of text/links';
 $string['doclang'] = 'Language for docs';
 $string['docroot'] = 'Moodle Docs document root';
index 4091898..5a84a6f 100644 (file)
@@ -122,6 +122,7 @@ $string['courseaggregation_any'] = 'ANY selected courses to be completed';
 $string['coursealreadycompleted'] = 'You have already completed this course';
 $string['coursecomplete'] = 'Course complete';
 $string['coursecompleted'] = 'Course completed';
+$string['coursecompletedmessage'] = '<p>Congratulations!</p><p>You just completed the following course: <a href="{$a->courselink}">{$a->coursename}</a>.</p>';
 $string['coursecompletion'] = 'Course completion';
 $string['coursecompletioncondition'] = 'Condition: {$a}';
 $string['coursegrade'] = 'Course grade';
index f5a35c1..bf203b5 100644 (file)
@@ -1231,6 +1231,7 @@ $string['messageprovider:badgecreatornotice'] = 'Badge creator notifications';
 $string['messageprovider:badgerecipientnotice'] = 'Badge recipient notifications';
 $string['messageprovider:competencyplancomment'] = 'Comment posted on a learning plan';
 $string['messageprovider:competencyusercompcomment'] = 'Comment posted on a competency';
+$string['messageprovider:coursecompleted'] = 'Course completed';
 $string['messageprovider:courserequestapproved'] = 'Course creation request approval notification';
 $string['messageprovider:courserequested'] = 'Course creation request notification';
 $string['messageprovider:courserequestrejected'] = 'Course creation request rejection notification';
index e20dd2c..84f00e2 100644 (file)
@@ -601,34 +601,36 @@ class core_user {
             // The user has chosen to delete the selected users picture.
             $fs->delete_area_files($context->id, 'user', 'icon'); // Drop all images in area.
             $newpicture = 0;
+        }
 
-        } else {
-            // Save newly uploaded file, this will avoid context mismatch for newly created users.
-            file_save_draft_area_files($usernew->imagefile, $context->id, 'user', 'newicon', 0, $filemanageroptions);
-            if (($iconfiles = $fs->get_area_files($context->id, 'user', 'newicon')) && count($iconfiles) == 2) {
-                // Get file which was uploaded in draft area.
-                foreach ($iconfiles as $file) {
-                    if (!$file->is_directory()) {
-                        break;
-                    }
-                }
-                // Copy file to temporary location and the send it for processing icon.
-                if ($iconfile = $file->copy_content_to_temp()) {
-                    // There is a new image that has been uploaded.
-                    // Process the new image and set the user to make use of it.
-                    // NOTE: Uploaded images always take over Gravatar.
-                    $newpicture = (int)process_new_icon($context, 'user', 'icon', 0, $iconfile);
-                    // Delete temporary file.
-                    @unlink($iconfile);
-                    // Remove uploaded file.
-                    $fs->delete_area_files($context->id, 'user', 'newicon');
-                } else {
-                    // Something went wrong while creating temp file.
-                    // Remove uploaded file.
-                    $fs->delete_area_files($context->id, 'user', 'newicon');
-                    return false;
+        // Save newly uploaded file, this will avoid context mismatch for newly created users.
+        if (!isset($usernew->imagefile)) {
+            $usernew->imagefile = 0;
+        }
+        file_save_draft_area_files($usernew->imagefile, $context->id, 'user', 'newicon', 0, $filemanageroptions);
+        if (($iconfiles = $fs->get_area_files($context->id, 'user', 'newicon')) && count($iconfiles) == 2) {
+            // Get file which was uploaded in draft area.
+            foreach ($iconfiles as $file) {
+                if (!$file->is_directory()) {
+                    break;
                 }
             }
+            // Copy file to temporary location and the send it for processing icon.
+            if ($iconfile = $file->copy_content_to_temp()) {
+                // There is a new image that has been uploaded.
+                // Process the new image and set the user to make use of it.
+                // NOTE: Uploaded images always take over Gravatar.
+                $newpicture = (int)process_new_icon($context, 'user', 'icon', 0, $iconfile);
+                // Delete temporary file.
+                @unlink($iconfile);
+                // Remove uploaded file.
+                $fs->delete_area_files($context->id, 'user', 'newicon');
+            } else {
+                // Something went wrong while creating temp file.
+                // Remove uploaded file.
+                $fs->delete_area_files($context->id, 'user', 'newicon');
+                return false;
+            }
         }
 
         if ($newpicture != $user->picture) {
index eeef52e..1ffde1b 100644 (file)
@@ -86,6 +86,9 @@ $messageproviders = array (
         ),
     ),
 
+    // Course completed. Requires course completion configured at course level. It does not work with just activity progress.
+    'coursecompleted' => [],
+
     // Badge award notification to a badge recipient.
     'badgerecipientnotice' => array (
         'defaults' => array(
index 94f0c0d..c589221 100644 (file)
@@ -287,6 +287,13 @@ $functions = array(
         'type'          => 'read',
         'ajax'          => true,
     ],
+    'core_calendar_get_calendar_export_token' => [
+        'classname'     => 'core_calendar\external\export\token',
+        'methodname'    => 'execute',
+        'description'   => 'Return the auth token required for exporting a calendar.',
+        'type'          => 'read',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ],
     'core_cohort_add_cohort_members' => array(
         'classname' => 'core_cohort_external',
         'methodname' => 'add_cohort_members',
index 7f33390..9a4a50e 100644 (file)
@@ -1217,6 +1217,9 @@ class external_settings {
     /** @var string The session lang */
     private $lang = '';
 
+    /** @var string The timezone to use during this WS request */
+    private $timezone = '';
+
     /**
      * Constructor - protected - can not be instanciated
      */
@@ -1337,6 +1340,24 @@ class external_settings {
     public function get_lang() {
         return $this->lang;
     }
+
+    /**
+     * Set timezone
+     *
+     * @param string $timezone
+     */
+    public function set_timezone($timezone) {
+        $this->timezone = $timezone;
+    }
+
+    /**
+     * Get timezone
+     *
+     * @return string
+     */
+    public function get_timezone() {
+        return $this->timezone;
+    }
 }
 
 /**
index d1270bc..8afd063 100644 (file)
@@ -5984,7 +5984,7 @@ function email_should_be_diverted($email) {
         return true;
     }
 
-    $patterns = array_map('trim', explode(',', $CFG->divertallemailsexcept));
+    $patterns = array_map('trim', preg_split("/[\s,]+/", $CFG->divertallemailsexcept));
     foreach ($patterns as $pattern) {
         if (preg_match("/$pattern/", $email)) {
             return false;
index 53e4071..7b5ff60 100644 (file)
@@ -978,6 +978,32 @@ class core_completionlib_testcase extends advanced_testcase {
         $this->assertEventLegacyData($data, $event);
     }
 
+    /**
+     * Test course completed message.
+     */
+    public function test_course_completed_message() {
+        $this->setup_data();
+        $this->setAdminUser();
+
+        $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
+        $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
+
+        // Mark course as complete and get the message.
+        $sink = $this->redirectMessages();
+        $ccompletion->mark_complete();
+        $messages = $sink->get_messages();
+        $sink->close();
+
+        $this->assertCount(1, $messages);
+        $message = array_pop($messages);
+
+        $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom);
+        $this->assertEquals($this->user->id, $message->useridto);
+        $this->assertEquals('coursecompleted', $message->eventtype);
+        $this->assertEquals(get_string('coursecompleted', 'completion'), $message->subject);
+        $this->assertContains($this->course->fullname, $message->fullmessage);
+    }
+
     /**
      * Test course completed event.
      */
index 0b89adb..c1fba03 100644 (file)
@@ -3297,6 +3297,26 @@ class core_moodlelib_testcase extends advanced_testcase {
                 ),
                 false,
             ),
+            'divertsexceptionsnewline' => array(
+                'divertallemailsto' => 'somewhere@elsewhere.com',
+                'divertallemailsexcept' => "@dev.com\nfred(\+.*)?@example.com",
+                array(
+                    'dev1@dev.com',
+                    'fred@example.com',
+                    'fred+verp@example.com',
+                ),
+                false,
+            ),
+            'alsodivertsnewline' => array(
+                'divertallemailsto' => 'somewhere@elsewhere.com',
+                'divertallemailsexcept' => "@dev.com\nfred(\+.*)?@example.com",
+                array(
+                    'foo@example.com',
+                    'test@real.com',
+                    'fred.jones@example.com',
+                ),
+                true,
+            ),
         );
     }
 
index 03e2009..c450915 100644 (file)
@@ -403,7 +403,7 @@ function folder_dndupload_handle($uploadinfo) {
 function folder_get_coursemodule_info($cm) {
     global $DB;
     if (!($folder = $DB->get_record('folder', array('id' => $cm->instance),
-            'id, name, display, showexpanded, showdownloadfolder, intro, introformat'))) {
+            'id, name, display, showexpanded, showdownloadfolder, forcedownload, intro, introformat'))) {
         return NULL;
     }
     $cminfo = new cached_cm_info();
@@ -413,6 +413,7 @@ function folder_get_coursemodule_info($cm) {
         $fdata = new stdClass();
         $fdata->showexpanded = $folder->showexpanded;
         $fdata->showdownloadfolder = $folder->showdownloadfolder;
+        $fdata->forcedownload = $folder->forcedownload;
         if ($cm->showdescription && strlen(trim($folder->intro))) {
             $fdata->intro = $folder->intro;
             if ($folder->introformat != FORMAT_MOODLE) {
index 842d10a..b491217 100644 (file)
@@ -51,7 +51,8 @@ class backup_qtype_essay_plugin extends backup_qtype_plugin {
         $essay = new backup_nested_element('essay', array('id'), array(
                 'responseformat', 'responserequired', 'responsefieldlines',
                 'attachments', 'attachmentsrequired', 'graderinfo',
-                'graderinfoformat', 'responsetemplate', 'responsetemplateformat', 'filetypeslist'));
+                'graderinfoformat', 'responsetemplate', 'responsetemplateformat',
+                'filetypeslist', 'maxbytes'));
 
         // Now the own qtype tree.
         $pluginwrapper->add_child($essay);
index 88314ab..4177509 100644 (file)
@@ -17,6 +17,7 @@
         <FIELD NAME="graderinfoformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The text format for graderinfo."/>
         <FIELD NAME="responsetemplate" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The template to pre-populate student's response field during attempt."/>
         <FIELD NAME="responsetemplateformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The text format for responsetemplate."/>
+        <FIELD NAME="maxbytes" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Maximum size of attached files in bytes."/>
         <FIELD NAME="filetypeslist" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="What attachment file type a student is allowed to include with their response. * or empty means unlimited."/>
       </FIELDS>
       <KEYS>
index 2022579..6ee1f49 100644 (file)
@@ -30,7 +30,7 @@ defined('MOODLE_INTERNAL') || die();
  * @param int $oldversion the version we are upgrading from.
  */
 function xmldb_qtype_essay_upgrade($oldversion) {
-    global $CFG;
+    global $CFG, $DB;
 
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
@@ -47,5 +47,21 @@ function xmldb_qtype_essay_upgrade($oldversion) {
     // Automatically generated Moodle v3.9.0 release upgrade line.
     // Put any upgrade step following this.
 
+    $dbman = $DB->get_manager();
+    if ($oldversion < 2021052501) {
+
+        // Define field maxbytes to be added to qtype_essay_options.
+        $table = new xmldb_table('qtype_essay_options');
+        $field = new xmldb_field('maxbytes', XMLDB_TYPE_INTEGER, '10', null,
+            XMLDB_NOTNULL, null, '0', 'responsetemplateformat');
+
+        // Conditionally launch add field maxbytes.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Essay savepoint reached.
+        upgrade_plugin_savepoint(true, 2021052501, 'qtype', 'essay');
+    }
     return true;
 }
index 3f97bd3..68f16e8 100644 (file)
@@ -69,6 +69,10 @@ class qtype_essay_edit_form extends question_edit_form {
         $mform->addHelpButton('filetypeslist', 'acceptedfiletypes', 'qtype_essay');
         $mform->disabledIf('filetypeslist', 'attachments', 'eq', 0);
 
+        $mform->addElement('select', 'maxbytes', get_string('maxbytes', 'qtype_essay'), $qtype->max_file_size_options());
+        $mform->setDefault('maxbytes', '0');
+        $mform->disabledIf('maxbytes', 'attachments', 'eq', 0);
+
         $mform->addElement('header', 'responsetemplateheader', get_string('responsetemplateheader', 'qtype_essay'));
         $mform->addElement('editor', 'responsetemplate', get_string('responsetemplate', 'qtype_essay'),
                 array('rows' => 10),  array_merge($this->editoroptions, array('maxfiles' => 0)));
@@ -93,6 +97,7 @@ class qtype_essay_edit_form extends question_edit_form {
         $question->attachments = $question->options->attachments;
         $question->attachmentsrequired = $question->options->attachmentsrequired;
         $question->filetypeslist = $question->options->filetypeslist;
+        $question->maxbytes = $question->options->maxbytes;
 
         $draftid = file_get_submitted_draft_itemid('graderinfo');
         $question->graderinfo = array();
index 424ae8f..1ee6378 100644 (file)
@@ -36,6 +36,7 @@ $string['formatnoinline'] = 'No online text';
 $string['formatplain'] = 'Plain text';
 $string['graderinfo'] = 'Information for graders';
 $string['graderinfoheader'] = 'Grader Information';
+$string['maxbytes'] = 'Maximum file size';
 $string['mustattach'] = 'When "No online text" is selected, or responses are optional, you must allow at least one attachment.';
 $string['mustrequire'] = 'When "No online text" is selected, or responses are optional, you must require at least one attachment.';
 $string['mustrequirefewer'] = 'You cannot require more attachments than you allow.';
index 73466bf..aa8cbf4 100644 (file)
@@ -44,6 +44,9 @@ class qtype_essay_question extends question_with_responses {
     public $responsefieldlines;
     public $attachments;
 
+    /** @var int maximum file size in bytes */
+    public $maxbytes;
+
     /** @var int The number of attachments required for a response to be complete. */
     public $attachmentsrequired;
 
index 71f03d8..5420a24 100644 (file)
@@ -72,6 +72,7 @@ class qtype_essay extends question_type {
         } else {
             $options->filetypeslist = $formdata->filetypeslist;
         }
+        $options->maxbytes = $formdata->maxbytes ?? 0;
         $options->graderinfo = $this->import_or_save_files($formdata->graderinfo,
                 $context, 'qtype_essay', 'graderinfo', $formdata->id);
         $options->graderinfoformat = $formdata->graderinfo['format'];
@@ -93,6 +94,7 @@ class qtype_essay extends question_type {
         $question->responsetemplateformat = $questiondata->options->responsetemplateformat;
         $filetypesutil = new \core_form\filetypes_util();
         $question->filetypeslist = $filetypesutil->normalize_file_types($questiondata->options->filetypeslist);
+        $question->maxbytes = $questiondata->options->maxbytes;
     }
 
     public function delete_question($questionid, $contextid) {
@@ -162,6 +164,15 @@ class qtype_essay extends question_type {
         );
     }
 
+    /**
+     * Return array of the choices that should be offered for the maximum file sizes.
+     * @return array|lang_string[]|string[]
+     */
+    public function max_file_size_options() {
+        global $CFG, $COURSE;
+        return get_max_upload_sizes($CFG->maxbytes, $COURSE->maxbytes);
+    }
+
     public function move_files($questionid, $oldcontextid, $newcontextid) {
         parent::move_files($questionid, $oldcontextid, $newcontextid);
         $fs = get_file_storage();
index 06116ea..a0cb569 100644 (file)
@@ -45,7 +45,7 @@ class qtype_essay_renderer extends qtype_renderer {
 
         if (!$step->has_qt_var('answer') && empty($options->readonly)) {
             // Question has never been answered, fill it with response template.
-            $step = new question_attempt_step(array('answer'=>$question->responsetemplate));
+            $step = new question_attempt_step(array('answer' => $question->responsetemplate));
         }
 
         if (empty($options->readonly)) {
@@ -122,6 +122,7 @@ class qtype_essay_renderer extends qtype_renderer {
         $pickeroptions->accepted_types = $qa->get_question()->filetypeslist;
 
         $fm = new form_filemanager($pickeroptions);
+        $fm->options->maxbytes = $qa->get_question()->maxbytes;;
         $filesrenderer = $this->page->get_renderer('core', 'files');
 
         $text = '';
index a531afc..9b8357b 100644 (file)
@@ -23,3 +23,7 @@
 .que.essay div.qtype_essay_response textarea {
     width: 100%;
 }
+
+.que.essay .ablock .filemanager .fp-restrictions {
+    margin-top: 1em;
+}
diff --git a/question/type/essay/tests/behat/max_file_size.feature b/question/type/essay/tests/behat/max_file_size.feature
new file mode 100644 (file)
index 0000000..943bae1
--- /dev/null
@@ -0,0 +1,32 @@
+@qtype @qtype_essay
+Feature: In an essay question, let the question author choose the maxbytes for attachments
+In order to constrain student submissions for marking
+As a teacher
+I need to choose the appropriate maxbytes for attachments
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email               |
+      | teacher1 | T1        | Teacher1 | teacher1@moodle.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+    And the following "question categories" exist:
+      | contextlevel | reference | name           |
+      | Course       | C1        | Test questions |
+    And the following "questions" exist:
+      | questioncategory | qtype | name          | template         | attachments | maxbytes |
+      | Test questions   | essay | essay-1-20MB  | editor           | 1           | 20971520 |
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I navigate to "Question bank" in current page administration
+
+  @javascript @_switch_window
+  Scenario: Preview an Essay question and see the allowed maximum file sizes and number of attachments.
+    When I choose "Preview" action for "essay-1-20MB" in the question bank
+    And I switch to "questionpreview" window
+    And I should see "Please write a story about a frog."
+    And I should see "Maximum file size: 20MB, maximum number of files: 1"
+    And I switch to the main window
index c759bbd..984e5b7 100644 (file)
@@ -89,6 +89,7 @@ class qtype_essay_test_helper extends question_test_helper {
         $fromform->attachments = 0;
         $fromform->attachmentsrequired = 0;
         $fromform->filetypeslist = '';
+        $fromform->maxbytes = 0;
         $fromform->graderinfo = array('text' => '', 'format' => FORMAT_HTML);
         $fromform->responsetemplate = array('text' => '', 'format' => FORMAT_HTML);
 
@@ -140,6 +141,7 @@ class qtype_essay_test_helper extends question_test_helper {
         $fromform->attachments = 3;
         $fromform->attachmentsrequired = 0;
         $fromform->filetypeslist = '';
+        $fromform->maxbytes = 0;
         $fromform->graderinfo = array('text' => '', 'format' => FORMAT_HTML);
         $fromform->responsetemplate = array('text' => '', 'format' => FORMAT_HTML);
 
@@ -176,6 +178,7 @@ class qtype_essay_test_helper extends question_test_helper {
         $fromform->attachments = 0;
         $fromform->attachmentsrequired = 0;
         $fromform->filetypeslist = '';
+        $fromform->maxbytes = 0;
         $fromform->graderinfo = array('text' => '', 'format' => FORMAT_HTML);
         $fromform->responsetemplate = array('text' => '', 'format' => FORMAT_HTML);
 
@@ -209,6 +212,7 @@ class qtype_essay_test_helper extends question_test_helper {
         $q->attachments = 3;
         $q->attachmentsrequired = 1;
         $q->filetypeslist = '';
+        $q->maxbytes = 0;
         return $q;
     }
 
index c9e0316..34029cd 100644 (file)
@@ -26,7 +26,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component = 'qtype_essay';
-$plugin->version   = 2021052500;
+$plugin->version   = 2021052501;
 
 $plugin->requires  = 2021052500;
 
index 65b5cd0..0b545e3 100644 (file)
 
 #core-cache-rescan-definitions,
 #core-cache-mode-mappings .edit-link,
-#core-cache-lock-summary .new-instance {
+#core-cache-lock-additional-actions .new-instance {
     margin-top: 0.5em;
     text-align: center;
 }
index 6a60c9a..916d24e 100644 (file)
@@ -12420,7 +12420,7 @@ input[disabled] {
 
 #core-cache-rescan-definitions,
 #core-cache-mode-mappings .edit-link,
-#core-cache-lock-summary .new-instance {
+#core-cache-lock-additional-actions .new-instance {
   margin-top: 0.5em;
   text-align: center; }
 
index 37f2d5b..72bfb01 100644 (file)
@@ -12634,7 +12634,7 @@ input[disabled] {
 
 #core-cache-rescan-definitions,
 #core-cache-mode-mappings .edit-link,
-#core-cache-lock-summary .new-instance {
+#core-cache-lock-additional-actions .new-instance {
   margin-top: 0.5em;
   text-align: center; }
 
index 3c05ae9..bf55c8c 100644 (file)
@@ -29,9 +29,9 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2021052500.16;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2021052500.17;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
-$release  = '4.0dev (Build: 20200924)'; // Human-friendly version name
+$release  = '4.0dev (Build: 20200929)'; // Human-friendly version name
 $branch   = '400';                      // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.
index fe6c061..5257640 100644 (file)
@@ -1160,6 +1160,7 @@ abstract class webservice_server implements webservice_server_interface {
             'fileurl' => array('default' => true, 'type' => PARAM_BOOL),
             'filter' => array('default' => false, 'type' => PARAM_BOOL),
             'lang' => array('default' => '', 'type' => PARAM_LANG),
+            'timezone' => array('default' => '', 'type' => PARAM_TIMEZONE),
         );
 
         // Load the external settings with the web service settings.
@@ -1235,7 +1236,7 @@ abstract class webservice_base_server extends webservice_server {
      * @uses die
      */
     public function run() {
-        global $CFG, $SESSION;
+        global $CFG, $USER, $SESSION;
 
         // we will probably need a lot of memory in some functions
         raise_memory_limit(MEMORY_EXTRA);
@@ -1287,6 +1288,12 @@ abstract class webservice_base_server extends webservice_server {
             }
         }
 
+        // Change timezone only in sites where it isn't forced.
+        $newtimezone = $settings->get_timezone();
+        if (!empty($newtimezone) && (!isset($CFG->forcetimezone) || $CFG->forcetimezone == 99)) {
+            $USER->timezone = $newtimezone;
+        }
+
         // finally, execute the function - any errors are catched by the default exception handler
         $this->execute();