Merge branch 'MDL-69758-master' of https://github.com/sammarshallou/moodle into master
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Thu, 1 Oct 2020 21:44:05 +0000 (23:44 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Thu, 1 Oct 2020 21:44:05 +0000 (23:44 +0200)
265 files changed:
.eslintignore
.stylelintignore
admin/admin_settings_search_form.php [deleted file]
admin/cli/svgtool.php
admin/search.php
admin/settings/server.php
admin/tests/behat/behat_admin.php
admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-debug.js
admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-min.js
admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search.js
admin/tool/capability/yui/src/search/js/search.js
admin/tool/dataprivacy/classes/external.php
admin/tool/dataprivacy/classes/output/renderer.php
admin/tool/dbtransfer/locallib.php
admin/tool/mobile/classes/api.php
admin/tool/mobile/classes/external.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
admin/tool/templatelibrary/amd/build/search.min.js
admin/tool/templatelibrary/amd/build/search.min.js.map
admin/tool/templatelibrary/amd/src/search.js
admin/tool/templatelibrary/templates/list_templates_page.mustache
admin/tool/usertours/classes/privacy/provider.php
admin/tool/usertours/classes/tour.php
admin/tool/usertours/db/upgrade.php
admin/tool/usertours/tests/privacy_provider_test.php
admin/tool/usertours/tests/tour_test.php
admin/tool/usertours/version.php
admin/webservice/testclient.php
auth/email/classes/external.php
auth/email/tests/external_test.php
backup/moodle2/restore_stepslib.php
backup/util/ui/renderer.php
badges/tests/badgeslib_test.php
blocks/blog_menu/block_blog_menu.php
blocks/blog_menu/tests/behat/block_blog_menu_activity.feature
blocks/blog_menu/tests/behat/block_blog_menu_course.feature
blocks/classes/external.php
blocks/globalsearch/block_globalsearch.php
blocks/search_forums/classes/output/search_form.php
blocks/search_forums/templates/search_form.mustache
blocks/search_forums/tests/behat/block_search_forums_course.feature
blocks/search_forums/tests/behat/block_search_forums_frontpage.feature
blocks/settings/renderer.php
blocks/tests/externallib_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/externallib.php
calendar/lib.php
calendar/tests/externallib_test.php
calendar/tests/lib_test.php
cohort/index.php
competency/classes/external/competency_framework_exporter.php
completion/completion_completion.php
config-dist.php
contentbank/amd/build/search.min.js
contentbank/amd/build/search.min.js.map
contentbank/amd/build/selectors.min.js
contentbank/amd/build/selectors.min.js.map
contentbank/amd/src/search.js
contentbank/amd/src/selectors.js
contentbank/classes/contentbank.php
contentbank/edit.php
contentbank/index.php
contentbank/templates/bankcontent/search.mustache
contentbank/tests/contentbank_test.php
contentbank/upload.php
course/amd/build/local/activitychooser/dialogue.min.js
course/amd/build/local/activitychooser/dialogue.min.js.map
course/amd/build/local/activitychooser/selectors.min.js
course/amd/build/local/activitychooser/selectors.min.js.map
course/amd/src/local/activitychooser/dialogue.js
course/amd/src/local/activitychooser/selectors.js
course/classes/category.php
course/classes/management_renderer.php
course/externallib.php
course/lib.php
course/management.php
course/renderer.php
course/search.php
course/templates/local/activitychooser/search.mustache
course/tests/behat/course_search.feature
course/tests/externallib_test.php
course/upgrade.txt
enrol/externallib.php
enrol/tests/externallib_test.php
filter/algebra/filter.php
filter/tex/lib.php
grade/report/singleview/classes/local/screen/screen.php
grade/report/singleview/js/singleview.js
grade/report/singleview/lang/en/gradereport_singleview.php
install/lang/ar/error.php
install/lang/ar/moodle.php
install/lang/te/error.php
lang/en/admin.php
lang/en/completion.php
lang/en/contentbank.php
lang/en/deprecated.txt
lang/en/form.php
lang/en/moodle.php
lang/en/repository.php
lib/adminlib.php
lib/amd/build/search-input.min.js [deleted file]
lib/amd/build/search-input.min.js.map [deleted file]
lib/amd/build/templates.min.js.map
lib/amd/src/search-input.js [deleted file]
lib/amd/src/templates.js
lib/classes/component.php
lib/classes/files/curl_security_helper.php
lib/classes/output/mustache_engine.php
lib/classes/output/mustache_helper_collection.php
lib/classes/user.php
lib/datalib.php
lib/db/caches.php
lib/db/install.php
lib/db/install.xml
lib/db/messages.php
lib/db/services.php
lib/db/upgrade.php
lib/deprecatedlib.php
lib/enrollib.php
lib/external/externallib.php
lib/external/tests/external_test.php
lib/externallib.php
lib/filelib.php
lib/filestorage/tests/fixtures/passwordis1.zip [new file with mode: 0644]
lib/filestorage/tests/zip_packer_test.php
lib/form/classes/filetypes_util.php
lib/form/filemanager.php
lib/form/filepicker.php
lib/form/filetypes.php
lib/form/tests/filetypes_util_test.php
lib/http-message/LICENSE [new file with mode: 0644]
lib/http-message/readme_moodle.txt [new file with mode: 0644]
lib/http-message/src/MessageInterface.php [new file with mode: 0644]
lib/http-message/src/RequestInterface.php [new file with mode: 0644]
lib/http-message/src/ResponseInterface.php [new file with mode: 0644]
lib/http-message/src/ServerRequestInterface.php [new file with mode: 0644]
lib/http-message/src/StreamInterface.php [new file with mode: 0644]
lib/http-message/src/UploadedFileInterface.php [new file with mode: 0644]
lib/http-message/src/UriInterface.php [new file with mode: 0644]
lib/licenselib.php
lib/moodlelib.php
lib/myprofilelib.php
lib/outputrenderers.php
lib/php-enum/LICENSE [new file with mode: 0644]
lib/php-enum/readme_moodle.txt [new file with mode: 0644]
lib/php-enum/src/Enum.php [new file with mode: 0644]
lib/phpunit/classes/util.php
lib/setuplib.php
lib/templates/checkbox.mustache [new file with mode: 0644]
lib/templates/popover_region.mustache
lib/templates/search_input.mustache [new file with mode: 0644]
lib/templates/search_input_auto.mustache [new file with mode: 0644]
lib/templates/search_input_navbar.mustache [new file with mode: 0644]
lib/tests/completionlib_test.php
lib/tests/curl_security_helper_test.php
lib/tests/datalib_test.php
lib/tests/moodlelib_test.php
lib/tests/output_mustache_helper_collection_test.php
lib/tests/setuplib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/upgradelib.php
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js
lib/yui/src/notification/js/exception.js
lib/zipstream/LICENSE [new file with mode: 0644]
lib/zipstream/readme_moodle.txt [new file with mode: 0644]
lib/zipstream/src/Bigint.php [new file with mode: 0644]
lib/zipstream/src/DeflateStream.php [new file with mode: 0644]
lib/zipstream/src/Exception.php [new file with mode: 0644]
lib/zipstream/src/Exception/EncodingException.php [new file with mode: 0644]
lib/zipstream/src/Exception/FileNotFoundException.php [new file with mode: 0644]
lib/zipstream/src/Exception/FileNotReadableException.php [new file with mode: 0644]
lib/zipstream/src/Exception/IncompatibleOptionsException.php [new file with mode: 0644]
lib/zipstream/src/Exception/OverflowException.php [new file with mode: 0644]
lib/zipstream/src/Exception/StreamNotReadableException.php [new file with mode: 0644]
lib/zipstream/src/File.php [new file with mode: 0644]
lib/zipstream/src/Option/Archive.php [new file with mode: 0644]
lib/zipstream/src/Option/File.php [new file with mode: 0644]
lib/zipstream/src/Option/Method.php [new file with mode: 0644]
lib/zipstream/src/Option/Version.php [new file with mode: 0644]
lib/zipstream/src/Stream.php [new file with mode: 0644]
lib/zipstream/src/ZipStream.php [new file with mode: 0644]
message/externallib.php
message/lib.php
message/templates/message_drawer_view_overview_header.mustache
message/templates/message_drawer_view_search_header.mustache
message/templates/message_popover.mustache
message/tests/externallib_test.php
mnet/xmlrpc/serverlib.php
mod/assign/externallib.php
mod/folder/lib.php
mod/forum/classes/output/quick_search_form.php
mod/forum/externallib.php
mod/forum/templates/quick_search_form.mustache
mod/glossary/view.php
mod/lti/amd/build/contentitem.min.js
mod/lti/amd/build/contentitem.min.js.map
mod/lti/amd/src/contentitem.js
mod/lti/auth.php
mod/lti/edit_form.php
mod/lti/lang/en/deprecated.txt
mod/lti/lang/en/lti.php
mod/lti/locallib.php
mod/lti/mod_form.js
mod/lti/mod_form.php
mod/lti/templates/tool_deeplinking_results.mustache [new file with mode: 0644]
mod/lti/tests/behat/contentitem.feature
mod/lti/tests/behat/contentitemregistration.feature
mod/lti/tests/locallib_test.php
mod/lti/upgrade.txt
mod/quiz/attemptlib.php
mod/quiz/classes/external.php
mod/quiz/module.js
mod/quiz/renderer.php
mod/quiz/styles.css
mod/quiz/templates/timer.mustache [moved from blocks/settings/templates/search_form.mustache with 52% similarity]
mod/quiz/tests/external_test.php
mod/quiz/upgrade.txt
mod/wiki/lib.php
mod/workshop/locallib.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
repository/draftfiles_ajax.php
search/tests/behat/behat_search.php
tag/manage.php
theme/boost/scss/moodle/admin.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/modules.scss
theme/boost/scss/moodle/popover-region.scss
theme/boost/scss/moodle/search.scss
theme/boost/style/moodle.css
theme/boost/templates/navbar.mustache
theme/classic/style/moodle.css
theme/classic/templates/navbar.mustache
user/editlib.php
user/tests/behat/view_full_profile.feature
version.php
webservice/lib.php
webservice/tests/helpers.php
webservice/upgrade.txt

index e4546c5..0d0a6ea 100644 (file)
@@ -67,6 +67,9 @@ lib/babel-polyfill/
 lib/polyfills/
 lib/emoji-data/
 lib/plist/
+lib/zipstream/
+lib/php-enum/
+lib/http-message/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
index 5fae1d4..0207e41 100644 (file)
@@ -68,6 +68,9 @@ lib/babel-polyfill/
 lib/polyfills/
 lib/emoji-data/
 lib/plist/
+lib/zipstream/
+lib/php-enum/
+lib/http-message/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
diff --git a/admin/admin_settings_search_form.php b/admin/admin_settings_search_form.php
deleted file mode 100644 (file)
index ad42300..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-<?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/>.
-
-/**
- * Admin settings search form
- *
- * @package    admin
- * @copyright  2016 Damyon Wiese
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-require_once $CFG->libdir.'/formslib.php';
-
-/**
- * Admin settings search form
- *
- * @package    admin
- * @copyright  2016 Damyon Wiese
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class admin_settings_search_form extends moodleform {
-    function definition () {
-        $mform = $this->_form;
-
-        //$mform->addElement('header', 'settingsheader', get_string('search', 'admin'));
-        $elements = [];
-        $elements[] = $mform->createElement('text', 'query', get_string('query', 'admin'));
-        $elements[] = $mform->createElement('submit', 'search', get_string('search'));
-        $mform->addGroup($elements);
-        $mform->setType('query', PARAM_RAW);
-        $mform->setDefault('query', optional_param('query', '', PARAM_RAW));
-    }
-}
index 929a7c9..e83ef1d 100644 (file)
@@ -38,17 +38,17 @@ if ($unrecognized) {
 }
 
 // If necessary add files that should be ignored - such as in 3rd party plugins.
-$blacklist = array();
+$ignorelist = array();
 $path = $options['path'];
 if (!file_exists($path)) {
     cli_error("Invalid path $path");
 }
 
 if ($options['ie9fix']) {
-    core_admin_recurse_svgs($path, '', 'core_admin_svgtool_ie9fix', $blacklist);
+    core_admin_recurse_svgs($path, '', 'core_admin_svgtool_ie9fix', $ignorelist);
 
 } else if ($options['noaspectratio']) {
-    core_admin_recurse_svgs($path, '', 'core_admin_svgtool_noaspectratio', $blacklist);
+    core_admin_recurse_svgs($path, '', 'core_admin_svgtool_noaspectratio', $ignorelist);
 
 } else {
     $help =
@@ -153,9 +153,9 @@ function core_admin_svgtool_noaspectratio($file) {
  * @param string $base
  * @param string $sub
  * @param string $filecallback
- * @param array $blacklist
+ * @param array $ignorelist List of files to be ignored and skipped.
  */
-function core_admin_recurse_svgs($base, $sub, $filecallback, $blacklist) {
+function core_admin_recurse_svgs($base, $sub, $filecallback, $ignorelist) {
     if (is_dir("$base/$sub")) {
         $items = new DirectoryIterator("$base/$sub");
         foreach ($items as $item) {
@@ -163,7 +163,7 @@ function core_admin_recurse_svgs($base, $sub, $filecallback, $blacklist) {
                 continue;
             }
             $file = $item->getFilename();
-            core_admin_recurse_svgs("$base/$sub", $file, $filecallback, $blacklist);
+            core_admin_recurse_svgs("$base/$sub", $file, $filecallback, $ignorelist);
         }
         unset($item);
         unset($items);
@@ -174,7 +174,7 @@ function core_admin_recurse_svgs($base, $sub, $filecallback, $blacklist) {
             return;
         }
         $file = realpath("$base/$sub");
-        if (in_array($file, $blacklist)) {
+        if (in_array($file, $ignorelist)) {
             return;
         }
         $filecallback($file);
index 5539517..98ec900 100644 (file)
@@ -68,9 +68,16 @@ if ($errormsg !== '') {
 $showsettingslinks = true;
 
 if ($hassiteconfig) {
-    require_once("admin_settings_search_form.php");
-    $form = new admin_settings_search_form();
-    $form->display();
+    $data = [
+        'action' => new moodle_url('/admin/search.php'),
+        'btnclass' => 'btn-primary',
+        'inputname' => 'query',
+        'searchstring' => get_string('search'),
+        'query' => $query,
+        'extraclasses' => 'd-flex justify-content-center'
+    ];
+    echo $OUTPUT->render_from_template('core/search_input', $data);
+
     echo '<hr>';
     if ($query) {
         echo admin_search_settings_html($query);
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 af027de..98f5d91 100644 (file)
@@ -56,7 +56,7 @@ class behat_admin extends behat_base {
             $this->execute('behat_navigation::i_select_from_flat_navigation_drawer', [get_string('administrationsite')]);
 
             // Search by label.
-            $this->execute('behat_forms::i_set_the_field_to', [get_string('query', 'admin'), $label]);
+            $this->execute('behat_forms::i_set_the_field_to', [get_string('search'), $label]);
             $this->execute("behat_forms::press_button", get_string('search', 'admin'));
 
             // Admin settings does not use the same DOM structure than other moodle forms
index 1c05534..ed3d79c 100644 (file)
Binary files a/admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-debug.js and b/admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-debug.js differ
index 6520ca2..30e97fc 100644 (file)
Binary files a/admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-min.js and b/admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search-min.js differ
index 1c05534..ed3d79c 100644 (file)
Binary files a/admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search.js and b/admin/tool/capability/yui/build/moodle-tool_capability-search/moodle-tool_capability-search.js differ
index 1e4b96c..71bd152 100644 (file)
@@ -51,6 +51,13 @@ SEARCH.prototype = {
      * @protected
      */
     button: null,
+    /**
+     * The cancel button for the form.
+     * @property button
+     * @type Node
+     * @protected
+     */
+    cancel: null,
     /**
      * The last search node if there is one.
      * If there is a last search node then the last search term will be persisted between requests.
@@ -74,17 +81,28 @@ SEARCH.prototype = {
         this.button = this.form.all('input[type=submit]');
         this.lastsearch = this.form.one('input[name=search]');
 
-        var div = Y.Node.create('<div id="capabilitysearchui" data-fieldtype="text"></div>'),
-            label = Y.Node.create('<label for="capabilitysearch">' + this.get('strsearch') + '</label>');
-        this.input = Y.Node.create('<input type="text" id="capabilitysearch" />');
+        var div = Y.Node.create('<div id="capabilitysearchui" class="input-group simplesearchform mb-2"' +
+            'data-fieldtype="text"></div>'),
+            label = Y.Node.create('<label for="capabilitysearch"><span class="sr-only"' +
+                this.get('strsearch') + '</span></label>');
+        this.cancel = Y.Node.create('<a class="btn btn-clear d-none icon-no-margin">' +
+                '<i class="icon fa fa-times fa-fw " aria-hidden="true"></i>' +
+                '</a>');
+        this.input = Y.Node.create('<input type="text" class="form-control withclear" placeholder="' +
+            this.get('strsearch') + '"id="capabilitysearch" />');
 
-        div.append(label).append(this.input);
+        div.append(label).append(this.input).append(this.cancel);
 
         this.select.insert(div, 'before');
 
         this.input.on('keyup', this.typed, this);
         this.select.on('change', this.validate, this);
 
+        this.cancel.on('click', function() {
+            this.input.set('value', '');
+            this.typed();
+        }, this);
+
         if (this.lastsearch) {
             this.input.set('value', this.lastsearch.get('value'));
             this.typed();
@@ -131,6 +149,11 @@ SEARCH.prototype = {
                 last.set('selected', true);
             }
         }
+        if (search !== '') {
+            this.cancel.removeClass("d-none");
+        } else {
+            this.cancel.addClass("d-none");
+        }
         this.validate();
     }
 };
index 4c4e719..7306eb2 100644 (file)
@@ -1613,7 +1613,7 @@ class external extends external_api {
      */
     private static function get_tree_node_structure($allowchildbranches = true) {
         $fields = [
-            'text' => new external_value(PARAM_TEXT, 'The node text', VALUE_REQUIRED),
+            'text' => new external_value(PARAM_RAW, 'The node text', VALUE_REQUIRED),
             'expandcontextid' => new external_value(PARAM_INT, 'The contextid this node expands', VALUE_REQUIRED),
             'expandelement' => new external_value(PARAM_ALPHA, 'What element is this node expanded to', VALUE_REQUIRED),
             'contextid' => new external_value(PARAM_INT, 'The node contextid', VALUE_REQUIRED),
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);
     }
index f37f62a..eb0eaba 100644 (file)
@@ -142,7 +142,7 @@ function tool_dbtransfer_get_drivers() {
         $dblibrary = $matches[2];
 
         if ($dbtype === 'sqlite3') {
-            // Blacklist unfinished drivers.
+            // The sqlite3 driver is not fully working yet and should not be returned.
             continue;
         }
 
index 006d917..0fd5f72 100644 (file)
@@ -488,6 +488,7 @@ class api {
                 '$mmSideMenuDelegate_mmaCompetency' => new lang_string('myplans', 'tool_lp'),
                 'CoreMainMenuDelegate_AddonBlog' => new lang_string('blog', 'blog'),
                 '$mmSideMenuDelegate_mmaFiles' => new lang_string('files'),
+                'CoreMainMenuDelegate_CoreTag' => new lang_string('tags'),
                 '$mmSideMenuDelegate_website' => new lang_string('webpage'),
                 '$mmSideMenuDelegate_help' => new lang_string('help'),
                 'CoreMainMenuDelegate_QrReader' => new lang_string('scanqrcode', 'tool_mobile'),
index b3c56bb..8b9057c 100644 (file)
@@ -139,7 +139,7 @@ class external extends external_api {
             array(
                 'wwwroot' => new external_value(PARAM_RAW, 'Site URL.'),
                 'httpswwwroot' => new external_value(PARAM_RAW, 'Site https URL (if httpslogin is enabled).'),
-                'sitename' => new external_value(PARAM_TEXT, 'Site name.'),
+                'sitename' => new external_value(PARAM_RAW, 'Site name.'),
                 'guestlogin' => new external_value(PARAM_INT, 'Whether guest login is enabled.'),
                 'rememberusername' => new external_value(PARAM_INT, 'Values: 0 for No, 1 for Yes, 2 for optional.'),
                 'authloginviaemail' => new external_value(PARAM_INT, 'Whether log in via email is enabled.'),
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 ea1aac5..74f8c50 100644 (file)
Binary files a/admin/tool/templatelibrary/amd/build/search.min.js and b/admin/tool/templatelibrary/amd/build/search.min.js differ
index 1611c7e..0245bb8 100644 (file)
Binary files a/admin/tool/templatelibrary/amd/build/search.min.js.map and b/admin/tool/templatelibrary/amd/build/search.min.js.map differ
index 18623df..9caf344 100644 (file)
@@ -45,8 +45,13 @@ define(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates'
      */
     var refreshSearch = function(themename) {
         var componentStr = $('[data-field="component"]').val();
-        var searchStr = $('[data-field="search"]').val();
+        var searchStr = $('[data-region="list-templates"] [data-region="input"]').val();
 
+        if (searchStr !== '') {
+            $('[data-region="list-templates"] [data-action="clearsearch"]').removeClass('d-none');
+        } else {
+            $('[data-region="list-templates"] [data-action="clearsearch"]').addClass('d-none');
+        }
         // Trigger the search.
         document.location.hash = searchStr;
 
@@ -84,9 +89,14 @@ define(['jquery', 'core/ajax', 'core/log', 'core/notification', 'core/templates'
     };
     // Add change handlers to refresh the list.
     $('[data-region="list-templates"]').on('change', '[data-field="component"]', changeHandler);
-    $('[data-region="list-templates"]').on('input', '[data-field="search"]', changeHandler);
+    $('[data-region="list-templates"]').on('input', '[data-region="input"]', changeHandler);
+    $('[data-action="clearsearch"]').on('click', function() {
+        $('[data-region="input"]').val('');
+        refreshSearch(config.theme);
+        $(this).addClass('d-none');
+    });
 
-    $('[data-field="search"]').val(document.location.hash.replace('#', ''));
+    $('[data-region="input"]').val(document.location.hash.replace('#', ''));
     refreshSearch(config.theme);
     return {};
 });
index 0526c94..c5a454e 100644 (file)
     Context variables required for this template:
     * allcomponents - array of components containing templates. Each component has a name and a component attribute.
 
+}}
+{{!
+    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/>.
+}}
+{{!
+    @template tool_templatelibrary/list_templates_page
+
+    Moodle template to the template library
+
+    The purpose of this template is build the entire page for the template library (by including smaller templates).
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * data-region, data-field
+
+    Context variables required for this template:
+    * allcomponents - array of components containing templates. Each component has a name and a component attribute.
+
 }}
 <div data-region="list-templates">
     <form class="form-horizontal">
-        <div class="control-group">
-            <label for="selectcomponent" class="control-label">{{#str}}component, tool_templatelibrary{{/str}}</label>
-            <div class="controls">
-                <select id="selectcomponent" data-field="component">
-                    <option value="">{{#str}}all, tool_templatelibrary{{/str}}</option>
-                    {{#allcomponents}}
-                        <option value="{{component}}">{{name}}</option>
-                    {{/allcomponents}}
-                </select>
-            </div>
-        </div>
-        <div class="control-group">
-            <label for="search" class="control-label">{{#str}}search, tool_templatelibrary{{/str}}</label>
-            <div class="controls">
-                <input type="text" id="search" data-field="search"/>
-            </div>
-        </div>
+    {{< core_form/element-template }}
+        {{$label}}
+            <div class="col-form-label">{{#str}}component, tool_templatelibrary{{/str}}</div>
+        {{/label}}
+
+        {{$element}}
+            <select id="selectcomponent" class="form-control" data-field="component">
+                <option value="">{{#str}}all, tool_templatelibrary{{/str}}</option>
+                {{#allcomponents}}
+                    <option value="{{component}}">{{name}}</option>
+                {{/allcomponents}}
+            </select>
+        {{/element}}
+    {{/ core_form/element-template }}
+
+    {{< core_form/element-template }}
+        {{$element}}
+            {{< core/search_input_auto }}
+                {{$label}}{{{ searchstring }}}{{/label}}
+                {{$placeholder}}{{#str}}
+                    search, core
+                {{/str}}{{/placeholder}}
+            {{/ core/search_input_auto }}
+        {{/element}}
+    {{/ core_form/element-template }}
     </form>
     <hr/>
     {{> tool_templatelibrary/search_results }}
index 3713d07..21b70de 100644 (file)
@@ -47,7 +47,7 @@ class provider implements
     /**
      * Returns meta data about this system.
      *
-     * @param   collection     $itemcollection The initialised item collection to add items to.
+     * @param   collection     $items The initialised item collection to add items to.
      * @return  collection     A listing of user data stored through this system.
      */
     public static function get_metadata(collection $items) : collection {
@@ -64,7 +64,7 @@ class provider implements
      * @param   int         $userid The userid of the user whose data is to be exported.
      */
     public static function export_user_preferences(int $userid) {
-        $preferences = get_user_preferences();
+        $preferences = get_user_preferences(null, null, $userid);
         foreach ($preferences as $name => $value) {
             $descriptionidentifier = null;
             $tourid = null;
index 4c4a201..0765ee3 100644 (file)
@@ -557,9 +557,10 @@ class tour {
 
         // Remove the configuration for the tour.
         $DB->delete_records('tool_usertours_tours', array('id' => $this->id));
-
         helper::reset_tour_sortorder();
 
+        $this->remove_user_preferences();
+
         return null;
     }
 
@@ -585,6 +586,16 @@ class tour {
         return $this;
     }
 
+    /**
+     * Remove stored user preferences for the tour
+     */
+    protected function remove_user_preferences(): void {
+        global $DB;
+
+        $DB->delete_records('user_preferences', ['name' => self::TOUR_LAST_COMPLETED_BY_USER . $this->get_id()]);
+        $DB->delete_records('user_preferences', ['name' => self::TOUR_REQUESTED_BY_USER . $this->get_id()]);
+    }
+
     /**
      * Whether this tour should be displayed to the user.
      *
@@ -665,11 +676,9 @@ class tour {
      * @return  $this
      */
     public function mark_major_change() {
-        global $DB;
-
         // Clear old reset and completion notes.
-        $DB->delete_records('user_preferences', ['name' => self::TOUR_LAST_COMPLETED_BY_USER . $this->get_id()]);
-        $DB->delete_records('user_preferences', ['name' => self::TOUR_REQUESTED_BY_USER . $this->get_id()]);
+        $this->remove_user_preferences();
+
         $this->set_config('majorupdatetime', time());
         $this->persist();
 
index 6769e01..66420f5 100644 (file)
@@ -25,6 +25,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 use tool_usertours\manager;
+use tool_usertours\tour;
 
 /**
  * Upgrade the user tours plugin.
@@ -57,5 +58,26 @@ function xmldb_tool_usertours_upgrade($oldversion) {
     // Automatically generated Moodle v3.9.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2021052501) {
+        // Clean up user preferences of deleted tours.
+        $select = $DB->sql_like('name', ':lastcompleted') . ' OR ' . $DB->sql_like('name', ':requested');
+        $params = [
+            'lastcompleted' => tour::TOUR_LAST_COMPLETED_BY_USER . '%',
+            'requested' => tour::TOUR_REQUESTED_BY_USER . '%',
+        ];
+
+        $preferences = $DB->get_records_select('user_preferences', $select, $params, '', 'DISTINCT name');
+        foreach ($preferences as $preference) {
+            // Match tour ID at the end of the preference name, remove all of that preference type if tour ID doesn't exist.
+            if (preg_match('/(?<tourid>\d+)$/', $preference->name, $matches) &&
+                    !$DB->record_exists('tool_usertours_tours', ['id' => $matches['tourid']])) {
+
+                $DB->delete_records('user_preferences', ['name' => $preference->name]);
+            }
+        }
+
+        upgrade_plugin_savepoint(true, 2021052501, 'tool', 'usertours');
+    }
+
     return true;
 }
index 17138da..8e552e1 100644 (file)
@@ -15,9 +15,9 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Unit tests for the block_html implementation of the privacy API.
+ * Unit tests for the tool_usertours implementation of the privacy API.
  *
- * @package    block_html
+ * @package    tool_usertours
  * @category   test
  * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -27,19 +27,22 @@ defined('MOODLE_INTERNAL') || die();
 
 use \core_privacy\local\metadata\collection;
 use \core_privacy\local\request\writer;
-use \core_privacy\local\request\approved_contextlist;
-use \core_privacy\local\request\deletion_criteria;
 use \tool_usertours\tour;
 use \tool_usertours\privacy\provider;
 
 /**
- * Unit tests for the block_html implementation of the privacy API.
+ * Unit tests for the tool_usertours implementation of the privacy API.
  *
  * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class tool_usertours_privacy_testcase extends \core_privacy\tests\provider_testcase {
+class tool_usertours_privacy_provider_testcase extends \core_privacy\tests\provider_testcase {
 
+    /**
+     * Helper method for creating a tour
+     *
+     * @return tour
+     */
     protected function create_test_tour(): tour {
         return (new tour())
             ->set_name('test_tour')
@@ -118,6 +121,37 @@ class tool_usertours_privacy_testcase extends \core_privacy\tests\provider_testc
         $this->assertCount(2, (array) $prefs);
     }
 
+    /**
+     * Make sure we are exporting preferences for the correct user
+     */
+    public function test_export_user_preferences_correct_user(): void {
+        $this->resetAfterTest();
+
+        $tour = $this->create_test_tour();
+
+        // Create test user, mark them as having completed the tour.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+        $tour->mark_user_completed();
+
+        // Switch to admin user, mark them as having reset the tour.
+        $this->setAdminUser();
+        $tour->request_user_reset();
+
+        // Export test users preferences.
+        provider::export_user_preferences($user->id);
+
+        $writer = writer::with_context(\context_system::instance());
+        $this->assertTrue($writer->has_any_data());
+
+        $prefs = $writer->get_user_preferences('tool_usertours');
+        $this->assertCount(1, (array) $prefs);
+
+        // We should have received back the "completed tour" preference of the test user.
+        $this->assertStringStartsWith('You last marked the "' . $tour->get_name() . '" user tour as completed on',
+            reset($prefs)->description);
+    }
+
     /**
      * Ensure that export_user_preferences excludes deleted tours.
      */
index d08a9df..c5bf23f 100644 (file)
@@ -64,7 +64,7 @@ class tour_testcase extends advanced_testcase {
     /**
      * Helper to mock the database.
      *
-     * @return moodle_database
+     * @return \PHPUnit\Framework\MockObject\MockObject
      */
     public function mock_database() {
         global $DB;
@@ -569,9 +569,14 @@ class tour_testcase extends advanced_testcase {
 
         // Mock the database.
         $DB = $this->mock_database();
-        $DB->expects($this->once())
+
+        $DB->expects($this->exactly(3))
             ->method('delete_records')
-            ->with($this->equalTo('tool_usertours_tours'), $this->equalTo(['id' => $id]))
+            ->withConsecutive(
+                [$this->equalTo('tool_usertours_tours'), $this->equalTo(['id' => $id])],
+                [$this->equalTo('user_preferences'), $this->equalTo(['name' => tour::TOUR_LAST_COMPLETED_BY_USER . $id])],
+                [$this->equalTo('user_preferences'), $this->equalTo(['name' => tour::TOUR_REQUESTED_BY_USER . $id])]
+            )
             ->willReturn(null)
             ;
 
index 2f2cce4..ead833e 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2021052500;            // The current module version (Date: YYYYMMDDXX).
+$plugin->version   = 2021052501;            // The current module version (Date: YYYYMMDDXX).
 $plugin->requires  = 2021052500;            // Requires this Moodle version.
 $plugin->component = 'tool_usertours';      // Full name of the plugin (used for diagnostics).
index e56c325..c7b77cc 100644 (file)
@@ -61,7 +61,7 @@ foreach ($allfunctions as $f) {
     }
 }
 
-// whitelisting security
+// Allow only functions available for testing.
 if (!isset($functions[$function])) {
     $function = '';
 }
@@ -81,7 +81,9 @@ foreach ($active_protocols as $p) {
     }
     $protocols[$p] = get_string('pluginname', 'webservice_'.$p);
 }
-if (!isset($protocols[$protocol])) { // whitelisting security
+
+// Allow only protocols supporting the test client.
+if (!isset($protocols[$protocol])) {
     $protocol = '';
 }
 
index 782f033..52e7227 100644 (file)
@@ -147,12 +147,12 @@ class auth_email_external extends external_api {
                         array(
                             'id' => new external_value(PARAM_INT, 'Profile field id', VALUE_OPTIONAL),
                             'shortname' => new external_value(PARAM_ALPHANUMEXT, 'Profile field shortname', VALUE_OPTIONAL),
-                            'name' => new external_value(PARAM_TEXT, 'Profield field name', VALUE_OPTIONAL),
+                            'name' => new external_value(PARAM_RAW, 'Profield field name', VALUE_OPTIONAL),
                             'datatype' => new external_value(PARAM_ALPHANUMEXT, 'Profield field datatype', VALUE_OPTIONAL),
                             'description' => new external_value(PARAM_RAW, 'Profield field description', VALUE_OPTIONAL),
                             'descriptionformat' => new external_format_value('description'),
                             'categoryid' => new external_value(PARAM_INT, 'Profield field category id', VALUE_OPTIONAL),
-                            'categoryname' => new external_value(PARAM_TEXT, 'Profield field category name', VALUE_OPTIONAL),
+                            'categoryname' => new external_value(PARAM_RAW, 'Profield field category name', VALUE_OPTIONAL),
                             'sortorder' => new external_value(PARAM_INT, 'Profield field sort order', VALUE_OPTIONAL),
                             'required' => new external_value(PARAM_INT, 'Profield field required', VALUE_OPTIONAL),
                             'locked' => new external_value(PARAM_INT, 'Profield field locked', VALUE_OPTIONAL),
index 0220640..f62f95c 100644 (file)
@@ -93,6 +93,51 @@ class auth_email_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('textarea', $namedarray['sometext']['datatype']);
     }
 
+    /**
+     * Test get_signup_settings with mathjax in a profile field.
+     */
+    public function test_get_signup_settings_with_mathjax_in_profile_fields() {
+        global $CFG, $DB;
+
+        require_once($CFG->dirroot . '/lib/externallib.php');
+
+        // Enable MathJax filter in content and headings.
+        $this->configure_filters([
+            ['name' => 'mathjaxloader', 'state' => TEXTFILTER_ON, 'move' => -1, 'applytostrings' => true],
+        ]);
+
+        // Create category with MathJax and a new field with MathJax.
+        $categoryname = 'Cat $$(a+b)=2$$';
+        $fieldname = 'Some text $$(a+b)=2$$';
+        $categoryid = $DB->insert_record('user_info_category', array('name' => $categoryname, 'sortorder' => 1));
+        $field3 = $DB->insert_record('user_info_field', array(
+                'shortname' => 'mathjaxname', 'name' => $fieldname, 'categoryid' => $categoryid,
+                'datatype' => 'textarea', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 2));
+
+        $result = auth_email_external::get_signup_settings();
+        $result = external_api::clean_returnvalue(auth_email_external::get_signup_settings_returns(), $result);
+
+        // Format the original data.
+        $sitecontext = context_system::instance();
+        $categoryname = external_format_string($categoryname, $sitecontext->id);
+        $fieldname = external_format_string($fieldname, $sitecontext->id);
+
+        // Whip up a array with named entries to easily check against.
+        $namedarray = array();
+        foreach ($result['profilefields'] as $key => $value) {
+            $namedarray[$value['shortname']] = $value;
+        }
+
+        // Check the new profile field.
+        $this->assertArrayHasKey('mathjaxname', $namedarray);
+        $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">',
+                $namedarray['mathjaxname']['categoryname']);
+        $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">',
+                $namedarray['mathjaxname']['name']);
+        $this->assertEquals($categoryname, $namedarray['mathjaxname']['categoryname']);
+        $this->assertEquals($fieldname, $namedarray['mathjaxname']['name']);
+    }
+
     public function test_signup_user() {
         global $DB;
 
index 7e9e325..2b28e74 100644 (file)
@@ -1881,6 +1881,11 @@ class restore_course_structure_step extends restore_structure_step {
             $data->idnumber = '';
         }
 
+        // If we restore a course from this site, let's capture the original course id.
+        if ($isnewcourse && $this->get_task()->is_samesite()) {
+            $data->originalcourseid = $this->get_task()->get_old_courseid();
+        }
+
         // Any empty value for course->hiddensections will lead to 0 (default, show collapsed).
         // It has been reported that some old 1.9 courses may have it null leading to DB error. MDL-31532
         if (empty($data->hiddensections)) {
index d5b44ce..4a1f187 100644 (file)
@@ -720,7 +720,7 @@ class core_backup_renderer extends plugin_renderer_base {
     public function render_restore_course_search(restore_course_search $component) {
         $url = $component->get_url();
 
-        $output = html_writer::start_tag('div', array('class' => 'restore-course-search form-inline mb-1'));
+        $output = html_writer::start_tag('div', array('class' => 'restore-course-search mb-1'));
         $output .= html_writer::start_tag('div', array('class' => 'rcs-results table-sm w-75'));
 
         $table = new html_table();
@@ -759,22 +759,14 @@ class core_backup_renderer extends plugin_renderer_base {
         $output .= html_writer::table($table);
         $output .= html_writer::end_tag('div');
 
-        $output .= html_writer::start_tag('div', array('class' => 'rcs-search'));
-        $attrs = array(
-            'type' => 'text',
-            'name' => restore_course_search::$VAR_SEARCH,
-            'value' => $component->get_search(),
-            'class' => 'form-control'
-        );
-        $output .= html_writer::empty_tag('input', $attrs);
-        $attrs = array(
-            'type' => 'submit',
-            'name' => 'searchcourses',
-            'value' => get_string('search'),
-            'class' => 'btn btn-secondary'
-        );
-        $output .= html_writer::empty_tag('input', $attrs);
-        $output .= html_writer::end_tag('div');
+        $data = [
+            'inform' => true,
+            'extraclasses' => 'rcs-search mb-3 w-25',
+            'inputname' => restore_course_search::$VAR_SEARCH,
+            'searchstring' => get_string('searchcourses'),
+            'query' => $component->get_search(),
+        ];
+        $output .= $this->output->render_from_template('core/search_input', $data);
 
         $output .= html_writer::end_tag('div');
         return $output;
@@ -880,7 +872,7 @@ class core_backup_renderer extends plugin_renderer_base {
     public function render_restore_category_search(restore_category_search $component) {
         $url = $component->get_url();
 
-        $output = html_writer::start_tag('div', array('class' => 'restore-course-search form-inline mb-1'));
+        $output = html_writer::start_tag('div', array('class' => 'restore-course-search mb-1'));
         $output .= html_writer::start_tag('div', array('class' => 'rcs-results table-sm w-75'));
 
         $table = new html_table();
@@ -922,22 +914,14 @@ class core_backup_renderer extends plugin_renderer_base {
         $output .= html_writer::table($table);
         $output .= html_writer::end_tag('div');
 
-        $output .= html_writer::start_tag('div', array('class' => 'rcs-search'));
-        $attrs = array(
-            'type' => 'text',
-            'name' => restore_category_search::$VAR_SEARCH,
-            'value' => $component->get_search(),
-            'class' => 'form-control'
-        );
-        $output .= html_writer::empty_tag('input', $attrs);
-        $attrs = array(
-            'type' => 'submit',
-            'name' => 'searchcourses',
-            'value' => get_string('search'),
-            'class' => 'btn btn-secondary'
-        );
-        $output .= html_writer::empty_tag('input', $attrs);
-        $output .= html_writer::end_tag('div');
+        $data = [
+            'inform' => true,
+            'extraclasses' => 'rcs-search mb-3 w-25',
+            'inputname' => restore_category_search::$VAR_SEARCH,
+            'searchstring' => get_string('search'),
+            'query' => $component->get_search(),
+        ];
+        $output .= $this->output->render_from_template('core/search_input', $data);
 
         $output .= html_writer::end_tag('div');
         return $output;
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 7bc53b1..3c4d9c9 100644 (file)
@@ -50,7 +50,7 @@ class block_blog_menu extends block_base {
     }
 
     function get_content() {
-        global $CFG;
+        global $CFG, $OUTPUT;
 
         // detect if blog enabled
         if ($this->content !== NULL) {
@@ -98,15 +98,14 @@ class block_blog_menu extends block_base {
 
         // Prepare the footer for this block
         if (has_capability('moodle/blog:search', context_system::instance())) {
-            // Full-text search field
-            $form  = html_writer::tag('label', get_string('search', 'admin'), array('for' => 'blogsearchquery',
-                'class' => 'accesshide'));
-            $form .= html_writer::empty_tag('input', array('id' => 'blogsearchquery', 'class' => 'form-control mr-1',
-                'type' => 'text', 'name' => 'search'));
-            $form .= html_writer::empty_tag('input', array('type' => 'submit', 'class' => 'btn btn-secondary',
-                'value' => get_string('search')));
-            $this->content->footer = html_writer::tag('form', html_writer::tag('div', $form), array(
-                'class' => 'blogsearchform form-inline', 'method' => 'get', 'action' => new moodle_url('/blog/index.php')));
+
+            $data = [
+                'action' => new moodle_url('/blog/index.php'),
+                'inputname' => 'search',
+                'searchstring' => get_string('search', 'admin'),
+                'extraclasses' => 'mt-3'
+            ];
+            $this->content->footer = $OUTPUT->render_from_template('core/search_input', $data);
         } else {
             // No footer to display
             $this->content->footer = '';
index 9a342d2..586e00c 100644 (file)
@@ -206,7 +206,7 @@ Feature: Enable Block blog menu in an activity
     When I log in as "teacher1"
     And I am on "Course 1" course homepage
     And I follow "Test assignment 1"
-    And I set the field "blogsearchquery" to "First"
+    And I set the field "Search" to "First"
     And I press "Search"
     Then I should see "S1 First Blog"
     And I should see "S2 First Blog"
index 39821d2..11d9625 100644 (file)
@@ -183,7 +183,7 @@ Feature: Students can use block blog menu in a course
     And I log out
     When I log in as "teacher1"
     And I am on "Course 1" course homepage
-    And I set the field "blogsearchquery" to "First"
+    And I set the field "Search" to "First"
     And I press "Search"
     Then I should see "S1 First Blog"
     And I should see "S2 First Blog"
index e9f09ac..cc08180 100644 (file)
@@ -59,7 +59,7 @@ class core_block_external extends external_api {
                 'visible'       => new external_value(PARAM_BOOL, 'Whether the block is visible.', VALUE_OPTIONAL),
                 'contents'      => new external_single_structure(
                     array(
-                        'title'         => new external_value(PARAM_TEXT, 'Block title.'),
+                        'title'         => new external_value(PARAM_RAW, 'Block title.'),
                         'content'       => new external_value(PARAM_RAW, 'Block contents.'),
                         'contentformat' => new external_format_value('content'),
                         'footer'        => new external_value(PARAM_RAW, 'Block footer.'),
index d3d96b2..91892ae 100644 (file)
@@ -64,32 +64,17 @@ class block_globalsearch extends block_base {
             return $this->content;
         }
 
-        $url = new moodle_url('/search/index.php');
-        $this->content->footer .= html_writer::link($url, get_string('advancedsearch', 'search'));
+        $data = [
+            'action' => new moodle_url('/search/index.php'),
+            'inputname' => 'q',
+            'searchstring' => get_string('search'),
+        ];
 
-        $this->content->text  = html_writer::start_tag('div', array('class' => 'searchform'));
-        $this->content->text .= html_writer::start_tag('form', array('action' => $url->out()));
-        $this->content->text .= html_writer::start_tag('fieldset', array('action' => 'invisiblefieldset'));
-
-        // Input.
-        $this->content->text .= html_writer::tag('label', get_string('search', 'search'),
-            array('for' => 'searchform_search', 'class' => 'accesshide'));
-        $inputoptions = array('id' => 'searchform_search', 'name' => 'q', 'class' => 'form-control',
-            'type' => 'text', 'size' => '15');
-        $this->content->text .= html_writer::empty_tag('input', $inputoptions);
-
-        // Context id.
         if ($this->page->context && $this->page->context->contextlevel !== CONTEXT_SYSTEM) {
-            $this->content->text .= html_writer::empty_tag('input', ['type' => 'hidden',
-                    'name' => 'context', 'value' => $this->page->context->id]);
+            $data['hiddenfields'] = (object) ['name' => 'context', 'value' => $this->page->context->id];
         }
 
-        // Search button.
-        $this->content->text .= html_writer::tag('button', get_string('search', 'search'),
-            array('id' => 'searchform_button', 'type' => 'submit', 'title' => 'globalsearch', 'class' => 'btn btn-secondary'));
-        $this->content->text .= html_writer::end_tag('fieldset');
-        $this->content->text .= html_writer::end_tag('form');
-        $this->content->text .= html_writer::end_tag('div');
+        $this->content->text = $OUTPUT->render_from_template('core/search_input', $data);
 
         return $this->content;
     }
index 8213abe..76b41a0 100644 (file)
@@ -44,8 +44,6 @@ class search_form implements renderable, templatable {
     protected $courseid;
     /** @var moodle_url The form action URL. */
     protected $actionurl;
-    /** @var moodle_url The advanced search URL. */
-    protected $advancedsearchurl;
     /** @var help_icon The help icon. */
     protected $helpicon;
 
@@ -56,17 +54,17 @@ class search_form implements renderable, templatable {
      */
     public function __construct($courseid) {
         $this->courseid = $courseid;
-        $this->actionurl = new moodle_url('/mod/forum/search.php');
-        $this->advancedsearchurl = new moodle_url('/mod/forum/search.php', ['id' => $this->courseid]);
+        $this->actionurl = new moodle_url('/mod/forum/search.php', ['id' => $courseid]);
         $this->helpicon = new help_icon('search', 'core');
     }
 
     public function export_for_template(renderer_base $output) {
         $data = [
-            'actionurl' => $this->actionurl->out(false),
-            'courseid' => $this->courseid,
-            'advancedsearchurl' => $this->advancedsearchurl->out(false),
+            'action' => $this->actionurl,
             'helpicon' => $this->helpicon->export_for_template($output),
+            'hiddenfields' => (object) ['name' => 'id', 'value' => $this->courseid],
+            'inputname' => 'search',
+            'searchstring' => get_string('search')
         ];
         return $data;
     }
index b5f8133..320a546 100644 (file)
 
     Example context (json):
     {
-        "actionurl": "https://domain.example/mod/forum/search.php",
-        "courseid": "2",
-        "advancedsearchurl": "https://domain.example/mod/forum/search.php?id=2",
+        "action": "https://moodle.local/admin/search.php",
+        "inputname": "search",
+        "searchstring": "Search settings",
+        "value": "policy",
+        "hiddenfields": [
+            {
+                "name": "course",
+                "value": "11"
+            }
+        ],
         "helpicon": "<a class='btn'><i class='icon fa fa-question-circle'></i></a>"
     }
 }}
 <div class="searchform">
-    <form action="{{actionurl}}" class="form-inline">
-        <input type="hidden" name="id" value="{{courseid}}">
-        <div class="input-group w-100">
-            <label class="sr-only" for="searchform_search">{{#str}}search{{/str}}</label>
-            <input id="searchform_search" name="search" type="text" class="form-control" size="10">
-            <div class="input-group-append">
-                <button class="btn btn-secondary" id="searchform_button" type="submit">{{#str}}go{{/str}}</button>
-            </div>
-        </div>
-    </form>
+    {{>core/search_input}}
     <div class="mt-3">
-        <a href="{{advancedsearchurl}}">{{#str}}advancedsearch, block_search_forums{{/str}}</a>
+        <a href="{{action}}">{{#str}}advancedsearch, block_search_forums{{/str}}</a>
         {{#helpicon}}
             {{>core/help_icon}}
         {{/helpicon}}
index dfdeb6f..9444846 100644 (file)
@@ -29,9 +29,8 @@ Feature: The search forums block allows users to search for forum posts on cours
   Scenario: Use the search forum block in a course without any forum posts
     Given I log in as "student1"
     And I am on "Course 1" course homepage
-    When I set the following fields to these values:
-      | searchform_search | Moodle |
-    And I press "Go"
+    When I set the field "Search" to "Moodle"
+    And I press "Search"
     Then I should see "No posts"
 
   Scenario: Use the search forum block in a course with a hidden forum and search for posts
@@ -50,9 +49,8 @@ Feature: The search forums block allows users to search for forum posts on cours
     When I log in as "student1"
     And I am on "Course 1" course homepage
     And "Search forums" "block" should exist
-    And I set the following fields to these values:
-      | searchform_search | message |
-    And I press "Go"
+    When I set the field "Search" to "message"
+    And I press "Search"
     Then I should see "No posts"
 
   Scenario: Use the search forum block in a course and search for posts
@@ -65,7 +63,6 @@ Feature: The search forums block allows users to search for forum posts on cours
     When I log in as "student1"
     And I am on "Course 1" course homepage
     And "Search forums" "block" should exist
-    And I set the following fields to these values:
-      | searchform_search | message |
-    And I press "Go"
+    When I set the field "Search" to "message"
+    And I press "Search"
     Then I should see "My subject"
index 53f3ba9..31d8683 100644 (file)
@@ -17,15 +17,13 @@ Feature: The search forums block allows users to search for forum posts on front
   Scenario: Use the search forum block on the frontpage and search for posts as a user
     Given I log in as "student1"
     And I am on site homepage
-    When I set the following fields to these values:
-      | searchform_search | Moodle |
-    And I press "Go"
+    When I set the field "Search" to "Moodle"
+    And I press "Search"
     Then I should see "No posts"
 
   Scenario: Use the search forum block on the frontpage and search for posts as a guest
     Given I log in as "guest"
     And I am on site homepage
-    When I set the following fields to these values:
-      | searchform_search | Moodle |
-    And I press "Go"
+    When I set the field "Search" to "Moodle"
+    And I press "Search"
     Then I should see "No posts"
index bceefd8..3e92848 100644 (file)
@@ -144,11 +144,12 @@ class block_settings_renderer extends plugin_renderer_base {
 
     public function search_form(moodle_url $formtarget, $searchvalue) {
         $data = [
-                'action' => $formtarget->out(false),
-                'label' => get_string('searchinsettings', 'admin'),
-                'searchvalue' => $searchvalue
+            'action' => $formtarget,
+            'inputname' => 'query',
+            'searchstring' => get_string('searchinsettings', 'admin'),
+            'query' => $searchvalue
         ];
-        return $this->render_from_template('block_settings/search_form', $data);
+        return $this->render_from_template('core/search_input', $data);
     }
 
 }
index ee1b61b..f5bb942 100644 (file)
@@ -235,6 +235,85 @@ class core_block_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(5, $configcounts);
     }
 
+    /**
+     * Test get_course_blocks contents with mathjax.
+     */
+    public function test_get_course_blocks_contents_with_mathjax() {
+        global $DB, $CFG;
+
+        require_once($CFG->dirroot . '/lib/externallib.php');
+
+        $this->resetAfterTest(true);
+
+        // Enable MathJax filter in content and headings.
+        $this->configure_filters([
+            ['name' => 'mathjaxloader', 'state' => TEXTFILTER_ON, 'move' => -1, 'applytostrings' => true],
+        ]);
+
+        // Create a few stuff to test with.
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id);
+        $coursecontext = context_course::instance($course->id);
+
+        // Create a HTML block.
+        $title = 'My block $$(a+b)=2$$';
+        $body = 'My block contents $$(a+b)=2$$';
+        $bodyformat = FORMAT_MOODLE;
+        $page = new moodle_page();
+        $page->set_context($coursecontext);
+        $page->set_pagelayout('course');
+        $course->format = course_get_format($course)->get_format();
+        $page->set_pagetype('course-view-' . $course->format);
+        $page->blocks->load_blocks();
+        $newblock = 'html';
+        $page->blocks->add_block_at_end_of_default_region($newblock);
+
+        $this->setUser($user);
+        // Re-create the page.
+        $page = new moodle_page();
+        $page->set_context($coursecontext);
+        $page->set_pagelayout('course');
+        $course->format = course_get_format($course)->get_format();
+        $page->set_pagetype('course-view-' . $course->format);
+        $page->blocks->load_blocks();
+        $blocks = $page->blocks->get_blocks_for_region($page->blocks->get_default_region());
+        $block = end($blocks);
+        $block = block_instance('html', $block->instance);
+        $nonscalar = [
+            'something' => true,
+        ];
+        $configdata = (object) [
+            'title' => $title,
+            'text' => [
+                'itemid' => 0,
+                'text' => $body,
+                'format' => $bodyformat,
+            ],
+            'nonscalar' => $nonscalar
+        ];
+        $block->instance_config_save((object) $configdata);
+
+        // Check for the new block.
+        $result = core_block_external::get_course_blocks($course->id, true);
+        $result = external_api::clean_returnvalue(core_block_external::get_course_blocks_returns(), $result);
+
+        // Format the original data.
+        $sitecontext = context_system::instance();
+        $title = external_format_string($title, $coursecontext->id);
+        list($body, $bodyformat) = external_format_text($body, $bodyformat, $coursecontext->id, 'block_html', 'content');
+
+        // Check that the block data is formatted.
+        $this->assertCount(1, $result['blocks']);
+        $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">',
+                $result['blocks'][0]['contents']['title']);
+        $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">',
+                $result['blocks'][0]['contents']['content']);
+        $this->assertEquals($title, $result['blocks'][0]['contents']['title']);
+        $this->assertEquals($body, $result['blocks'][0]['contents']['content']);
+    }
+
     /**
      * Test user get default dashboard blocks.
      */
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 3f186f1..b2c94b5 100644 (file)
@@ -319,6 +319,7 @@ class core_calendar_external extends external_api {
             $event = (array) $eventobj;
             // Description formatting.
             $calendareventobj = new calendar_event($event);
+            $event['name'] = $calendareventobj->format_external_name();
             list($event['description'], $event['format']) = $calendareventobj->format_external_text();
 
             if ($hassystemcap) {
@@ -365,7 +366,7 @@ class core_calendar_external extends external_api {
                 'events' => new external_multiple_structure( new external_single_structure(
                         array(
                             'id' => new external_value(PARAM_INT, 'event id'),
-                            'name' => new external_value(PARAM_TEXT, 'event name'),
+                            'name' => new external_value(PARAM_RAW, 'event name'),
                             'description' => new external_value(PARAM_RAW, 'Description', VALUE_OPTIONAL, null, NULL_ALLOWED),
                             'format' => new external_format_value('description'),
                             'courseid' => new external_value(PARAM_INT, 'course id'),
@@ -745,7 +746,7 @@ class core_calendar_external extends external_api {
                         'events' => new external_multiple_structure( new external_single_structure(
                                 array(
                                     'id' => new external_value(PARAM_INT, 'event id'),
-                                    'name' => new external_value(PARAM_TEXT, 'event name'),
+                                    'name' => new external_value(PARAM_RAW, 'event name'),
                                     'description' => new external_value(PARAM_RAW, 'Description', VALUE_OPTIONAL),
                                     'format' => new external_format_value('description'),
                                     'courseid' => new external_value(PARAM_INT, 'course id'),
index 9755765..3bf8caf 100644 (file)
@@ -968,6 +968,22 @@ class calendar_event {
         }
     }
 
+    /**
+     * Format the event name using the external API.
+     *
+     * This function should we used when text formatting is required in external functions.
+     *
+     * @return string Formatted name.
+     */
+    public function format_external_name() {
+        if ($this->editorcontext === null) {
+            // Switch on the event type to decide upon the appropriate context to use for this event.
+            $this->editorcontext = $this->get_context();
+        }
+
+        return external_format_string($this->properties->name, $this->editorcontext->id);
+    }
+
     /**
      * Format the text using the external API.
      *
@@ -3257,7 +3273,7 @@ function calendar_get_calendar_context($subscription) {
 }
 
 /**
- * Implements callback user_preferences, whitelists preferences that users are allowed to update directly
+ * Implements callback user_preferences, lists preferences that users are allowed to update directly
  *
  * Used in {@see core_user::fill_preferences_cache()}, see also {@see useredit_update_user_preference()}
  *
@@ -3667,11 +3683,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 +3920,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 fc5ea7e..1d41b31 100644 (file)
@@ -542,6 +542,42 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals($category2->id, $events['events'][1]['categoryid']);
     }
 
+    /**
+     * Test get_calendar_events with mathjax in the name.
+     */
+    public function test_get_calendar_events_with_mathjax() {
+        global $USER;
+
+        $this->resetAfterTest(true);
+        set_config('calendar_adminseesall', 1);
+        $this->setAdminUser();
+
+        // Enable MathJax filter in content and headings.
+        $this->configure_filters([
+            ['name' => 'mathjaxloader', 'state' => TEXTFILTER_ON, 'move' => -1, 'applytostrings' => true],
+        ]);
+
+        // Create a site event with mathjax in the name and description.
+        $siteevent = $this->create_calendar_event('Site Event $$(a+b)=2$$', $USER->id, 'site', 0, time(),
+                ['description' => 'Site Event Description $$(a+b)=2$$']);
+
+        // Now call the WebService.
+        $events = core_calendar_external::get_calendar_events();
+        $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
+
+        // Format the original data.
+        $sitecontext = context_system::instance();
+        $siteevent->name = $siteevent->format_external_name();
+        list($siteevent->description, $siteevent->descriptionformat) = $siteevent->format_external_text();
+
+        // Check that the event data is formatted.
+        $this->assertCount(1, $events['events']);
+        $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">', $events['events'][0]['name']);
+        $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">', $events['events'][0]['description']);
+        $this->assertEquals($siteevent->name, $events['events'][0]['name']);
+        $this->assertEquals($siteevent->description, $events['events'][0]['description']);
+    }
+
     /**
      * Test core_calendar_external::create_calendar_events
      */
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 0341bf9..02d1434 100644 (file)
@@ -103,19 +103,21 @@ if ($editcontrols = cohort_edit_controls($context, $baseurl)) {
 }
 
 // Add search form.
-$search  = html_writer::start_tag('form', array('id'=>'searchcohortquery', 'method'=>'get', 'class' => 'form-inline search-cohort'));
-$search .= html_writer::start_div('mb-1');
-$search .= html_writer::label(get_string('searchcohort', 'cohort'), 'cohort_search_q', true,
-        array('class' => 'mr-1')); // No : in form labels!
-$search .= html_writer::empty_tag('input', array('id' => 'cohort_search_q', 'type' => 'text', 'name' => 'search',
-        'value' => $searchquery, 'class' => 'form-control mr-1'));
-$search .= html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('search', 'cohort'),
-        'class' => 'btn btn-secondary'));
-$search .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'contextid', 'value'=>$contextid));
-$search .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'showall', 'value'=>$showall));
-$search .= html_writer::end_div();
-$search .= html_writer::end_tag('form');
-echo $search;
+$hiddenfields = [
+    (object) ['name' => 'contextid', 'value' => $contextid],
+    (object) ['name' => 'showall', 'value' => $showall]
+];
+
+$data = [
+    'action' => new moodle_url('/cohort/index.php'),
+    'inputname' => 'search',
+    'searchstring' => get_string('search', 'cohort'),
+    'query' => $searchquery,
+    'hiddenfields' => $hiddenfields,
+    'extraclasses' => 'mb-3'
+];
+
+echo $OUTPUT->render_from_template('core/search_input', $data);
 
 // Output pagination bar.
 echo $OUTPUT->paging_bar($cohorts['totalcohorts'], $page, 25, $baseurl);
index be279e7..9f0c2a1 100644 (file)
@@ -55,7 +55,7 @@ class competency_framework_exporter extends \core\external\persistent_exporter {
         $context = $this->persistent->get_context();
         $competenciescount = 0;
         try {
-            api::count_competencies($filters);
+            $competenciescount = api::count_competencies($filters);
         } catch (\required_capability_exception $re) {
             $competenciescount = 0;
         }
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..912b92e 100644 (file)
@@ -709,6 +709,20 @@ $CFG->admin = 'admin';
 //
 //      $CFG->forumpostcountchunksize = 5000;
 //
+// Course and category sorting
+//
+// If the number of courses in a category exceeds $CFG->maxcoursesincategory (10000 by default), it may lead to duplicate
+// sort orders of courses in separated categories. For example:
+// - Category A has the sort order of 10000, and has 10000 courses. The last course will have the sort order of 20000.
+// - Category B has the sort order of 20000, and has a course with the sort order of 20001.
+// - If we add another course in category A, it will have a sort order of 20001,
+// which is the same as the course in category B
+// The duplicate will cause sorting issue and hence we need to increase $CFG->maxcoursesincategory
+// to fix the duplicate sort order
+// Please also make sure $CFG->maxcoursesincategory * MAX_COURSE_CATEGORIES less than max integer.
+//
+// $CFG->maxcoursesincategory = 10000;
+//
 //=========================================================================
 // 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
 //=========================================================================
@@ -1057,6 +1071,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 16c80b0..8309c24 100644 (file)
Binary files a/contentbank/amd/build/search.min.js and b/contentbank/amd/build/search.min.js differ
index 0e06258..ae3bad7 100644 (file)
Binary files a/contentbank/amd/build/search.min.js.map and b/contentbank/amd/build/search.min.js.map differ
index 26786ec..5aba987 100644 (file)
Binary files a/contentbank/amd/build/selectors.min.js and b/contentbank/amd/build/selectors.min.js differ
index 2efa6a5..62be444 100644 (file)
Binary files a/contentbank/amd/build/selectors.min.js.map and b/contentbank/amd/build/selectors.min.js.map differ
index e604abc..40a99a5 100644 (file)
@@ -80,8 +80,7 @@ const registerListenerEvents = (root) => {
  * @param {String} searchQuery The search query.
  */
 const toggleSearchResultsView = async(body, searchQuery) => {
-    const clearSearchButton = body.find(selectors.elements.clearsearch)[0];
-    const searchIcon = body.find(selectors.elements.searchicon)[0];
+    const clearSearchButton = body.find(selectors.actions.clearSearch)[0];
 
     const navbarBreadcrumb = body.find(selectors.elements.cbnavbarbreadcrumb)[0];
     const navbarTotal = body.find(selectors.elements.cbnavbartotalsearch)[0];
@@ -91,7 +90,6 @@ const toggleSearchResultsView = async(body, searchQuery) => {
         // As the search query is present, search results should be displayed.
 
         // Display the "clear" search button in the activity chooser search bar.
-        searchIcon.classList.add('d-none');
         clearSearchButton.classList.remove('d-none');
 
         // Change the cb-navbar to display total items found.
@@ -103,7 +101,6 @@ const toggleSearchResultsView = async(body, searchQuery) => {
 
         // Hide the "clear" search button in the activity chooser search bar.
         clearSearchButton.classList.add('d-none');
-        searchIcon.classList.remove('d-none');
 
         // Display again the breadcrumb in the navbar.
         navbarBreadcrumb.classList.remove('d-none');
index 60fe955..f6de1b2 100644 (file)
@@ -42,7 +42,7 @@ export default {
     },
     actions: {
         search: getDataSelector('action', 'searchcontent'),
-        clearSearch: getDataSelector('action', 'clearsearchcontent'),
+        clearSearch: getDataSelector('action', 'clearsearch'),
         viewgrid: getDataSelector('action', 'viewgrid'),
         viewlist: getDataSelector('action', 'viewlist'),
         sortname: getDataSelector('action', 'sortname'),
@@ -55,8 +55,6 @@ export default {
         listitem: '.cb-listitem',
         cbnavbarbreadcrumb: '.cb-navbar-breadbrumb',
         cbnavbartotalsearch: '.cb-navbar-totalsearch',
-        clearsearch: '.input-group-append .clear-icon',
-        searchicon: '.input-group-append .search-icon',
         searchinput: '#searchinput',
         sortbutton: '.cb-btnsort'
     },
index 1e5c934..9152067 100644 (file)
@@ -36,6 +36,10 @@ use context;
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class contentbank {
+
+    /** @var array All the context levels allowed in the content bank */
+    private const ALLOWED_CONTEXT_LEVELS = [CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE];
+
     /** @var array Enabled content types. */
     private $enabledcontenttypes = null;
 
@@ -348,4 +352,14 @@ class contentbank {
         $contentclass = "\\$record->contenttype\\content";
         return new $contentclass($record);
     }
+
+    /**
+     * Whether the context is allowed.
+     *
+     * @param context $context Context to check.
+     * @return bool
+     */
+    public function is_context_allowed(context $context): bool {
+        return in_array($context->contextlevel, self::ALLOWED_CONTEXT_LEVELS);
+    }
 }
index cdddcd4..6d0c58d 100644 (file)
@@ -30,6 +30,12 @@ $contextid = required_param('contextid', PARAM_INT);
 $pluginname = required_param('plugin', PARAM_PLUGIN);
 $id = optional_param('id', null, PARAM_INT);
 $context = context::instance_by_id($contextid, MUST_EXIST);
+
+$cb = new \core_contentbank\contentbank();
+if (!$cb->is_context_allowed($context)) {
+    print_error('contextnotallowed', 'core_contentbank');
+}
+
 require_capability('moodle/contentbank:access', $context);
 
 $returnurl = new \moodle_url('/contentbank/view.php', ['id' => $id]);
index 33eff29..c4d2242 100644 (file)
@@ -30,6 +30,11 @@ $contextid    = optional_param('contextid', \context_system::instance()->id, PAR
 $search = optional_param('search', '', PARAM_CLEAN);
 $context = context::instance_by_id($contextid, MUST_EXIST);
 
+$cb = new \core_contentbank\contentbank();
+if (!$cb->is_context_allowed($context)) {
+    print_error('contextnotallowed', 'core_contentbank');
+}
+
 require_capability('moodle/contentbank:access', $context);
 
 $statusmsg = optional_param('statusmsg', '', PARAM_ALPHANUMEXT);
@@ -47,7 +52,6 @@ $PAGE->set_heading($title);
 $PAGE->set_pagetype('contentbank');
 
 // Get all contents managed by active plugins where the user has permission to render them.
-$cb = new \core_contentbank\contentbank();
 $contenttypes = [];
 $enabledcontenttypes = $cb->get_enabled_content_types();
 foreach ($enabledcontenttypes as $contenttypename) {
index 8d02863..6fc2e58 100644 (file)
     @template core_contentbank/bankcontent/search
 
     Example context (json):
-    {}
+    {
+    }
 
 }}
-<div class="searchbar input-group" role="search">
-    <label for="searchinput">
-        <span class="sr-only">{{#str}} searchcontentbankbyname, contentbank {{/str}}</span>
-    </label>
-    <input type="text"
-           id="searchinput"
-           class="form-control searchinput border-right-0"
-           placeholder="{{#str}} search, core {{/str}}"
-           name="search"
-           autocomplete="off"
-    >
-    <div class="input-group-append">
-        <div class="input-group-text bg-transparent">
-            <div class="search-icon">
-                <button class="btn p-0 align-baseline icon-no-margin" data-action="searchcontent"
-                    aria-label="{{#str}} search, core {{/str}}">
-                    <span class="d-flex" aria-hidden="true">{{#pix}} a/search, core {{/pix}}</span>
-                </button>
-            </div>
-            <div class="clear-icon d-none">
-                <button class="btn p-0 align-baseline icon-no-margin" data-action="clearsearchcontent"
-                    aria-label="{{#str}} clearsearch, core {{/str}}">
-                    <span class="d-flex" aria-hidden="true">{{#pix}} e/cancel_solid_circle, core {{/pix}}</span>
-                </button>
-            </div>
-        </div>
-    </div>
-</div>
\ No newline at end of file
+
+{{< core/search_input_auto }}
+    {{$label}}{{#str}}
+        searchcontentbankbyname, contentbank
+    {{/str}}{{/label}}
+    {{$placeholder}}{{#str}}
+        search, core
+    {{/str}}{{/placeholder}}
+{{/ core/search_input_auto }}
index 3d6a703..9ca729a 100644 (file)
@@ -631,4 +631,76 @@ class core_contentbank_testcase extends advanced_testcase {
         $this->expectException(Exception::class);
         $cb->get_content_from_id(0);
     }
+
+    /**
+     * Test the behaviour of is_context_allowed().
+     *
+     * @dataProvider context_provider
+     * @param  \Closure $getcontext Get the context to check.
+     * @param  bool $expectedresult Expected result.
+     *
+     * @covers ::is_context_allowed
+     */
+    public function test_is_context_allowed(\Closure $getcontext, bool $expectedresult): void {
+        $this->resetAfterTest();
+
+        $cb = new contentbank();
+        $context = $getcontext();
+        $this->assertEquals($expectedresult, $cb->is_context_allowed($context));
+    }
+
+    /**
+     * Data provider for test_is_context_allowed().
+     *
+     * @return array
+     */
+    public function context_provider(): array {
+
+        return [
+            'System context' => [
+                function (): \context {
+                    return \context_system::instance();
+                },
+                true,
+            ],
+            'User context' => [
+                function (): \context {
+                    $user = $this->getDataGenerator()->create_user();
+                    return \context_user::instance($user->id);
+                },
+                false,
+            ],
+            'Course category context' => [
+                function (): \context {
+                    $coursecat = $this->getDataGenerator()->create_category();
+                    return \context_coursecat::instance($coursecat->id);
+                },
+                true,
+            ],
+            'Course context' => [
+                function (): \context {
+                    $course = $this->getDataGenerator()->create_course();
+                    return \context_course::instance($course->id);
+                },
+                true,
+            ],
+            'Module context' => [
+                function (): \context {
+                    $course = $this->getDataGenerator()->create_course();
+                    $module = $this->getDataGenerator()->create_module('page', ['course' => $course->id]);
+                    return \context_module::instance($module->cmid);
+                },
+                false,
+            ],
+            'Block context' => [
+                function (): \context {
+                    $course = $this->getDataGenerator()->create_course();
+                    $coursecontext = context_course::instance($course->id);
+                    $block = $this->getDataGenerator()->create_block('online_users', ['parentcontextid' => $coursecontext->id]);
+                    return \context_block::instance($block->id);
+                },
+                false,
+            ],
+        ];
+    }
 }
index 4410de4..81a7870 100644 (file)
@@ -32,9 +32,12 @@ require_login();
 $contextid = optional_param('contextid', \context_system::instance()->id, PARAM_INT);
 $context = context::instance_by_id($contextid, MUST_EXIST);
 
-require_capability('moodle/contentbank:upload', $context);
-
 $cb = new \core_contentbank\contentbank();
+if (!$cb->is_context_allowed($context)) {
+    print_error('contextnotallowed', 'core_contentbank');
+}
+
+require_capability('moodle/contentbank:upload', $context);
 
 $id = optional_param('id', null, PARAM_INT);
 if ($id) {
index dbb33f4..d517d5b 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js and b/course/amd/build/local/activitychooser/dialogue.min.js differ
index 5c2bb8f..740fe3f 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js.map and b/course/amd/build/local/activitychooser/dialogue.min.js.map differ
index 0b4a6ed..45f8f4f 100644 (file)
Binary files a/course/amd/build/local/activitychooser/selectors.min.js and b/course/amd/build/local/activitychooser/selectors.min.js differ
index 177b53e..13f9e9f 100644 (file)
Binary files a/course/amd/build/local/activitychooser/selectors.min.js.map and b/course/amd/build/local/activitychooser/selectors.min.js.map differ
index cd045a3..a3bde76 100644 (file)
@@ -466,8 +466,7 @@ const toggleSearchResultsView = async(modal, mappedModules, searchQuery) => {
     const modalBody = modal.getBody()[0];
     const searchResultsContainer = modalBody.querySelector(selectors.regions.searchResults);
     const chooserContainer = modalBody.querySelector(selectors.regions.chooser);
-    const clearSearchButton = modalBody.querySelector(selectors.elements.clearsearch);
-    const searchIcon = modalBody.querySelector(selectors.elements.searchicon);
+    const clearSearchButton = modalBody.querySelector(selectors.actions.clearSearch);
 
     if (searchQuery.length > 0) { // Search query is present.
         const searchResultsData = searchModules(mappedModules, searchQuery);
@@ -481,7 +480,6 @@ const toggleSearchResultsView = async(modal, mappedModules, searchQuery) => {
             initChooserOptionsKeyboardNavigation(modalBody, mappedModules, searchResultItemsContainer, modal);
         }
         // Display the "clear" search button in the activity chooser search bar.
-        searchIcon.classList.add('d-none');
         clearSearchButton.classList.remove('d-none');
         // Hide the default chooser options container.
         chooserContainer.setAttribute('hidden', 'hidden');
@@ -490,7 +488,6 @@ const toggleSearchResultsView = async(modal, mappedModules, searchQuery) => {
     } else { // Search query is not present.
         // Hide the "clear" search button in the activity chooser search bar.
         clearSearchButton.classList.add('d-none');
-        searchIcon.classList.remove('d-none');
         // Hide the search results container.
         searchResultsContainer.setAttribute('hidden', 'hidden');
         // Display the default chooser options container.
index 6b54bbc..dd20e75 100644 (file)
@@ -86,8 +86,6 @@ export default {
         sitetopic: 'div.sitetopic',
         tab: 'a[data-toggle="tab"]',
         activetab: 'a[data-toggle="tab"][aria-selected="true"]',
-        visibletabs: 'a[data-toggle="tab"]:not(.d-none)',
-        searchicon: '.input-group-append .search-icon',
-        clearsearch: '.input-group-append .clear'
+        visibletabs: 'a[data-toggle="tab"]:not(.d-none)'
     },
 };
index abe5d56..187f851 100644 (file)
@@ -2319,7 +2319,8 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
         $context->update_moved($newparent);
 
         // Now make it last in new category.
-        $DB->set_field('course_categories', 'sortorder', MAX_COURSES_IN_CATEGORY * MAX_COURSE_CATEGORIES, ['id' => $this->id]);
+        $DB->set_field('course_categories', 'sortorder',
+            get_max_courses_in_category() * MAX_COURSE_CATEGORIES, ['id' => $this->id]);
 
         if ($hidecat) {
             fix_course_sortorder();
index 25340f8..3de3c7a 100644 (file)
@@ -1291,56 +1291,19 @@ class core_course_management_renderer extends plugin_renderer_base {
      * Renders html to display a course search form
      *
      * @param string $value default value to populate the search field
-     * @param string $format display format - 'plain' (default), 'short' or 'navbar'
      * @return string
      */
-    public function course_search_form($value = '', $format = 'plain') {
-        static $count = 0;
-        $formid = 'coursesearch';
-        if ((++$count) > 1) {
-            $formid .= $count;
-        }
-
-        switch ($format) {
-            case 'navbar' :
-                $formid = 'coursesearchnavbar';
-                $inputid = 'navsearchbox';
-                $inputsize = 20;
-                break;
-            case 'short' :
-                $inputid = 'shortsearchbox';
-                $inputsize = 12;
-                break;
-            default :
-                $inputid = 'coursesearchbox';
-                $inputsize = 30;
-        }
-
-        $strsearchcourses = get_string("searchcourses");
-        $searchurl = new moodle_url('/course/management.php');
-
-        $output = html_writer::start_div('row');
-        $output .= html_writer::start_div('col-md-12');
-        $output .= html_writer::start_tag('form', array('class' => 'card', 'id' => $formid,
-                'action' => $searchurl, 'method' => 'get'));
-        $output .= html_writer::start_tag('fieldset', array('class' => 'coursesearchbox invisiblefieldset'));
-        $output .= html_writer::tag('legend', $this->output->heading($strsearchcourses.': ', 2, 'm-0'),
-                array('class' => 'card-header'));
-        $output .= html_writer::start_div('card-body');
-        $output .= html_writer::start_div('input-group col-sm-6 col-lg-4 m-auto');
-        $output .= html_writer::empty_tag('input', array('class' => 'form-control', 'type' => 'text', 'id' => $inputid,
-                'size' => $inputsize, 'name' => 'search', 'value' => s($value), 'aria-label' => get_string('searchcourses')));
-        $output .= html_writer::start_tag('span', array('class' => 'input-group-btn'));
-        $output .= html_writer::tag('button', get_string('go'), array('class' => 'btn btn-primary', 'type' => 'submit'));
-        $output .= html_writer::end_tag('span');
-        $output .= html_writer::end_div();
-        $output .= html_writer::end_div();
-        $output .= html_writer::end_tag('fieldset');
-        $output .= html_writer::end_tag('form');
-        $output .= html_writer::end_div();
-        $output .= html_writer::end_div();
-
-        return $output;
+    public function course_search_form($value = '') {
+
+        $data = [
+            'action' => new moodle_url('/course/management.php'),
+            'btnclass' => 'btn-primary',
+            'extraclasses' => 'my-3 d-flex justify-content-center',
+            'inputname' => 'search',
+            'searchstring' => get_string('searchcourses'),
+            'value' => $value
+        ];
+        return $this->render_from_template('core/search_input', $data);
     }
 
     /**
index 5a896e7..ac64324 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);
@@ -426,7 +427,7 @@ class core_course_external extends external_api {
             new external_single_structure(
                 array(
                     'id' => new external_value(PARAM_INT, 'Section ID'),
-                    'name' => new external_value(PARAM_TEXT, 'Section name'),
+                    'name' => new external_value(PARAM_RAW, 'Section name'),
                     'visible' => new external_value(PARAM_INT, 'is the section visible', VALUE_OPTIONAL),
                     'summary' => new external_value(PARAM_RAW, 'Section description'),
                     'summaryformat' => new external_format_value('summary'),
@@ -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?',
@@ -669,12 +671,12 @@ class core_course_external extends external_api {
                 new external_single_structure(
                         array(
                             'id' => new external_value(PARAM_INT, 'course id'),
-                            'shortname' => new external_value(PARAM_TEXT, 'course short name'),
+                            'shortname' => new external_value(PARAM_RAW, 'course short name'),
                             'categoryid' => new external_value(PARAM_INT, 'category id'),
                             'categorysortorder' => new external_value(PARAM_INT,
                                     'sort order into the category', VALUE_OPTIONAL),
-                            'fullname' => new external_value(PARAM_TEXT, 'full name'),
-                            'displayname' => new external_value(PARAM_TEXT, 'course display name'),
+                            'fullname' => new external_value(PARAM_RAW, 'full name'),
+                            'displayname' => new external_value(PARAM_RAW, 'course display name'),
                             'idnumber' => new external_value(PARAM_RAW, 'id number', VALUE_OPTIONAL),
                             'summary' => new external_value(PARAM_RAW, 'summary'),
                             'summaryformat' => new external_format_value('summary'),
@@ -729,7 +731,7 @@ class core_course_external extends external_api {
                              ),
                             'customfields' => new external_multiple_structure(
                                 new external_single_structure(
-                                    ['name' => new external_value(PARAM_TEXT, 'The name of the custom field'),
+                                    ['name' => new external_value(PARAM_RAW, 'The name of the custom field'),
                                      'shortname' => new external_value(PARAM_ALPHANUMEXT, 'The shortname of the custom field'),
                                      'type'  => new external_value(PARAM_COMPONENT,
                                          'The type of the custom field - text, checkbox...'),
@@ -939,7 +941,7 @@ class core_course_external extends external_api {
             new external_single_structure(
                 array(
                     'id'       => new external_value(PARAM_INT, 'course id'),
-                    'shortname' => new external_value(PARAM_TEXT, 'short name'),
+                    'shortname' => new external_value(PARAM_RAW, 'short name'),
                 )
             )
         );
@@ -1485,7 +1487,7 @@ class core_course_external extends external_api {
         return new external_single_structure(
             array(
                 'id'       => new external_value(PARAM_INT, 'course id'),
-                'shortname' => new external_value(PARAM_TEXT, 'short name'),
+                'shortname' => new external_value(PARAM_RAW, 'short name'),
             )
         );
     }
@@ -1963,7 +1965,7 @@ class core_course_external extends external_api {
             new external_single_structure(
                 array(
                     'id' => new external_value(PARAM_INT, 'category id'),
-                    'name' => new external_value(PARAM_TEXT, 'category name'),
+                    'name' => new external_value(PARAM_RAW, 'category name'),
                     'idnumber' => new external_value(PARAM_RAW, 'category id number', VALUE_OPTIONAL),
                     'description' => new external_value(PARAM_RAW, 'category description'),
                     'descriptionformat' => new external_format_value('description'),
@@ -2069,7 +2071,7 @@ class core_course_external extends external_api {
             new external_single_structure(
                 array(
                     'id' => new external_value(PARAM_INT, 'new category id'),
-                    'name' => new external_value(PARAM_TEXT, 'new category name'),
+                    'name' => new external_value(PARAM_RAW, 'new category name'),
                 )
             )
         );
@@ -2607,11 +2609,11 @@ class core_course_external extends external_api {
     protected static function get_course_structure($onlypublicdata = true) {
         $coursestructure = array(
             'id' => new external_value(PARAM_INT, 'course id'),
-            'fullname' => new external_value(PARAM_TEXT, 'course full name'),
-            'displayname' => new external_value(PARAM_TEXT, 'course display name'),
-            'shortname' => new external_value(PARAM_TEXT, 'course short name'),
+            'fullname' => new external_value(PARAM_RAW, 'course full name'),
+            'displayname' => new external_value(PARAM_RAW, 'course display name'),
+            'shortname' => new external_value(PARAM_RAW, 'course short name'),
             'categoryid' => new external_value(PARAM_INT, 'category id'),
-            'categoryname' => new external_value(PARAM_TEXT, 'category name'),
+            'categoryname' => new external_value(PARAM_RAW, 'category name'),
             'sortorder' => new external_value(PARAM_INT, 'Sort order in the category', VALUE_OPTIONAL),
             'summary' => new external_value(PARAM_RAW, 'summary'),
             'summaryformat' => new external_format_value('summary'),
@@ -2861,7 +2863,7 @@ class core_course_external extends external_api {
                             new external_single_structure(
                                 array(
                                     'id' => new external_value(PARAM_ALPHANUMEXT, 'Outcome id'),
-                                    'name'  => new external_value(PARAM_TEXT, 'Outcome full name'),
+                                    'name'  => new external_value(PARAM_RAW, 'Outcome full name'),
                                     'scale' => new external_value(PARAM_TEXT, 'Scale items')
                                 )
                             ),
index f8bd189..a834ba7 100644 (file)
@@ -2103,7 +2103,7 @@ function move_courses($courseids, $categoryid) {
         $course->id = $dbcourse->id;
         $course->timemodified = time();
         $course->category  = $category->id;
-        $course->sortorder = $category->sortorder + MAX_COURSES_IN_CATEGORY - $i++;
+        $course->sortorder = $category->sortorder + get_max_courses_in_category() - $i++;
         if ($category->visible == 0) {
             // Hide the course when moving into hidden category, do not update the visibleold flag - we want to get
             // to previous state if somebody unhides the category.
index 39ad5a7..5afc685 100644 (file)
@@ -487,6 +487,9 @@ if (count($notificationsfail) > 0) {
 }
 
 // Start the management form.
+
+echo $renderer->course_search_form($search);
+
 echo $renderer->management_form_start();
 
 echo $renderer->accessible_skipto_links($displaycategorylisting, $displaycourselisting, $displaycoursedetail);
@@ -518,6 +521,5 @@ echo $renderer->grid_end();
 
 // End of the management form.
 echo $renderer->management_form_end();
-echo $renderer->course_search_form($search);
 
 echo $renderer->footer();
index da82415..89b7e0c 100644 (file)
@@ -399,48 +399,22 @@ class core_course_renderer extends plugin_renderer_base {
     }
 
     /**
-     * Renders html to display a course search form.
+     * Renders html to display a course search form
      *
      * @param string $value default value to populate the search field
-     * @param string $format display format - 'plain' (default), 'short' or 'navbar'
      * @return string
      */
-    public function course_search_form($value = '', $format = 'plain') {
-        static $count = 0;
-        $formid = 'coursesearch';
-        if ((++$count) > 1) {
-            $formid .= $count;
-        }
-
-        switch ($format) {
-            case 'navbar' :
-                $formid = 'coursesearchnavbar';
-                $inputid = 'navsearchbox';
-                $inputsize = 20;
-                break;
-            case 'short' :
-                $inputid = 'shortsearchbox';
-                $inputsize = 12;
-                break;
-            default :
-                $inputid = 'coursesearchbox';
-                $inputsize = 30;
-        }
-
-        $data = new stdClass();
-        $data->searchurl = \core_search\manager::get_course_search_url()->out(false);
-        $data->id = $formid;
-        $data->inputid = $inputid;
-        $data->inputsize = $inputsize;
-        $data->value = $value;
-        $data->areaids = 'core_course-course';
+    public function course_search_form($value = '') {
 
-        if ($format != 'navbar') {
-            $helpicon = new \help_icon('coursesearch', 'core');
-            $data->helpicon = $helpicon->export_for_template($this);
-        }
-
-        return $this->render_from_template('core_course/course_search_form', $data);
+        $data = [
+            'action' => \core_search\manager::get_course_search_url(),
+            'btnclass' => 'btn-primary',
+            'inputname' => 'q',
+            'searchstring' => get_string('searchcourses'),
+            'hiddenfields' => (object) ['name' => 'areaids', 'value' => 'core_course-course'],
+            'query' => $value
+        ];
+        return $this->render_from_template('core/search_input', $data);
     }
 
     /**
@@ -1890,6 +1864,13 @@ class core_course_renderer extends plugin_renderer_base {
     public function search_courses($searchcriteria) {
         global $CFG;
         $content = '';
+
+        $search = '';
+        if (!empty($searchcriteria['search'])) {
+            $search = $searchcriteria['search'];
+        }
+        $content .= $this->course_search_form($search);
+
         if (!empty($searchcriteria)) {
             // print search results
 
@@ -1931,18 +1912,6 @@ class core_course_renderer extends plugin_renderer_base {
                 $content .= $this->heading(get_string('searchresults'). ": $totalcount");
                 $content .= $courseslist;
             }
-
-            if (!empty($searchcriteria['search'])) {
-                // print search form only if there was a search by search string, otherwise it is confusing
-                $content .= $this->box_start('generalbox mdl-align');
-                $content .= $this->course_search_form($searchcriteria['search']);
-                $content .= $this->box_end();
-            }
-        } else {
-            // just print search form
-            $content .= $this->box_start('generalbox mdl-align');
-            $content .= $this->course_search_form();
-            $content .= $this->box_end();
         }
         return $content;
     }
@@ -2464,7 +2433,7 @@ class core_course_renderer extends plugin_renderer_base {
                     break;
 
                 case FRONTPAGECOURSESEARCH:
-                    $output .= $this->box($this->course_search_form('', 'short'), 'mdl-align');
+                    $output .= $this->box($this->course_search_form(''), 'd-flex justify-content-center');
                     break;
 
             }
index 144767d..ccaf4b3 100644 (file)
@@ -95,7 +95,7 @@ if (empty($searchcriteria)) {
         $aurl = new moodle_url('/course/management.php', $searchcriteria);
         $searchform = $OUTPUT->single_button($aurl, get_string('managecourses'), 'get');
     } else {
-        $searchform = $courserenderer->course_search_form($search, 'navbar');
+        $searchform = $courserenderer->course_search_form($search);
     }
     $PAGE->set_button($searchform);
 
index 9283980..70dfac6 100644 (file)
     Example context (json):
     {}
 }}
-<div class="searchbar input-group" role="search">
-    <label for="searchinput">
-        <span class="sr-only">{{#str}} searchactivities, core {{/str}}</span>
-    </label>
-    <input type="text"
-           data-action="search"
-           id="searchinput"
-           class="form-control searchinput h-auto border-right-0 rounded-left px-3 py-2"
-           placeholder="{{#str}} search, core {{/str}}"
-           name="search"
-           autocomplete="off"
-    >
-    <div class="input-group-append">
-        <div class="input-group-text border-left-0 rounded-right bg-transparent px-3 py-2">
-            <div class="search-icon">
-                {{#pix}} a/search, core {{/pix}}
-            </div>
-            <div class="clear d-none">
-                <button class="btn p-0" data-action="clearsearch">
-                    <span class="d-flex" aria-hidden="true">{{#pix}} e/cancel_solid_circle, core {{/pix}}</span>
-                    <span class="sr-only">{{#str}} clearsearch, core {{/str}}</span>
-                </button>
-            </div>
-        </div>
-    </div>
-</div>
+{{< core/search_input_auto }}
+    {{$label}}{{#str}}
+        searchactivities, core
+    {{/str}}{{/label}}
+    {{$placeholder}}{{#str}}
+        search, core
+    {{/str}}{{/placeholder}}
+{{/ core/search_input_auto }}
+
 <div class="searchresultscontainer" data-region="search-results-container" hidden="hidden" aria-live="polite">
 </div>
index 635bc2d..bd5c99c 100644 (file)
@@ -20,8 +20,8 @@ Feature: Courses can be searched for and moved in bulk.
   Scenario: Search courses finds correct results
     Given I log in as "admin"
     And I go to the courses management page
-    When I set the field "coursesearchbox" to "Biology"
-    And I press "Go"
+    When I set the field "Search" to "Biology"
+    And I press "Search"
     Then I should see "Biology Y1"
     And I should see "Biology Y2"
     And I should not see "English Y1"
@@ -31,8 +31,8 @@ Feature: Courses can be searched for and moved in bulk.
   Scenario: Search courses and move results in bulk
     Given I log in as "admin"
     And I go to the courses management page
-    And I set the field "coursesearchbox" to "Biology"
-    And I press "Go"
+    And I set the field "Search" to "Biology"
+    And I press "Search"
     When I select course "Biology Y1" in the management interface
     And I select course "Biology Y2" in the management interface
     And I set the field "menumovecoursesto" to "Science"
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;