Merge branch 'MDL-69672-310' of git://github.com/aanabit/moodle into MOODLE_310_STABLE
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Thu, 1 Oct 2020 15:49:15 +0000 (17:49 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Thu, 1 Oct 2020 15:49:15 +0000 (17:49 +0200)
131 files changed:
admin/cli/svgtool.php
admin/settings/server.php
admin/tool/dataprivacy/classes/external.php
admin/tool/dbtransfer/locallib.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/usertours/classes/privacy/provider.php
admin/tool/usertours/tests/privacy_provider_test.php
admin/webservice/testclient.php
auth/email/classes/external.php
auth/email/tests/external_test.php
badges/tests/badgeslib_test.php
blocks/classes/external.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
completion/completion_completion.php
config-dist.php
course/classes/category.php
course/externallib.php
course/lib.php
enrol/externallib.php
enrol/tests/externallib_test.php
filter/algebra/filter.php
filter/tex/lib.php
lang/en/admin.php
lang/en/completion.php
lang/en/deprecated.txt
lang/en/form.php
lang/en/moodle.php
lang/en/repository.php
lib/adminlib.php
lib/amd/build/templates.min.js.map
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/install.php
lib/db/messages.php
lib/db/services.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/moodlelib.php
lib/outputrenderers.php
lib/phpunit/classes/util.php
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/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
message/externallib.php
message/lib.php
message/tests/externallib_test.php
mnet/xmlrpc/serverlib.php
mod/assign/externallib.php
mod/forum/externallib.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/classes/external.php
mod/quiz/tests/external_test.php
mod/quiz/upgrade.txt
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
theme/boost/scss/moodle/admin.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
user/editlib.php
version.php
webservice/tests/helpers.php
webservice/upgrade.txt

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 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 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 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 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 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 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 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 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 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 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 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 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 5ac27f8..ac64324 100644 (file)
@@ -427,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'),
@@ -671,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'),
@@ -731,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...'),
@@ -941,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'),
                 )
             )
         );
@@ -1487,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'),
             )
         );
     }
@@ -1965,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'),
@@ -2071,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'),
                 )
             )
         );
@@ -2609,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'),
@@ -2863,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 3d1f03f..d0fbaa9 100644 (file)
@@ -460,7 +460,7 @@ class core_enrol_external extends external_api {
                     'id'        => new external_value(PARAM_INT, 'id of course'),
                     'shortname' => new external_value(PARAM_RAW, 'short name of course'),
                     'fullname'  => new external_value(PARAM_RAW, 'long name of course'),
-                    'displayname' => new external_value(PARAM_TEXT, 'course display name for lists.', VALUE_OPTIONAL),
+                    'displayname' => new external_value(PARAM_RAW, 'course display name for lists.', VALUE_OPTIONAL),
                     'enrolledusercount' => new external_value(PARAM_INT, 'Number of enrolled users in this course',
                             VALUE_OPTIONAL),
                     'idnumber'  => new external_value(PARAM_RAW, 'id number of course'),
index baf1d70..3a1b1de 100644 (file)
@@ -521,6 +521,61 @@ class core_enrol_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(0, $enrolledincourses[0]['lastaccess']); // I can't see this, hidden by global setting.
     }
 
+    /**
+     * Test get_users_courses with mathjax in the name.
+     */
+    public function test_get_users_courses_with_mathjax() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        // Enable MathJax filter in content and headings.
+        $this->configure_filters([
+            ['name' => 'mathjaxloader', 'state' => TEXTFILTER_ON, 'move' => -1, 'applytostrings' => true],
+        ]);
+
+        // Create a course with MathJax in the name and summary.
+        $coursedata = [
+            'fullname'         => 'Course 1 $$(a+b)=2$$',
+            'shortname'         => 'Course 1 $$(a+b)=2$$',
+            'summary'          => 'Lightwork Course 1 description $$(a+b)=2$$',
+            'summaryformat'    => FORMAT_HTML,
+        ];
+
+        $course = self::getDataGenerator()->create_course($coursedata);
+        $context = context_course::instance($course->id);
+
+        // Enrol a student in the course.
+        $student = $this->getDataGenerator()->create_user();
+        $studentroleid = $DB->get_field('role', 'id', ['shortname' => 'student']);
+        $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentroleid);
+
+        $this->setUser($student);
+
+        // Call the external function.
+        $enrolledincourses = core_enrol_external::get_users_courses($student->id, true);
+
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $enrolledincourses = external_api::clean_returnvalue(core_enrol_external::get_users_courses_returns(), $enrolledincourses);
+
+        // Check that the amount of courses is the right one.
+        $this->assertCount(1, $enrolledincourses);
+
+        // Filter the values to compare them with the returned ones.
+        $course->fullname = external_format_string($course->fullname, $context->id);
+        $course->shortname = external_format_string($course->shortname, $context->id);
+        list($course->summary, $course->summaryformat) =
+             external_format_text($course->summary, $course->summaryformat, $context->id, 'course', 'summary', 0);
+
+        // Compare the values.
+        $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">', $enrolledincourses[0]['fullname']);
+        $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">', $enrolledincourses[0]['shortname']);
+        $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">', $enrolledincourses[0]['summary']);
+        $this->assertEquals($course->fullname, $enrolledincourses[0]['fullname']);
+        $this->assertEquals($course->shortname, $enrolledincourses[0]['shortname']);
+        $this->assertEquals($course->summary, $enrolledincourses[0]['summary']);
+    }
+
     /**
      * Test get_course_enrolment_methods
      */
index 61fe8e7..a0a3a4c 100644 (file)
@@ -227,7 +227,8 @@ class filter_algebra extends moodle_text_filter {
                   $texexp = preg_replace('/\\\int\\\left\((.+?),(.+?),(.+?)\\\right\)/s','\int_'. "{\$2}^{\$3}\$1 ",$texexp);
                   $texexp = preg_replace('/\\\int\\\left\((.+?d[a-z])\\\right\)/s','\int '. "\$1 ",$texexp);
                   $texexp = preg_replace('/\\\lim\\\left\((.+?),(.+?),(.+?)\\\right\)/s','\lim_'. "{\$2\\to \$3}\$1 ",$texexp);
-                  $texexp = str_replace('\mbox', '', $texexp); // now blacklisted in tex, sorry
+                  // Remove a forbidden keyword.
+                  $texexp = str_replace('\mbox', '', $texexp);
                   $texcache = new stdClass();
                   $texcache->filter = 'algebra';
                   $texcache->version = 1;
index f579fa5..51f994b 100644 (file)
@@ -59,21 +59,29 @@ function filter_tex_get_executable($debug=false) {
     print_error('mimetexisnotexist', 'error');
 }
 
-function filter_tex_sanitize_formula($texexp) {
-    /// Check $texexp against blacklist (whitelisting could be more complete but also harder to maintain)
-    $tex_blacklist = array(
-        'include','command','loop','repeat','open','toks','output',
-        'input','catcode','name','^^',
-        '\def','\edef','\gdef','\xdef',
-        '\every','\errhelp','\errorstopmode','\scrollmode','\nonstopmode',
-        '\batchmode','\read','\write','csname','\newhelp','\uppercase',
-        '\lowercase','\relax','\aftergroup',
-        '\afterassignment','\expandafter','\noexpand','\special',
-        '\let', '\futurelet','\else','\fi','\chardef','\makeatletter','\afterground',
-        '\noexpand','\line','\mathcode','\item','\section','\mbox','\declarerobustcommand'
-    );
-
-    return  str_ireplace($tex_blacklist, 'forbiddenkeyword', $texexp);
+/**
+ * Check the formula expression against the list of denied keywords.
+ *
+ * List of allowed could be more complete but also harder to maintain.
+ *
+ * @param string $texexp Formula expression to check.
+ * @return string Formula expression with denied keywords replaced with 'forbiddenkeyword'.
+ */
+function filter_tex_sanitize_formula(string $texexp): string {
+
+    $denylist = [
+        'include', 'command', 'loop', 'repeat', 'open', 'toks', 'output',
+        'input', 'catcode', 'name', '^^',
+        '\def', '\edef', '\gdef', '\xdef',
+        '\every', '\errhelp', '\errorstopmode', '\scrollmode', '\nonstopmode',
+        '\batchmode', '\read', '\write', 'csname', '\newhelp', '\uppercase',
+        '\lowercase', '\relax', '\aftergroup',
+        '\afterassignment', '\expandafter', '\noexpand', '\special',
+        '\let', '\futurelet', '\else', '\fi', '\chardef', '\makeatletter', '\afterground',
+        '\noexpand', '\line', '\mathcode', '\item', '\section', '\mbox', '\declarerobustcommand',
+    ];
+
+    return str_ireplace($denylist, 'forbiddenkeyword', $texexp);
 }
 
 function filter_tex_get_cmd($pathname, $texexp) {
index 6cbb336..455a2cd 100644 (file)
@@ -499,6 +499,15 @@ $string['disabled'] = 'Disabled';
 $string['disableuserimages'] = 'Disable user profile images';
 $string['displayerrorswarning'] = 'Enabling the PHP setting <em>display_errors</em> is not recommended on production sites because some error messages may reveal sensitive information about your server.';
 $string['displayloginfailures'] = 'Display login failures';
+$string['divertallemails'] = 'Email diverting';
+$string['divertallemailsdetail'] = 'Used as a safeguard in development environments when testing emails and should not be used in production.';
+$string['divertallemailsexcept'] = 'Email diversion exceptions';
+$string['divertallemailsexcept_desc'] = 'A list of email exception rules separated by either commas or new lines. Each rule is interpreted as a regular expression, eg<pre>simone@acme.com
+.*@acme.com
+fred(\\+.*)?@acme.com
+</pre>';
+$string['divertallemailsto'] = 'Divert all emails';
+$string['divertallemailsto_desc'] = 'If set then all emails will be diverted to this single email address instead.';
 $string['dndallowtextandlinks'] = 'Drag and drop upload of text/links';
 $string['doclang'] = 'Language for docs';
 $string['docroot'] = 'Moodle Docs document root';
index 4091898..5a84a6f 100644 (file)
@@ -122,6 +122,7 @@ $string['courseaggregation_any'] = 'ANY selected courses to be completed';
 $string['coursealreadycompleted'] = 'You have already completed this course';
 $string['coursecomplete'] = 'Course complete';
 $string['coursecompleted'] = 'Course completed';
+$string['coursecompletedmessage'] = '<p>Congratulations!</p><p>You just completed the following course: <a href="{$a->courselink}">{$a->coursename}</a>.</p>';
 $string['coursecompletion'] = 'Course completion';
 $string['coursecompletioncondition'] = 'Condition: {$a}';
 $string['coursegrade'] = 'Course grade';
index f5887d5..2bd4d58 100644 (file)
@@ -122,3 +122,4 @@ availablelicenses,core_admin
 managelicenses,core_admin
 userfilterplaceholder,core
 sitebackpackverify,core_badges
+filetypesnotwhitelisted,core_form
index 6344ad5..4014ba2 100644 (file)
@@ -42,11 +42,11 @@ $string['err_numeric'] = 'You must enter a number here.';
 $string['err_rangelength'] = 'You must enter between {$a->format[0]} and {$a->format[1]} characters here.';
 $string['err_required'] = 'You must supply a value here.';
 $string['err_wrappingwhitespace'] = 'The value must not start or end with whitespace.';
-$string['err_wrongfileextension'] = 'Some files ({$a->wrongfiles}) cannot be uploaded. Only file types {$a->whitelist} are allowed.';
+$string['err_wrongfileextension'] = 'Some files ({$a->wrongfiles}) cannot be uploaded. Only file types {$a->allowlist} are allowed.';
 $string['filesofthesetypes'] = 'Accepted file types:';
 $string['filetypesany'] = 'All file types';
 $string['filetypesnotall'] = 'It is not allowed to select \'All file types\' here';
-$string['filetypesnotwhitelisted'] = 'These file types are not allowed here: {$a}';
+$string['filetypesnotallowed'] = 'These file types are not allowed here: {$a}';
 $string['filetypesothers'] = 'Other files';
 $string['filetypesunknown'] = 'Unknown file types: {$a}';
 $string['general'] = 'General';
@@ -85,3 +85,6 @@ $string['timeunit'] = 'Time unit';
 $string['timing'] = 'Timing';
 $string['unmaskpassword'] = 'Unmask';
 $string['year'] = 'Year';
+
+// Deprecated since Moodle 3.10.
+$string['filetypesnotwhitelisted'] = 'These file types are not allowed here: {$a}';
index f5a35c1..bf203b5 100644 (file)
@@ -1231,6 +1231,7 @@ $string['messageprovider:badgecreatornotice'] = 'Badge creator notifications';
 $string['messageprovider:badgerecipientnotice'] = 'Badge recipient notifications';
 $string['messageprovider:competencyplancomment'] = 'Comment posted on a learning plan';
 $string['messageprovider:competencyusercompcomment'] = 'Comment posted on a competency';
+$string['messageprovider:coursecompleted'] = 'Course completed';
 $string['messageprovider:courserequestapproved'] = 'Course creation request approval notification';
 $string['messageprovider:courserequested'] = 'Course creation request notification';
 $string['messageprovider:courserequestrejected'] = 'Course creation request rejection notification';
index 05791e5..ddb5ecc 100644 (file)
@@ -58,6 +58,7 @@ $string['cannotdownload'] = 'Cannot download this file';
 $string['cannotdownloaddir'] = 'Cannot download this folder';
 $string['cannotinitplugin'] = 'Call plugin_init failed';
 $string['cannotunzipcontentunreadable'] = 'Cannot unzip this file because the contents of the file cannot be read.';
+$string['cannotunzipextractfileerror'] = 'Cannot unzip this file because one or more of it\'s files cannot be read.';
 $string['cannotunzipquotaexceeded'] = 'Cannot unzip this file because the maximum size allowed in this draft area will be exceeded.';
 $string['cleancache'] = 'Clean my cache files';
 $string['close'] = 'Close';
index 3a7045d..e820668 100644 (file)
@@ -9071,7 +9071,7 @@ function any_new_admin_settings($node) {
  */
 function db_should_replace($table, $column = ''): bool {
 
-    // TODO: this is horrible hack, we should do whitelisting and each plugin should be responsible for proper replacing...
+    // TODO: this is horrible hack, we should have a hook and each plugin should be responsible for proper replacing...
     $skiptables = ['config', 'config_plugins', 'filter_config', 'sessions',
         'events_queue', 'repository_instance_config', 'block_instances', 'files'];
 
@@ -11271,12 +11271,12 @@ class admin_setting_filetypes extends admin_setting_configtext {
 
         // No need to call parent's validation here as we are PARAM_RAW.
 
-        if ($this->util->is_whitelisted($data, $this->onlytypes)) {
+        if ($this->util->is_listed($data, $this->onlytypes)) {
             return true;
 
         } else {
-            $troublemakers = $this->util->get_not_whitelisted($data, $this->onlytypes);
-            return get_string('filetypesnotwhitelisted', 'core_form', implode(' ', $troublemakers));
+            $troublemakers = $this->util->get_not_listed($data, $this->onlytypes);
+            return get_string('filetypesnotallowed', 'core_form', implode(' ', $troublemakers));
         }
     }
 
index b75e37f..028dd34 100644 (file)
Binary files a/lib/amd/build/templates.min.js.map and b/lib/amd/build/templates.min.js.map differ
index 8ba0e8b..adb2528 100644 (file)
@@ -65,8 +65,8 @@ define([
     /** @var {Bool} isLoadingTemplates - Whether templates are currently being loaded */
     var isLoadingTemplates = false;
 
-    /** @var {Array} blacklistedNestedHelpers - List of helpers that can't be called within other helpers */
-    var blacklistedNestedHelpers = ['js'];
+    /** @var {Array} disallowedNestedHelpers - List of helpers that can't be called within other helpers */
+    var disallowedNestedHelpers = ['js'];
 
     /**
      * Search the various caches for a template promise for the given search key.
@@ -609,7 +609,7 @@ define([
      * template.
      *
      * This will parse the provided text before giving it to the helper function
-     * in order to remove any blacklisted nested helpers to prevent one helper
+     * in order to remove any disallowed nested helpers to prevent one helper
      * from calling another.
      *
      * In particular to prevent the JS helper from being called from within another
@@ -623,12 +623,12 @@ define([
     Renderer.prototype.addHelperFunction = function(helperFunction, context) {
         return function() {
             return function(sectionText, helper) {
-                // Override the blacklisted helpers in the template context with
+                // Override the disallowed helpers in the template context with
                 // a function that returns an empty string for use when executing
                 // other helpers. This is to prevent these helpers from being
                 // executed as part of the rendering of another helper in order to
                 // prevent any potential security issues.
-                var originalHelpers = blacklistedNestedHelpers.reduce(function(carry, name) {
+                var originalHelpers = disallowedNestedHelpers.reduce(function(carry, name) {
                     if (context.hasOwnProperty(name)) {
                         carry[name] = context[name];
                     }
@@ -636,14 +636,14 @@ define([
                     return carry;
                 }, {});
 
-                blacklistedNestedHelpers.forEach(function(helperName) {
+                disallowedNestedHelpers.forEach(function(helperName) {
                     context[helperName] = function() {
                         return '';
                     };
                 });
 
                 // Execute the helper with the modified context that doesn't include
-                // the blacklisted nested helpers. This prevents the blacklisted
+                // the disallowed nested helpers. This prevents the disallowed
                 // helpers from being called from within other helpers.
                 var result = helperFunction.apply(this, [context, sectionText, helper]);
 
index fd96e72..fa25ae4 100644 (file)
@@ -733,7 +733,7 @@ $cache = '.var_export($cache, true).';
     /**
      * List all core subsystems and their location
      *
-     * This is a whitelist of components that are part of the core and their
+     * This is a list of components that are part of the core and their
      * language strings are defined in /lang/en/<<subsystem>>.php. If a given
      * plugin is not listed here and it does not have proper plugintype prefix,
      * then it is considered as course activity module.
index 8c71720..8388a13 100644 (file)
@@ -15,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Contains a class providing functions used to check the host/port black/whitelists for curl.
+ * Contains a class providing functions used to check the allowed/blocked host/ports for curl.
  *
  * @package   core
  * @copyright 2016 Jake Dallimore
@@ -32,7 +32,7 @@ defined('MOODLE_INTERNAL') || exit();
  * Host and port checking for curl.
  *
  * This class provides a means to check URL/host/port against the system-level cURL security entries.
- * It does not provide a means to add URLs, hosts or ports to the black/white lists; this is configured manually
+ * It does not provide a means to add URLs, hosts or ports to the allowed/blocked lists; this is configured manually
  * via the site admin section of Moodle (See: 'Site admin' > 'Security' > 'HTTP Security').
  *
  * This class is currently used by the 'curl' wrapper class in lib/filelib.php.
@@ -55,12 +55,12 @@ class curl_security_helper extends curl_security_helper_base {
     ];
 
     /**
-     * Checks whether the given URL is blacklisted by checking its address and port number against the black/white lists.
+     * Checks whether the given URL is blocked by checking its address and port number against the allow/block lists.
      * The behaviour of this function can be classified as strict, as it returns true for URLs which are invalid or
-     * could not be parsed, as well as those valid URLs which were found in the blacklist.
+     * could not be parsed, as well as those valid URLs which were found in the blocklist.
      *
      * @param string $urlstring the URL to check.
-     * @return bool true if the URL is blacklisted or invalid and false if the URL is not blacklisted.
+     * @return bool true if the URL is blocked or invalid and false if the URL is not blocked.
      */
     public function url_is_blocked($urlstring) {
         // If no config data is present, then all hosts/ports are allowed.
@@ -85,7 +85,7 @@ class curl_security_helper extends curl_security_helper_base {
         }
 
         if ($parsed['port'] && $parsed['host']) {
-            // Check the host and port against the blacklist/whitelist entries.
+            // Check the host and port against the allow/block entries.
             return $this->host_is_blocked($parsed['host']) || $this->port_is_blocked($parsed['port']);
         }
         return true;
@@ -114,9 +114,9 @@ class curl_security_helper extends curl_security_helper_base {
      *  - This will perform a DNS reverse lookup if required.
      *
      * The behaviour of this function can be classified as strict, as it returns true for hosts which are invalid or
-     * could not be parsed, as well as those valid URLs which were found in the blacklist.
+     * could not be parsed, as well as those valid URLs which were found in the blocklist.
      *
-     * @param string $host the host component of the URL to check against the blacklist.
+     * @param string $host the host component of the URL to check against the blocklist.
      * @return bool true if the host is both valid and blocked, false otherwise.
      */
     protected function host_is_blocked($host) {
@@ -126,7 +126,7 @@ class curl_security_helper extends curl_security_helper_base {
 
         // Fix for square brackets in the 'host' portion of the URL (only occurs if an IPv6 address is specified).
         $host = str_replace(array('[', ']'), '', $host); // RFC3986, section 3.2.2.
-        $blacklistedhosts = $this->get_blacklisted_hosts_by_category();
+        $blockedhosts = $this->get_blocked_hosts_by_category();
 
         if (ip_utils::is_ip_address($host)) {
             if ($this->address_explicitly_blocked($host)) {
@@ -134,7 +134,7 @@ class curl_security_helper extends curl_security_helper_base {
             }
 
             // Only perform a reverse lookup if there is a point to it (i.e. we have rules to check against).
-            if ($blacklistedhosts['domain'] || $blacklistedhosts['domainwildcard']) {
+            if ($blockedhosts['domain'] || $blockedhosts['domainwildcard']) {
                 // DNS reverse lookup - supports both IPv4 and IPv6 address formats.
                 $hostname = gethostbyaddr($host);
                 if ($hostname !== $host && $this->host_explicitly_blocked($hostname)) {
@@ -147,7 +147,7 @@ class curl_security_helper extends curl_security_helper_base {
             }
 
             // Only perform a forward lookup if there are IP rules to check against.
-            if ($blacklistedhosts['ipv4'] || $blacklistedhosts['ipv6']) {
+            if ($blockedhosts['ipv4'] || $blockedhosts['ipv6']) {
                 // DNS forward lookup - returns a list of only IPv4 addresses!
                 $hostips = $this->get_host_list_by_name($host);
 
@@ -156,7 +156,7 @@ class curl_security_helper extends curl_security_helper_base {
                     return true;
                 }
 
-                // If any of the returned IPs are in the blacklist, block the request.
+                // If any of the returned IPs are in the blocklist, block the request.
                 foreach ($hostips as $hostip) {
                     if ($this->address_explicitly_blocked($hostip)) {
                         return true;
@@ -182,10 +182,10 @@ class curl_security_helper extends curl_security_helper_base {
     }
 
     /**
-     * Checks whether the given port is blocked, as determined by its absence on the ports whitelist.
-     * Ports are assumed to be blocked unless found in the whitelist.
+     * Checks whether the given port is blocked, as determined by its absence on the ports allowlist.
+     * Ports are assumed to be blocked unless found in the allowlist.
      *
-     * @param integer|string $port the port to check against the ports whitelist.
+     * @param integer|string $port the port to check against the ports allowlist.
      * @return bool true if the port is blocked, false otherwise.
      */
     protected function port_is_blocked($port) {
@@ -194,28 +194,28 @@ class curl_security_helper extends curl_security_helper_base {
         if (empty($port) || (string)$portnum !== (string)$port || $port < 0) {
             return true;
         }
-        $allowedports = $this->get_whitelisted_ports();
+        $allowedports = $this->get_allowed_ports();
         return !empty($allowedports) && !in_array($portnum, $allowedports);
     }
 
     /**
-     * Convenience method to check whether we have any entries in the host blacklist or ports whitelist admin settings.
-     * If no entries are found at all, the assumption is that the blacklist is disabled entirely.
+     * Convenience method to check whether we have any entries in the host blocklist or ports allowlist admin settings.
+     * If no entries are found at all, the assumption is that the blocklist is disabled entirely.
      *
      * @return bool true if one or more entries exist, false otherwise.
      */
     public function is_enabled() {
-        return (!empty($this->get_whitelisted_ports()) || !empty($this->get_blacklisted_hosts()));
+        return (!empty($this->get_allowed_ports()) || !empty($this->get_blocked_hosts()));
     }
 
     /**
      * Checks whether the input address is blocked by at any of the IPv4 or IPv6 address rules.
      *
      * @param string $addr the ip address to check.
-     * @return bool true if the address is covered by an entry in the blacklist, false otherwise.
+     * @return bool true if the address is covered by an entry in the blocklist, false otherwise.
      */
     protected function address_explicitly_blocked($addr) {
-        $blockedhosts = $this->get_blacklisted_hosts_by_category();
+        $blockedhosts = $this->get_blocked_hosts_by_category();
         $iphostsblocked = array_merge($blockedhosts['ipv4'], $blockedhosts['ipv6']);
         return address_in_subnet($addr, implode(',', $iphostsblocked));
     }
@@ -224,10 +224,10 @@ class curl_security_helper extends curl_security_helper_base {
      * Checks whether the input hostname is blocked by any of the domain/wildcard rules.
      *
      * @param string $host the hostname to check
-     * @return bool true if the host is covered by an entry in the blacklist, false otherwise.
+     * @return bool true if the host is covered by an entry in the blocklist, false otherwise.
      */
     protected function host_explicitly_blocked($host) {
-        $blockedhosts = $this->get_blacklisted_hosts_by_category();
+        $blockedhosts = $this->get_blocked_hosts_by_category();
         $domainhostsblocked = array_merge($blockedhosts['domain'], $blockedhosts['domainwildcard']);
         return ip_utils::is_domain_in_allowed_list($host, $domainhostsblocked);
     }
@@ -238,10 +238,10 @@ class curl_security_helper extends curl_security_helper_base {
      *
      * @return array of host/domain/ip entries from the 'curlsecurityblockedhosts' config.
      */
-    protected function get_blacklisted_hosts_by_category() {
+    protected function get_blocked_hosts_by_category() {
         // For each of the admin setting entries, check and place in the correct section of the config array.
         $config = ['ipv6' => [], 'ipv4' => [], 'domain' => [], 'domainwildcard' => []];
-        $entries = $this->get_blacklisted_hosts();
+        $entries = $this->get_blocked_hosts();
         foreach ($entries as $entry) {
             if (ip_utils::is_ipv6_address($entry) || ip_utils::is_ipv6_range($entry)) {
                 $config['ipv6'][] = $entry;
@@ -257,11 +257,11 @@ class curl_security_helper extends curl_security_helper_base {
     }
 
     /**
-     * Helper that returns the whitelisted ports, as defined in the 'curlsecurityallowedport' setting.
+     * Helper that returns the allowed ports, as defined in the 'curlsecurityallowedport' setting.
      *
-     * @return array the array of whitelisted ports.
+     * @return array the array of allowed ports.
      */
-    protected function get_whitelisted_ports() {
+    protected function get_allowed_ports() {
         global $CFG;
         if (!isset($CFG->curlsecurityallowedport)) {
             return [];
@@ -272,11 +272,11 @@ class curl_security_helper extends curl_security_helper_base {
     }
 
     /**
-     * Helper that returns the blacklisted hosts, as defined in the 'curlsecurityblockedhosts' setting.
+     * Helper that returns the blocked hosts, as defined in the 'curlsecurityblockedhosts' setting.
      *
-     * @return array the array of blacklisted host entries.
+     * @return array the array of blocked host entries.
      */
-    protected function get_blacklisted_hosts() {
+    protected function get_blocked_hosts() {
         global $CFG;
         if (!isset($CFG->curlsecurityblockedhosts)) {
             return [];
index 4dd2b90..ada43c2 100644 (file)
@@ -38,7 +38,7 @@ class mustache_engine extends \Mustache_Engine {
     /**
      * @var string[] Names of helpers that aren't allowed to be called within other helpers.
      */
-    private $blacklistednestedhelpers = [];
+    private $disallowednestedhelpers = [];
 
     /**
      * Mustache engine constructor.
@@ -47,13 +47,19 @@ class mustache_engine extends \Mustache_Engine {
      * $options = [
      *      // A list of helpers (by name) to prevent from executing within the rendering
      *      // of other helpers.
-     *      'blacklistednestedhelpers' => ['js']
+     *      'disallowednestedhelpers' => ['js']
      * ];
      * @param array $options [description]
      */
     public function __construct(array $options = []) {
+
         if (isset($options['blacklistednestedhelpers'])) {
-            $this->blacklistednestedhelpers = $options['blacklistednestedhelpers'];
+            debugging('blacklistednestedhelpers option is deprecated. Use disallowednestedhelpers instead.', DEBUG_DEVELOPER);
+            $this->disallowednestedhelpers = $options['blacklistednestedhelpers'];
+        }
+
+        if (isset($options['disallowednestedhelpers'])) {
+            $this->disallowednestedhelpers = $options['disallowednestedhelpers'];
         }
 
         parent::__construct($options);
@@ -69,7 +75,7 @@ class mustache_engine extends \Mustache_Engine {
     public function getHelpers()
     {
         if (!isset($this->helpers)) {
-            $this->helpers = new mustache_helper_collection(null, $this->blacklistednestedhelpers);
+            $this->helpers = new mustache_helper_collection(null, $this->disallowednestedhelpers);
         }
 
         return $this->helpers;
index 233d741..0c72ff6 100644 (file)
@@ -34,7 +34,7 @@ class mustache_helper_collection extends \Mustache_HelperCollection {
     /**
      * @var string[] Names of helpers that aren't allowed to be called within other helpers.
      */
-    private $blacklistednestedhelpers = [];
+    private $disallowednestedhelpers = [];
 
     /**
      * Helper Collection constructor.
@@ -44,43 +44,43 @@ class mustache_helper_collection extends \Mustache_HelperCollection {
      * @throws \Mustache_Exception_InvalidArgumentException if the $helpers argument isn't an array or Traversable
      *
      * @param array|\Traversable $helpers (default: null)
-     * @param string[] $blacklistednestedhelpers Names of helpers that aren't allowed to be called within other helpers.
+     * @param string[] $disallowednestedhelpers Names of helpers that aren't allowed to be called within other helpers.
      */
-    public function __construct($helpers = null, array $blacklistednestedhelpers = []) {
-        $this->blacklistednestedhelpers = $blacklistednestedhelpers;
+    public function __construct($helpers = null, array $disallowednestedhelpers = []) {
+        $this->disallowednestedhelpers = $disallowednestedhelpers;
         parent::__construct($helpers);
     }
 
     /**
      * Add a helper to this collection.
      *
-     * This function has overridden the parent implementation to provide blacklist
+     * This function has overridden the parent implementation to provide disallowing
      * functionality for certain helpers to prevent them being called from within
      * other helpers. This is because the JavaScript helper can be used in a
      * security exploit if it can be nested.
      *
      * The function will wrap callable helpers in an anonymous function that strips
-     * out the blacklisted helpers from the source string before giving it to the
-     * helper function. This prevents the blacklisted helper functions from being
+     * out the disallowed helpers from the source string before giving it to the
+     * helper function. This prevents the disallowed helper functions from being
      * called by nested render functions from within other helpers.
      *
      * @see \Mustache_HelperCollection::add()
      * @param string $name
      * @param mixed  $helper
      */
-    public function add($name, $helper)
-    {
-        $blacklist = $this->blacklistednestedhelpers;
+    public function add($name, $helper) {
 
-        if (is_callable($helper) && !empty($blacklist)) {
-            $helper = function($source, \Mustache_LambdaHelper $lambdahelper) use ($helper, $blacklist) {
+        $disallowedlist = $this->disallowednestedhelpers;
 
-                // Temporarily override the blacklisted helpers to return nothing
+        if (is_callable($helper) && !empty($disallowedlist)) {
+            $helper = function($source, \Mustache_LambdaHelper $lambdahelper) use ($helper, $disallowedlist) {
+
+                // Temporarily override the disallowed helpers to return nothing
                 // so that they can't be executed from within other helpers.
-                $disabledhelpers = $this->disable_helpers($blacklist);
+                $disabledhelpers = $this->disable_helpers($disallowedlist);
                 // Call the original function with the modified sources.
                 $result = call_user_func($helper, $source, $lambdahelper);
-                // Restore the original blacklisted helper implementations now
+                // Restore the original disallowed helper implementations now
                 // that this helper has finished executing so that the rest of
                 // the rendering process continues to work correctly.
                 $this->restore_helpers($disabledhelpers);
@@ -89,7 +89,7 @@ class mustache_helper_collection extends \Mustache_HelperCollection {
                 // This is done because a secondary render is called on the result
                 // of a helper function if it still includes mustache tags. See
                 // the section function of Mustache_Compiler for details.
-                return $this->strip_blacklisted_helpers($blacklist, $result);
+                return $this->strip_disallowed_helpers($disallowedlist, $result);
             };
         }
 
@@ -137,18 +137,18 @@ class mustache_helper_collection extends \Mustache_HelperCollection {
     }
 
     /**
-     * Parse the given string and remove any reference to blacklisted helpers.
+     * Parse the given string and remove any reference to disallowed helpers.
      *
      * E.g.
-     * $blacklist = ['js'];
+     * $disallowedlist = ['js'];
      * $string = "core, move, {{#js}} some nasty JS hack {{/js}}"
      * result: "core, move, {{}}"
      *
-     * @param  string[] $blacklist List of helper names to strip
+     * @param  string[] $disallowedlist List of helper names to strip
      * @param  string $string String to parse
      * @return string Parsed string
      */
-    public function strip_blacklisted_helpers($blacklist, $string) {
+    public function strip_disallowed_helpers($disallowedlist, $string) {
         $starttoken = \Mustache_Tokenizer::T_SECTION;
         $endtoken = \Mustache_Tokenizer::T_END_SECTION;
         if ($endtoken == '/') {
@@ -160,7 +160,7 @@ class mustache_helper_collection extends \Mustache_HelperCollection {
             // the user is able to change the delimeters on a per template
             // basis so they may not be curly braces.
             return '/\s*' . $starttoken . '\s*'. $name . '\W+.*' . $endtoken . '\s*' . $name . '\s*/';
-        }, $blacklist);
+        }, $disallowedlist);
 
         // This will strip out unwanted helpers from the $source string
         // before providing it to the original helper function.
@@ -168,9 +168,25 @@ class mustache_helper_collection extends \Mustache_HelperCollection {
         // Before:
         // "core, move, {{#js}} some nasty JS hack {{/js}}"
         // After:
-        // "core, move, {{}}"
+        // "core, move, {{}}".
         return preg_replace_callback($regexes, function() {
             return '';
         }, $string);
     }
+
+    /**
+     * Parse the given string and remove any reference to disallowed helpers.
+     *
+     * @deprecated Deprecated since Moodle 3.10 (MDL-69050) - use {@see self::strip_disallowed_helpers()}
+     * @param  string[] $disallowedlist List of helper names to strip
+     * @param  string $string String to parse
+     * @return string Parsed string
+     */
+    public function strip_blacklisted_helpers($disallowedlist, $string) {
+
+        debugging('mustache_helper_collection::strip_blacklisted_helpers() is deprecated. ' .
+            'Please use mustache_helper_collection::strip_disallowed_helpers() instead.', DEBUG_DEVELOPER);
+
+        return $this->strip_disallowed_helpers($disallowedlist, $string);
+    }
 }
index e20dd2c..84f00e2 100644 (file)
@@ -601,34 +601,36 @@ class core_user {
             // The user has chosen to delete the selected users picture.
             $fs->delete_area_files($context->id, 'user', 'icon'); // Drop all images in area.
             $newpicture = 0;
+        }
 
-        } else {
-            // Save newly uploaded file, this will avoid context mismatch for newly created users.
-            file_save_draft_area_files($usernew->imagefile, $context->id, 'user', 'newicon', 0, $filemanageroptions);
-            if (($iconfiles = $fs->get_area_files($context->id, 'user', 'newicon')) && count($iconfiles) == 2) {
-                // Get file which was uploaded in draft area.
-                foreach ($iconfiles as $file) {
-                    if (!$file->is_directory()) {
-                        break;
-                    }
-                }
-                // Copy file to temporary location and the send it for processing icon.
-                if ($iconfile = $file->copy_content_to_temp()) {
-                    // There is a new image that has been uploaded.
-                    // Process the new image and set the user to make use of it.
-                    // NOTE: Uploaded images always take over Gravatar.
-                    $newpicture = (int)process_new_icon($context, 'user', 'icon', 0, $iconfile);
-                    // Delete temporary file.
-                    @unlink($iconfile);
-                    // Remove uploaded file.
-                    $fs->delete_area_files($context->id, 'user', 'newicon');
-                } else {
-                    // Something went wrong while creating temp file.
-                    // Remove uploaded file.
-                    $fs->delete_area_files($context->id, 'user', 'newicon');
-                    return false;
+        // Save newly uploaded file, this will avoid context mismatch for newly created users.
+        if (!isset($usernew->imagefile)) {
+            $usernew->imagefile = 0;
+        }
+        file_save_draft_area_files($usernew->imagefile, $context->id, 'user', 'newicon', 0, $filemanageroptions);
+        if (($iconfiles = $fs->get_area_files($context->id, 'user', 'newicon')) && count($iconfiles) == 2) {
+            // Get file which was uploaded in draft area.
+            foreach ($iconfiles as $file) {
+                if (!$file->is_directory()) {
+                    break;
                 }
             }
+            // Copy file to temporary location and the send it for processing icon.
+            if ($iconfile = $file->copy_content_to_temp()) {
+                // There is a new image that has been uploaded.
+                // Process the new image and set the user to make use of it.
+                // NOTE: Uploaded images always take over Gravatar.
+                $newpicture = (int)process_new_icon($context, 'user', 'icon', 0, $iconfile);
+                // Delete temporary file.
+                @unlink($iconfile);
+                // Remove uploaded file.
+                $fs->delete_area_files($context->id, 'user', 'newicon');
+            } else {
+                // Something went wrong while creating temp file.
+                // Remove uploaded file.
+                $fs->delete_area_files($context->id, 'user', 'newicon');
+                return false;
+            }
         }
 
         if ($newpicture != $user->picture) {
index 2ca8e54..613f82f 100644 (file)
@@ -784,7 +784,6 @@ function get_courses_search($searchterms, $sort, $page, $recordsperpage, &$total
  *
  * @global object
  * @global object
- * @uses MAX_COURSES_IN_CATEGORY
  * @uses MAX_COURSE_CATEGORIES
  * @uses SITEID
  * @uses CONTEXT_COURSE
@@ -801,7 +800,8 @@ function fix_course_sortorder() {
 
     if ($unsorted = $DB->get_records('course_categories', array('sortorder'=>0))) {
         //move all categories that are not sorted yet to the end
-        $DB->set_field('course_categories', 'sortorder', MAX_COURSES_IN_CATEGORY*MAX_COURSE_CATEGORIES, array('sortorder'=>0));
+        $DB->set_field('course_categories', 'sortorder',
+            get_max_courses_in_category() * MAX_COURSE_CATEGORIES, array('sortorder' => 0));
         $cacheevents['changesincoursecat'] = true;
     }
 
@@ -901,7 +901,7 @@ function fix_course_sortorder() {
         $categories = array();
         foreach ($updatecounts as $cat) {
             $cat->coursecount = $cat->newcount;
-            if ($cat->coursecount >= MAX_COURSES_IN_CATEGORY) {
+            if ($cat->coursecount >= get_max_courses_in_category()) {
                 $categories[] = $cat->id;
             }
             unset($cat->newcount);
@@ -909,7 +909,11 @@ function fix_course_sortorder() {
         }
         if (!empty($categories)) {
             $str = implode(', ', $categories);
-            debugging("The number of courses (category id: $str) has reached MAX_COURSES_IN_CATEGORY (" . MAX_COURSES_IN_CATEGORY . "), it will cause a sorting performance issue, please increase the value of MAX_COURSES_IN_CATEGORY in lib/datalib.php file. See tracker issue: MDL-25669", DEBUG_DEVELOPER);
+            debugging("The number of courses (category id: $str) has reached max number of courses " .
+                "in a category (" . get_max_courses_in_category() . "). It will cause a sorting performance issue. " .
+                "Please set higher value for \$CFG->maxcoursesincategory in config.php. " .
+                "Please also make sure \$CFG->maxcoursesincategory * MAX_COURSE_CATEGORIES less than max integer. " .
+                "See tracker issues: MDL-25669 and MDL-69573", DEBUG_DEVELOPER);
         }
         $cacheevents['changesincoursecat'] = true;
     }
@@ -918,13 +922,13 @@ function fix_course_sortorder() {
     $sql = "SELECT DISTINCT cc.id, cc.sortorder
               FROM {course_categories} cc
               JOIN {course} c ON c.category = cc.id
-             WHERE c.sortorder < cc.sortorder OR c.sortorder > cc.sortorder + ".MAX_COURSES_IN_CATEGORY;
+             WHERE c.sortorder < cc.sortorder OR c.sortorder > cc.sortorder + " . get_max_courses_in_category();
 
     if ($fixcategories = $DB->get_records_sql($sql)) {
         //fix the course sortorder ranges
         foreach ($fixcategories as $cat) {
             $sql = "UPDATE {course}
-                       SET sortorder = ".$DB->sql_modulo('sortorder', MAX_COURSES_IN_CATEGORY)." + ?
+                       SET sortorder = ".$DB->sql_modulo('sortorder', get_max_courses_in_category())." + ?
                      WHERE category = ?";
             $DB->execute($sql, array($cat->sortorder, $cat->id));
         }
@@ -992,7 +996,6 @@ function fix_course_sortorder() {
  * @todo Document the arguments of this function better
  *
  * @global object
- * @uses MAX_COURSES_IN_CATEGORY
  * @uses CONTEXT_COURSECAT
  * @param array $children
  * @param int $sortorder
@@ -1009,7 +1012,7 @@ function _fix_course_cats($children, &$sortorder, $parent, $depth, $path, &$fixc
     $changesmade = false;
 
     foreach ($children as $cat) {
-        $sortorder = $sortorder + MAX_COURSES_IN_CATEGORY;
+        $sortorder = $sortorder + get_max_courses_in_category();
         $update = false;
         if ($parent != $cat->parent or $depth != $cat->depth or $path.'/'.$cat->id != $cat->path) {
             $cat->parent = $parent;
@@ -1804,3 +1807,19 @@ function decompose_update_into_safe_changes(array $newvalues, $unusedvalue) {
 
     return $safechanges;
 }
+
+/**
+ * Return maximum number of courses in a category
+ *
+ * @uses MAX_COURSES_IN_CATEGORY
+ * @return int number of courses
+ */
+function get_max_courses_in_category() {
+    global $CFG;
+    // Use default MAX_COURSES_IN_CATEGORY if $CFG->maxcoursesincategory is not set or invalid.
+    if (!isset($CFG->maxcoursesincategory) || clean_param($CFG->maxcoursesincategory, PARAM_INT) == 0) {
+        return MAX_COURSES_IN_CATEGORY;
+    } else {
+        return $CFG->maxcoursesincategory;
+    }
+}
index 0722a2a..5d4d556 100644 (file)
@@ -107,7 +107,7 @@ function xmldb_main_install() {
     $cat = new stdClass();
     $cat->name         = get_string('miscellaneous');
     $cat->depth        = 1;
-    $cat->sortorder    = MAX_COURSES_IN_CATEGORY;
+    $cat->sortorder    = get_max_courses_in_category();
     $cat->timemodified = time();
     $catid = $DB->insert_record('course_categories', $cat);
     $DB->set_field('course_categories', 'path', '/'.$catid, array('id'=>$catid));
index eeef52e..1ffde1b 100644 (file)
@@ -86,6 +86,9 @@ $messageproviders = array (
         ),
     ),
 
+    // Course completed. Requires course completion configured at course level. It does not work with just activity progress.
+    'coursecompleted' => [],
+
     // Badge award notification to a badge recipient.
     'badgerecipientnotice' => array (
         'defaults' => array(
index 94f0c0d..c589221 100644 (file)
@@ -287,6 +287,13 @@ $functions = array(
         'type'          => 'read',
         'ajax'          => true,
     ],
+    'core_calendar_get_calendar_export_token' => [
+        'classname'     => 'core_calendar\external\export\token',
+        'methodname'    => 'execute',
+        'description'   => 'Return the auth token required for exporting a calendar.',
+        'type'          => 'read',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ],
     'core_cohort_add_cohort_members' => array(
         'classname' => 'core_cohort_external',
         'methodname' => 'add_cohort_members',
index e20ad23..61db87c 100644 (file)
@@ -49,7 +49,7 @@ function events_trigger() {
 /**
  * List all core subsystems and their location
  *
- * This is a whitelist of components that are part of the core and their
+ * This is a list of components that are part of the core and their
  * language strings are defined in /lang/en/<<subsystem>>.php. If a given
  * plugin is not listed here and it does not have proper plugintype prefix,
  * then it is considered as course activity module.
index fbb5c39..193f0c5 100644 (file)
@@ -1158,7 +1158,6 @@ function enrol_selfenrol_available($courseid) {
             continue;
         }
         if ($instance->enrol === 'guest') {
-            // blacklist known temporary guest plugins
             continue;
         }
         if ($plugins[$instance->enrol]->show_enrolme_link($instance)) {
index c8e08ab..7bc8ab6 100644 (file)
@@ -497,7 +497,7 @@ class core_external extends external_api {
                 'value' => new external_value(PARAM_RAW, 'value of the item as it is stored', VALUE_OPTIONAL),
                 'itemid' => new external_value(PARAM_RAW, 'identifier of the updated item', VALUE_OPTIONAL),
                 'edithint' => new external_value(PARAM_NOTAGS, 'hint for editing element', VALUE_OPTIONAL),
-                'editlabel' => new external_value(PARAM_NOTAGS, 'label for editing element', VALUE_OPTIONAL),
+                'editlabel' => new external_value(PARAM_RAW, 'label for editing element', VALUE_OPTIONAL),
                 'type' => new external_value(PARAM_ALPHA, 'type of the element (text, toggle, select)', VALUE_OPTIONAL),
                 'options' => new external_value(PARAM_RAW, 'options of the element, format depends on type', VALUE_OPTIONAL),
                 'linkeverything' => new external_value(PARAM_INT, 'Should everything be wrapped in the edit link or link displayed separately', VALUE_OPTIONAL),
index bf43f49..788371a 100644 (file)
@@ -199,9 +199,41 @@ class core_external_testcase extends externallib_advanced_testcase {
         $tag = $this->getDataGenerator()->create_tag();
         $res = core_external::update_inplace_editable('core_tag', 'tagname', $tag->id, 'new tag name');
         $res = external_api::clean_returnvalue(core_external::update_inplace_editable_returns(), $res);
+
         $this->assertEquals('new tag name', $res['value']);
     }
 
+    /**
+     * Test update_inplace_editable with mathjax.
+     */
+    public function test_update_inplace_editable_with_mathjax() {
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        // Enable MathJax filter in content and headings.
+        $this->configure_filters([
+            ['name' => 'mathjaxloader', 'state' => TEXTFILTER_ON, 'move' => -1, 'applytostrings' => true],
+        ]);
+
+        // Create a forum.
+        $course = $this->getDataGenerator()->create_course();
+        $forum = self::getDataGenerator()->create_module('forum', array('course' => $course->id, 'name' => 'forum name'));
+
+        // Change the forum name.
+        $newname = 'New forum name $$(a+b)=2$$';
+        $res = core_external::update_inplace_editable('core_course', 'activityname', $forum->cmid, $newname);
+        $res = external_api::clean_returnvalue(core_external::update_inplace_editable_returns(), $res);
+
+        // Format original data.
+        $context = context_module::instance($forum->cmid);
+        $newname = external_format_string($newname, $context->id);
+        $editlabel = get_string('newactivityname', '', $newname);
+
+        // Check editlabel is the same and has mathjax.
+        $this->assertStringContainsString('<span class="filter_mathjaxloader_equation">', $res['editlabel']);
+        $this->assertEquals($editlabel, $res['editlabel']);
+    }
+
     public function test_get_user_dates() {
         $this->resetAfterTest();
 
index 9a4a50e..17b4a29 100644 (file)
@@ -1105,7 +1105,7 @@ function external_generate_token_for_current_user($service) {
             $unsettoken = true;
         }
 
-        // Remove token if its ip not in whitelist.
+        // Remove token if its IP is restricted.
         if (isset($token->iprestriction) and !address_in_subnet(getremoteaddr(), $token->iprestriction)) {
             $unsettoken = true;
         }
index fd0c43e..6ec5d7b 100644 (file)
@@ -3018,7 +3018,7 @@ class curl {
     private $cookie   = false;
     /** @var bool tracks multiple headers in response - redirect detection */
     private $responsefinished = false;
-    /** @var security helper class, responsible for checking host/ports against blacklist/whitelist entries.*/
+    /** @var security helper class, responsible for checking host/ports against allowed/blocked entries.*/
     private $securityhelper;
     /** @var bool ignoresecurity a flag which can be supplied to the constructor, allowing security to be bypassed. */
     private $ignoresecurity;
@@ -3571,7 +3571,7 @@ class curl {
      * @return bool
      */
     protected function request($url, $options = array()) {
-        // Reset here so that the data is valid when result returned from cache, or if we return due to a blacklist hit.
+        // Reset here so that the data is valid when result returned from cache, or if we return due to a blocked URL hit.
         $this->reset_request_state_vars();
 
         if ((defined('PHPUNIT_TEST') && PHPUNIT_TEST)) {
@@ -3581,8 +3581,8 @@ class curl {
             }
         }
 
-        // If curl security is enabled, check the URL against the blacklist before calling curl_exec.
-        // Note: This will only check the base url. In the case of redirects, the blacklist is also after the curl_exec.
+        // If curl security is enabled, check the URL against the list of blocked URLs before calling curl_exec.
+        // Note: This will only check the base url. In the case of redirects, the blocking check is also after the curl_exec.
         if (!$this->ignoresecurity && $this->securityhelper->url_is_blocked($url)) {
             $this->error = $this->securityhelper->get_blocked_url_string();
             return $this->error;
@@ -3605,7 +3605,7 @@ class curl {
         $this->errno = curl_errno($curl);
         // Note: $this->response and $this->rawresponse are filled by $hits->formatHeader callback.
 
-        // In the case of redirects (which curl blindly follows), check the post-redirect URL against the blacklist entries too.
+        // In the case of redirects (which curl blindly follows), check the post-redirect URL against the list of blocked list too.
         if (intval($this->info['redirect_count']) > 0 && !$this->ignoresecurity
             && $this->securityhelper->url_is_blocked($this->info['url'])) {
             $this->reset_request_state_vars();
diff --git a/lib/filestorage/tests/fixtures/passwordis1.zip b/lib/filestorage/tests/fixtures/passwordis1.zip
new file mode 100644 (file)
index 0000000..0c8c4f0
Binary files /dev/null and b/lib/filestorage/tests/fixtures/passwordis1.zip differ
index 860b8af..4b36265 100644 (file)
@@ -525,6 +525,24 @@ class core_files_zip_packer_testcase extends advanced_testcase implements file_p
         unlink($archive);
     }
 
+    /**
+     * Test opening an encrypted archive
+     */
+    public function test_open_encrypted_archive() {
+        $this->resetAfterTest();
+
+        // The archive contains a single encrypted "hello.txt" file.
+        $archive = __DIR__ . '/fixtures/passwordis1.zip';
+
+        /** @var zip_packer $packer */
+        $packer = get_file_packer('application/zip');
+        $result = $packer->extract_to_pathname($archive, make_temp_directory('zip'));
+
+        $this->assertIsArray($result);
+        $this->assertArrayHasKey('hello.txt', $result);
+        $this->assertEquals('Can not read file from zip archive', $result['hello.txt']);
+    }
+
     /**
      * Tests the progress reporting.
      */
index 2dcbe2b..fb74b3f 100644 (file)
@@ -285,7 +285,7 @@ class filetypes_util {
             $types = [];
 
             foreach ($groupinfo->extensions as $extension) {
-                if ($onlytypes && !$this->is_whitelisted($extension, $onlytypes)) {
+                if ($onlytypes && !$this->is_listed($extension, $onlytypes)) {
                     $group->selectable = false;
                     $group->expanded = true;
                     $group->ext = '';
@@ -328,7 +328,7 @@ class filetypes_util {
                 continue;
             }
             $extension = '.'.$extension;
-            if ($onlytypes && !$this->is_whitelisted($extension, $onlytypes)) {
+            if ($onlytypes && !$this->is_listed($extension, $onlytypes)) {
                 continue;
             }
             if (!isset($info['groups']) || empty($info['groups'])) {
@@ -416,35 +416,53 @@ class filetypes_util {
     }
 
     /**
-     * Should the given file type be considered as a part of the given whitelist.
+     * Should the file type be considered as a part of the given list.
+     *
+     * If multiple types are provided, all of them must be part of the list. Empty type is part of any list.
+     * Any type is part of an empty list.
+     *
+     * @param string|array $types File type or list of types to be checked.
+     * @param string|array $list An array or string listing the types to check against.
+     * @return boolean
+     */
+    public function is_listed($types, $list) {
+        return empty($this->get_not_listed($types, $list));
+    }
+
+    /**
+     * Should the given file type be considered as a part of the given list.
      *
      * If multiple types are provided, all of them must be part of the
-     * whitelist. Empty type is part of any whitelist. Any type is part of an
-     * empty whitelist.
+     * list. Empty type is part of any list. Any type is part of an
+     * empty list.
      *
-     * @param string|array $types File types to be checked
-     * @param string|array $whitelist An array or string of whitelisted types
+     * @deprecated since Moodle 3.10 MDL-69050 - please use {@see self::is_listed()} instead.
+     * @param string|array $types File type or list of types to be checked.
+     * @param string|array $list An array or string listing the types to check against.
      * @return boolean
      */
-    public function is_whitelisted($types, $whitelist) {
-        return empty($this->get_not_whitelisted($types, $whitelist));
+    public function is_whitelisted($types, $list) {
+
+        debugging('filetypes_util::is_whitelisted() is deprecated. Please use filetypes_util::is_listed() instead.',
+            DEBUG_DEVELOPER);
+
+        return $this->is_listed($types, $list);
     }
 
     /**
-     * Returns all types that are not part of the give whitelist.
+     * Returns all types that are not part of the givelist.
      *
-     * This is similar check to the {@link self::is_whitelisted()} but this one
-     * actually returns the extra types.
+     * This is similar check to the {@see self::is_listed()} but this one actually returns the extra types.
      *
-     * @param string|array $types File types to be checked
-     * @param string|array $whitelist An array or string of whitelisted types
-     * @return array Types not present in the whitelist
+     * @param string|array $types File type or list of types to be checked.
+     * @param string|array $list An array or string listing the types to check against.
+     * @return array Types not present in the list.
      */
-    public function get_not_whitelisted($types, $whitelist) {
+    public function get_not_listed($types, $list) {
 
-        $whitelistedtypes = $this->expand($whitelist, true, true);
+        $listedtypes = $this->expand($list, true, true);
 
-        if (empty($whitelistedtypes) || $whitelistedtypes == ['*']) {
+        if (empty($listedtypes) || $listedtypes == ['*']) {
             return [];
         }
 
@@ -454,22 +472,40 @@ class filetypes_util {
             return [];
         }
 
-        return array_diff($giventypes, $whitelistedtypes);
+        return array_diff($giventypes, $listedtypes);
+    }
+
+    /**
+     * Returns all types that are not part of the given list.
+     *
+     * This is similar check to the {@see self::is_listed()} but this one actually returns the extra types.
+     *
+     * @deprecated since Moodle 3.10 MDL-69050 - please use {@see self::get_not_whitelisted()} instead.
+     * @param string|array $types File type or list of types to be checked.
+     * @param string|array $list An array or string listing the types to check against.
+     * @return array Types not present in the list.
+     */
+    public function get_not_whitelisted($types, $list) {
+
+        debugging('filetypes_util::get_not_whitelisted() is deprecated. Please use filetypes_util::get_not_listed() instead.',
+            DEBUG_DEVELOPER);
+
+        return $this->get_not_listed($types, $list);
     }
 
     /**
      * Is the given filename of an allowed file type?
      *
-     * Empty whitelist is interpretted as "any file type is allowed" rather
+     * Empty allowlist is interpreted as "any file type is allowed" rather
      * than "no file can be uploaded".
      *
      * @param string $filename the file name
-     * @param string|array $whitelist list of allowed file extensions
+     * @param string|array $allowlist list of allowed file extensions
      * @return boolean True if the file type is allowed, false if not
      */
-    public function is_allowed_file_type($filename, $whitelist) {
+    public function is_allowed_file_type($filename, $allowlist) {
 
-        $allowedextensions = $this->expand($whitelist);
+        $allowedextensions = $this->expand($allowlist);
 
         if (empty($allowedextensions) || $allowedextensions == ['*']) {
             return true;
index 8ec10b0..59fb995 100644 (file)
@@ -328,9 +328,9 @@ class MoodleQuickForm_filemanager extends HTML_QuickForm_element implements temp
         }
 
         $filetypesutil = new \core_form\filetypes_util();
-        $whitelist = $filetypesutil->normalize_file_types($this->_options['accepted_types']);
+        $allowlist = $filetypesutil->normalize_file_types($this->_options['accepted_types']);
 
-        if (empty($whitelist) || $whitelist === ['*']) {
+        if (empty($allowlist) || $allowlist === ['*']) {
             // Any file type is allowed, nothing to check here.
             return;
         }
@@ -344,14 +344,14 @@ class MoodleQuickForm_filemanager extends HTML_QuickForm_element implements temp
         }
 
         foreach ($draftfiles as $file) {
-            if (!$filetypesutil->is_allowed_file_type($file->filename, $whitelist)) {
+            if (!$filetypesutil->is_allowed_file_type($file->filename, $allowlist)) {
                 $wrongfiles[] = $file->filename;
             }
         }
 
         if ($wrongfiles) {
             $a = array(
-                'whitelist' => implode(', ', $whitelist),
+                'allowlist' => implode(', ', $allowlist),
                 'wrongfiles' => implode(', ', $wrongfiles),
             );
             return get_string('err_wrongfileextension', 'core_form', $a);
index 35b08fe..e10fd46 100644 (file)
@@ -248,9 +248,9 @@ class MoodleQuickForm_filepicker extends HTML_QuickForm_input implements templat
     public function validateSubmitValue($value) {
 
         $filetypesutil = new \core_form\filetypes_util();
-        $whitelist = $filetypesutil->normalize_file_types($this->_options['accepted_types']);
+        $allowlist = $filetypesutil->normalize_file_types($this->_options['accepted_types']);
 
-        if (empty($whitelist) || $whitelist === ['*']) {
+        if (empty($allowlist) || $allowlist === ['*']) {
             // Any file type is allowed, nothing to check here.
             return;
         }
@@ -264,14 +264,14 @@ class MoodleQuickForm_filepicker extends HTML_QuickForm_input implements templat
         }
 
         foreach ($draftfiles->list as $file) {
-            if (!$filetypesutil->is_allowed_file_type($file->filename, $whitelist)) {
+            if (!$filetypesutil->is_allowed_file_type($file->filename, $allowlist)) {
                 $wrongfiles[] = $file->filename;
             }
         }
 
         if ($wrongfiles) {
             $a = array(
-                'whitelist' => implode(', ', $whitelist),
+                'allowlist' => implode(', ', $allowlist),
                 'wrongfiles' => implode(', ', $wrongfiles),
             );
             return get_string('err_wrongfileextension', 'core_form', $a);
index 9c70b5a..29838de 100644 (file)
@@ -242,10 +242,10 @@ class MoodleQuickForm_filetypes extends MoodleQuickForm_group {
 
         if ($this->onlytypes) {
             // Assert that all file types are allowed here.
-            $notwhitelisted = $this->util->get_not_whitelisted($value['filetypes'], $this->onlytypes);
+            $notlisted = $this->util->get_not_listed($value['filetypes'], $this->onlytypes);
 
-            if ($notwhitelisted) {
-                return get_string('filetypesnotwhitelisted', 'core_form', implode(', ', $notwhitelisted));
+            if ($notlisted) {
+                return get_string('filetypesnotallowed', 'core_form', implode(', ', $notlisted));
             }
         }
 
index 355fefe..623ca69 100644 (file)
@@ -207,67 +207,67 @@ class filetypes_util_testcase extends advanced_testcase {
     /**
      * Test checking that a type is among others.
      */
-    public function test_is_whitelisted() {
+    public function test_is_listed() {
 
         $this->resetAfterTest(true);
         $util = new filetypes_util();
 
         // These should be intuitively true.
-        $this->assertTrue($util->is_whitelisted('txt', 'text/plain'));
-        $this->assertTrue($util->is_whitelisted('txt', 'doc txt rtf'));
-        $this->assertTrue($util->is_whitelisted('.txt', '.doc;.txt;.rtf'));
-        $this->assertTrue($util->is_whitelisted('audio', 'text/plain audio video'));
-        $this->assertTrue($util->is_whitelisted('text/plain', 'text/plain audio video'));
-        $this->assertTrue($util->is_whitelisted('jpg jpe jpeg', 'image/jpeg'));
-        $this->assertTrue($util->is_whitelisted(['jpg', 'jpe', '.png'], 'image'));
+        $this->assertTrue($util->is_listed('txt', 'text/plain'));
+        $this->assertTrue($util->is_listed('txt', 'doc txt rtf'));
+        $this->assertTrue($util->is_listed('.txt', '.doc;.txt;.rtf'));
+        $this->assertTrue($util->is_listed('audio', 'text/plain audio video'));
+        $this->assertTrue($util->is_listed('text/plain', 'text/plain audio video'));
+        $this->assertTrue($util->is_listed('jpg jpe jpeg', 'image/jpeg'));
+        $this->assertTrue($util->is_listed(['jpg', 'jpe', '.png'], 'image'));
 
         // These should be intuitively false.
-        $this->assertFalse($util->is_whitelisted('.gif', 'text/plain'));
+        $this->assertFalse($util->is_listed('.gif', 'text/plain'));
 
         // Not all text/plain formats are in the document group.
-        $this->assertFalse($util->is_whitelisted('text/plain', 'document'));
+        $this->assertFalse($util->is_listed('text/plain', 'document'));
 
         // Not all documents (and also the group itself) is not a plain text.
-        $this->assertFalse($util->is_whitelisted('document', 'text/plain'));
+        $this->assertFalse($util->is_listed('document', 'text/plain'));
 
         // This may look wrong at the first sight as you might expect that the
         // mimetype should simply map to an extension ...
-        $this->assertFalse($util->is_whitelisted('image/jpeg', '.jpg'));
+        $this->assertFalse($util->is_listed('image/jpeg', '.jpg'));
 
         // But it is principally same situation as this (there is no 1:1 mapping).
-        $this->assertFalse($util->is_whitelisted('.c', '.txt'));
-        $this->assertTrue($util->is_whitelisted('.txt .c', 'text/plain'));
-        $this->assertFalse($util->is_whitelisted('text/plain', '.c'));
+        $this->assertFalse($util->is_listed('.c', '.txt'));
+        $this->assertTrue($util->is_listed('.txt .c', 'text/plain'));
+        $this->assertFalse($util->is_listed('text/plain', '.c'));
 
         // Any type is included if the filter is empty.
-        $this->assertTrue($util->is_whitelisted('txt', ''));
-        $this->assertTrue($util->is_whitelisted('txt', '*'));
+        $this->assertTrue($util->is_listed('txt', ''));
+        $this->assertTrue($util->is_listed('txt', '*'));
 
-        // Empty value is part of any whitelist.
-        $this->assertTrue($util->is_whitelisted('', '.txt'));
+        // Empty value is part of any list.
+        $this->assertTrue($util->is_listed('', '.txt'));
     }
 
     /**
-     * Test getting types not present in a whitelist.
+     * Test getting types not present in a list.
      */
-    public function test_get_not_whitelisted() {
+    public function test_get_not_listed() {
 
         $this->resetAfterTest(true);
         $util = new filetypes_util();
 
-        $this->assertEmpty($util->get_not_whitelisted('txt', 'text/plain'));
-        $this->assertEmpty($util->get_not_whitelisted('txt', '.doc .txt .rtf'));
-        $this->assertEmpty($util->get_not_whitelisted('txt', 'text/plain'));
-        $this->assertEmpty($util->get_not_whitelisted(['jpg', 'jpe', 'jpeg'], 'image/jpeg'));
-        $this->assertEmpty($util->get_not_whitelisted('', 'foo/bar'));
-        $this->assertEmpty($util->get_not_whitelisted('.foobar', ''));
-        $this->assertEmpty($util->get_not_whitelisted('.foobar', '*'));
+        $this->assertEmpty($util->get_not_listed('txt', 'text/plain'));
+        $this->assertEmpty($util->get_not_listed('txt', '.doc .txt .rtf'));
+        $this->assertEmpty($util->get_not_listed('txt', 'text/plain'));
+        $this->assertEmpty($util->get_not_listed(['jpg', 'jpe', 'jpeg'], 'image/jpeg'));
+        $this->assertEmpty($util->get_not_listed('', 'foo/bar'));
+        $this->assertEmpty($util->get_not_listed('.foobar', ''));
+        $this->assertEmpty($util->get_not_listed('.foobar', '*'));
 
         // Returned list is normalized so extensions have the dot added.
-        $this->assertContains('.exe', $util->get_not_whitelisted('exe', '.c .h'));
+        $this->assertContains('.exe', $util->get_not_listed('exe', '.c .h'));
 
-        // If this looks wrong to you, see {@link test_is_whitelisted()} for more details on this behaviour.
-        $this->assertContains('image/jpeg', $util->get_not_whitelisted('image/jpeg', '.jpg .jpeg'));
+        // If this looks wrong to you, see {@see self::test_is_listed()} for more details on this behaviour.
+        $this->assertContains('image/jpeg', $util->get_not_listed('image/jpeg', '.jpg .jpeg'));
     }
 
     /**
@@ -361,44 +361,44 @@ class filetypes_util_testcase extends advanced_testcase {
      */
     public function is_allowed_file_type_provider() {
         return [
-            'Filetype not in extension whitelist' => [
+            'Filetype not in extension list' => [
                 'filename' => 'test.xml',
-                'whitelist' => '.png .jpg',
+                'list' => '.png .jpg',
                 'expected' => false
             ],
-            'Filetype not in mimetype whitelist' => [
+            'Filetype not in mimetype list' => [
                 'filename' => 'test.xml',
-                'whitelist' => 'image/png',
+                'list' => 'image/png',
                 'expected' => false
             ],
-            'Filetype not in group whitelist' => [
+            'Filetype not in group list' => [
                 'filename' => 'test.xml',
-                'whitelist' => 'web_file',
+                'list' => 'web_file',
                 'expected' => false
             ],
-            'Filetype in whitelist as extension' => [
+            'Filetype in list as extension' => [
                 'filename' => 'test.xml',
-                'whitelist' => 'xml',
+                'list' => 'xml',
                 'expected' => true
             ],
-            'Empty whitelist should allow all' => [
+            'Empty list should allow all' => [
                 'filename' => 'test.xml',
-                'whitelist' => '',
+                'list' => '',
                 'expected' => true
             ],
-            'Filetype in whitelist but later on' => [
+            'Filetype in list but later on' => [
                 'filename' => 'test.xml',
-                'whitelist' => 'gif;jpeg,image/png xml xlsx',
+                'list' => 'gif;jpeg,image/png xml xlsx',
                 'expected' => true
             ],
-            'Filetype in whitelist as mimetype' => [
+            'Filetype in list as mimetype' => [
                 'filename' => 'test.xml',
-                'whitelist' => 'image/png application/xml',
+                'list' => 'image/png application/xml',
                 'expected' => true
             ],
-            'Filetype in whitelist as group' => [
+            'Filetype in list as group' => [
                 'filename' => 'test.html',
-                'whitelist' => 'video,web_file',
+                'list' => 'video,web_file',
                 'expected' => true
             ],
         ];
@@ -408,12 +408,12 @@ class filetypes_util_testcase extends advanced_testcase {
      * Test is_allowed_file_type().
      * @dataProvider is_allowed_file_type_provider
      * @param string $filename The filename to check
-     * @param string $whitelist The space , or ; separated list of types supported
+     * @param string $list The space , or ; separated list of types supported
      * @param boolean $expected The expected result. True if the file is allowed, false if not.
      */
-    public function test_is_allowed_file_type($filename, $whitelist, $expected) {
+    public function test_is_allowed_file_type($filename, $list, $expected) {
         $util = new filetypes_util();
-        $this->assertSame($expected, $util->is_allowed_file_type($filename, $whitelist));
+        $this->assertSame($expected, $util->is_allowed_file_type($filename, $list));
     }
 
     /**
@@ -484,4 +484,26 @@ class filetypes_util_testcase extends advanced_testcase {
         $util = new filetypes_util();
         $this->assertSame($expected, $util->get_unknown_file_types($filetypes));
     }
+
+    /**
+     * Test that a debugging noticed is displayed when calling is_whitelisted().
+     */
+    public function test_deprecation_is_whitelisted() {
+
+        $util = new filetypes_util();
+        $this->assertTrue($util->is_whitelisted('txt', 'text/plain'));
+        $this->assertDebuggingCalled('filetypes_util::is_whitelisted() is deprecated. ' .
+            'Please use filetypes_util::is_listed() instead.', DEBUG_DEVELOPER);
+    }
+
+    /**
+     * Test that a debugging noticed is displayed when calling get_not_whitelisted().
+     */
+    public function test_deprecation_get_not_whitelisted() {
+
+        $util = new filetypes_util();
+        $this->assertEmpty($util->get_not_whitelisted('txt', 'text/plain'));
+        $this->assertDebuggingCalled('filetypes_util::get_not_whitelisted() is deprecated. ' .
+            'Please use filetypes_util::get_not_listed() instead.', DEBUG_DEVELOPER);
+    }
 }
index d1270bc..8afd063 100644 (file)
@@ -5984,7 +5984,7 @@ function email_should_be_diverted($email) {
         return true;
     }
 
-    $patterns = array_map('trim', explode(',', $CFG->divertallemailsexcept));
+    $patterns = array_map('trim', preg_split("/[\s,]+/", $CFG->divertallemailsexcept));
     foreach ($patterns as $pattern) {
         if (preg_match("/$pattern/", $email)) {
             return false;
index dd9bd24..d81e43a 100644 (file)
@@ -129,7 +129,7 @@ class renderer_base {
                 // Don't allow the JavaScript helper to be executed from within another
                 // helper. If it's allowed it can be used by users to inject malicious
                 // JS into the page.
-                'blacklistednestedhelpers' => ['js']));
+                'disallowednestedhelpers' => ['js']));
 
         }
 
index 844cc74..94ec441 100644 (file)
@@ -102,7 +102,7 @@ class phpunit_util extends testing_util {
      * @return void
      */
     public static function reset_all_data($detectchanges = false) {
-        global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION, $FULLME;
+        global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION, $FULLME, $FILTERLIB_PRIVATE;
 
         // Stop any message redirection.
         self::stop_message_redirection();
@@ -202,6 +202,7 @@ class phpunit_util extends testing_util {
         $FULLME = null;
         $ME = null;
         $SCRIPT = null;
+        $FILTERLIB_PRIVATE = null;
 
         // Empty sessison and set fresh new not-logged-in user.
         \core\session\manager::init_empty_session();
index 53e4071..7b5ff60 100644 (file)
@@ -978,6 +978,32 @@ class core_completionlib_testcase extends advanced_testcase {
         $this->assertEventLegacyData($data, $event);
     }
 
+    /**
+     * Test course completed message.
+     */
+    public function test_course_completed_message() {
+        $this->setup_data();
+        $this->setAdminUser();
+
+        $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
+        $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
+
+        // Mark course as complete and get the message.
+        $sink = $this->redirectMessages();
+        $ccompletion->mark_complete();
+        $messages = $sink->get_messages();
+        $sink->close();
+
+        $this->assertCount(1, $messages);
+        $message = array_pop($messages);
+
+        $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom);
+        $this->assertEquals($this->user->id, $message->useridto);
+        $this->assertEquals('coursecompleted', $message->eventtype);
+        $this->assertEquals(get_string('coursecompleted', 'completion'), $message->subject);
+        $this->assertContains($this->course->fullname, $message->fullmessage);
+    }
+
     /**
      * Test course completed event.
      */
index f2691b7..d5a3d70 100644 (file)
@@ -78,7 +78,7 @@ class core_curl_security_helper_testcase extends advanced_testcase {
         ];
         // Format: url, blocked hosts, allowed ports, expected result.
         return [
-            // Base set without the blacklist enabled - no checking takes place.
+            // Base set without the blocklist enabled - no checking takes place.
             [$simpledns, "http://localhost/x.png", "", "", false],       // IP=127.0.0.1, Port=80 (port inferred from http).
             [$simpledns, "http://localhost:80/x.png", "", "", false],    // IP=127.0.0.1, Port=80 (specific port overrides http scheme).
             [$simpledns, "https://localhost/x.png", "", "", false],      // IP=127.0.0.1, Port=443 (port inferred from https).
@@ -136,7 +136,7 @@ class core_curl_security_helper_testcase extends advanced_testcase {
 
             // Test using multiple A records.
             // Multiple record DNS gives two IPs for the same host, we want to make
-            // sure that if we blacklist one of those (doesn't matter which one)
+            // sure that if we block one of those (doesn't matter which one)
             // the request is blocked.
             [$multiplerecorddns, "http://sub.example.com", '1.2.3.4', "", true],
             [$multiplerecorddns, "http://sub.example.com", '5.6.7.8', "", true],
@@ -284,8 +284,8 @@ class core_curl_security_helper_testcase extends advanced_testcase {
             ["80", "80\n443", false],
             [80, "80\n443", false],
             [443, "80\n443", false],
-            [0, "", true], // Port 0 and below are always invalid, even when the admin hasn't set whitelist entries.
-            [-1, "", true], // Port 0 and below are always invalid, even when the admin hasn't set whitelist entries.
+            [0, "", true], // Port 0 and below are always invalid, even when the admin hasn't set allowed entries.
+            [-1, "", true], // Port 0 and below are always invalid, even when the admin hasn't set allowed entries.
             [null, "", true], // Non-string, non-int values are invalid.
         ];
     }
index b334a3f..76250b2 100644 (file)
@@ -653,4 +653,97 @@ class core_datalib_testcase extends advanced_testcase {
             $this->assertInstanceOf('coding_exception', $e);
         }
     }
+
+    /**
+     * Test max courses in category
+     */
+    public function test_max_courses_in_category() {
+        global $CFG;
+        $this->resetAfterTest();
+
+        // Default settings.
+        $this->assertEquals(MAX_COURSES_IN_CATEGORY, get_max_courses_in_category());
+
+        // Misc category.
+        $misc = core_course_category::get_default();
+        $this->assertEquals(MAX_COURSES_IN_CATEGORY, $misc->sortorder);
+
+        $category1 = $this->getDataGenerator()->create_category();
+        $category2 = $this->getDataGenerator()->create_category();
+
+        // Check category sort orders.
+        $this->assertEquals(MAX_COURSES_IN_CATEGORY, core_course_category::get($misc->id)->sortorder);
+        $this->assertEquals(MAX_COURSES_IN_CATEGORY * 2, core_course_category::get($category1->id)->sortorder);
+        $this->assertEquals(MAX_COURSES_IN_CATEGORY * 3, core_course_category::get($category2->id)->sortorder);
+
+        // Create courses.
+        $course1 = $this->getDataGenerator()->create_course(['category' => $category1->id]);
+        $course2 = $this->getDataGenerator()->create_course(['category' => $category2->id]);
+        $course3 = $this->getDataGenerator()->create_course(['category' => $category1->id]);
+        $course4 = $this->getDataGenerator()->create_course(['category' => $category2->id]);
+
+        // Check course sort orders.
+        $this->assertEquals(MAX_COURSES_IN_CATEGORY * 2 + 2, get_course($course1->id)->sortorder);
+        $this->assertEquals(MAX_COURSES_IN_CATEGORY * 3 + 2, get_course($course2->id)->sortorder);
+        $this->assertEquals(MAX_COURSES_IN_CATEGORY * 2 + 1, get_course($course3->id)->sortorder);
+        $this->assertEquals(MAX_COURSES_IN_CATEGORY * 3 + 1, get_course($course4->id)->sortorder);
+
+        // Increase max course in category.
+        $CFG->maxcoursesincategory = 20000;
+        $this->assertEquals(20000, get_max_courses_in_category());
+
+        // The sort order has not yet fixed, these sort orders should be the same as before.
+        // Categories.
+        $this->assertEquals(MAX_COURSES_IN_CATEGORY, core_course_category::get($misc->id)->sortorder);
+        $this->assertEquals(MAX_COURSES_IN_CATEGORY * 2, core_course_category::get($category1->id)->sortorder);
+        $this->assertEquals(MAX_COURSES_IN_CATEGORY * 3, core_course_category::get($category2->id)->sortorder);
+        // Courses in category 1.
+        $this->assertEquals(MAX_COURSES_IN_CATEGORY * 2 + 2, get_course($course1->id)->sortorder);
+        $this->assertEquals(MAX_COURSES_IN_CATEGORY * 2 + 1, get_course($course3->id)->sortorder);
+        // Courses in category 2.
+        $this->assertEquals(MAX_COURSES_IN_CATEGORY * 3 + 2, get_course($course2->id)->sortorder);
+        $this->assertEquals(MAX_COURSES_IN_CATEGORY * 3 + 1, get_course($course4->id)->sortorder);
+
+        // Create new category so that the sort orders are applied.
+        $category3 = $this->getDataGenerator()->create_category();
+        // Categories.
+        $this->assertEquals(20000, core_course_category::get($misc->id)->sortorder);
+        $this->assertEquals(20000 * 2, core_course_category::get($category1->id)->sortorder);
+        $this->assertEquals(20000 * 3, core_course_category::get($category2->id)->sortorder);
+        $this->assertEquals(20000 * 4, core_course_category::get($category3->id)->sortorder);
+        // Courses in category 1.
+        $this->assertEquals(20000 * 2 + 2, get_course($course1->id)->sortorder);
+        $this->assertEquals(20000 * 2 + 1, get_course($course3->id)->sortorder);
+        // Courses in category 2.
+        $this->assertEquals(20000 * 3 + 2, get_course($course2->id)->sortorder);
+        $this->assertEquals(20000 * 3 + 1, get_course($course4->id)->sortorder);
+    }
+
+    /**
+     * Test debug message for max courses in category
+     */
+    public function test_debug_max_courses_in_category() {
+        global $CFG;
+        $this->resetAfterTest();
+
+        // Set to small value so that we can check the debug message.
+        $CFG->maxcoursesincategory = 3;
+        $this->assertEquals(3, get_max_courses_in_category());
+
+        $category1 = $this->getDataGenerator()->create_category();
+
+        // There is only one course, no debug message.
+        $this->getDataGenerator()->create_course(['category' => $category1->id]);
+        $this->assertDebuggingNotCalled();
+        // There are two courses, no debug message.
+        $this->getDataGenerator()->create_course(['category' => $category1->id]);
+        $this->assertDebuggingNotCalled();
+        // There is debug message when number of courses reaches the maximum number.
+        $this->getDataGenerator()->create_course(['category' => $category1->id]);
+        $this->assertDebuggingCalled("The number of courses (category id: $category1->id) has reached max number of courses " .
+            "in a category (" . get_max_courses_in_category() . "). It will cause a sorting performance issue. " .
+            "Please set higher value for \$CFG->maxcoursesincategory in config.php. " .
+            "Please also make sure \$CFG->maxcoursesincategory * MAX_COURSE_CATEGORIES less than max integer. " .
+            "See tracker issues: MDL-25669 and MDL-69573");
+    }
 }
index 0b89adb..c1fba03 100644 (file)
@@ -3297,6 +3297,26 @@ class core_moodlelib_testcase extends advanced_testcase {
                 ),
                 false,
             ),
+            'divertsexceptionsnewline' => array(
+                'divertallemailsto' => 'somewhere@elsewhere.com',
+                'divertallemailsexcept' => "@dev.com\nfred(\+.*)?@example.com",
+                array(
+                    'dev1@dev.com',
+                    'fred@example.com',
+                    'fred+verp@example.com',
+                ),
+                false,
+            ),
+            'alsodivertsnewline' => array(
+                'divertallemailsto' => 'somewhere@elsewhere.com',
+                'divertallemailsexcept' => "@dev.com\nfred(\+.*)?@example.com",
+                array(
+                    'foo@example.com',
+                    'test@real.com',
+                    'fred.jones@example.com',
+                ),
+                true,
+            ),
         );
     }
 
index aaac1e9..52e3976 100644 (file)
@@ -30,94 +30,94 @@ use core\output\mustache_helper_collection;
  */
 class core_output_mustache_helper_collection_testcase extends advanced_testcase {
     /**
-     * Test cases to confirm that blacklisted helpers are stripped from the source
+     * Test cases to confirm that disallowed helpers are stripped from the source
      * text by the helper before being passed to other another helper. This prevents
      * nested calls to helpers.
      */
-    public function get_strip_blacklisted_helpers_testcases() {
+    public function get_strip_disallowed_helpers_testcases() {
         return [
-            'no blacklist' => [
-                'blacklist' => [],
+            'no disallowed' => [
+                'disallowed' => [],
                 'input' => 'core, move, {{#js}} some nasty JS {{/js}}',
                 'expected' => 'core, move, {{#js}} some nasty JS {{/js}}'
             ],
-            'blacklist no match' => [
-                'blacklist' => ['foo'],
+            'disallowed no match' => [
+                'disallowed' => ['foo'],
                 'input' => 'core, move, {{#js}} some nasty JS {{/js}}',
                 'expected' => 'core, move, {{#js}} some nasty JS {{/js}}'
             ],
-            'blacklist partial match 1' => [
-                'blacklist' => ['js'],
+            'disallowed partial match 1' => [
+                'disallowed' => ['js'],
                 'input' => 'core, move, {{#json}} some nasty JS {{/json}}',
                 'expected' => 'core, move, {{#json}} some nasty JS {{/json}}'
             ],
-            'blacklist partial match 2' => [
-                'blacklist' => ['js'],
+            'disallowed partial match 2' => [
+                'disallowed' => ['js'],
                 'input' => 'core, move, {{#onjs}} some nasty JS {{/onjs}}',
                 'expected' => 'core, move, {{#onjs}} some nasty JS {{/onjs}}'
             ],
-            'single blacklist 1' => [
-                'blacklist' => ['js'],
+            'single disallowed 1' => [
+                'disallowed' => ['js'],
                 'input' => 'core, move, {{#js}} some nasty JS {{/js}}',
                 'expected' => 'core, move, {{}}'
             ],
-            'single blacklist 2' => [
-                'blacklist' => ['js'],
+            'single disallowed 2' => [
+                'disallowed' => ['js'],
                 'input' => 'core, move, {{ # js }} some nasty JS {{ /  js }}',
                 'expected' => 'core, move, {{}}'
             ],
-            'single blacklist 3' => [
-                'blacklist' => ['js'],
+            'single disallowed 3' => [
+                'disallowed' => ['js'],
                 'input' => 'core, {{#js}} some nasty JS {{/js}}, test',
                 'expected' => 'core, {{}}, test'
             ],
-            'single blacklist 3' => [
-                'blacklist' => ['js'],
+            'single disallowed 3' => [
+                'disallowed' => ['js'],
                 'input' => 'core, {{#ok}} this is ok {{/ok}}, {{#js}} some nasty JS {{/js}}',
                 'expected' => 'core, {{#ok}} this is ok {{/ok}}, {{}}'
             ],
-            'single blacklist multiple matches 1' => [
-                'blacklist' => ['js'],
+            'single disallowed multiple matches 1' => [
+                'disallowed' => ['js'],
                 'input' => 'core, {{#js}} some nasty JS {{/js}}, {{#js}} some nasty JS {{/js}}',
                 'expected' => 'core, {{}}'
             ],
-            'single blacklist multiple matches 2' => [
-                'blacklist' => ['js'],
+            'single disallowed multiple matches 2' => [
+                'disallowed' => ['js'],
                 'input' => 'core, {{ # js }} some nasty JS {{ /  js }}, {{ # js }} some nasty JS {{ /  js }}',
                 'expected' => 'core, {{}}'
             ],
-            'single blacklist multiple matches nested 1' => [
-                'blacklist' => ['js'],
+            'single disallowed multiple matches nested 1' => [
+                'disallowed' => ['js'],
                 'input' => 'core, move, {{#js}} some nasty JS {{#js}} some nasty JS {{/js}} {{/js}}',
                 'expected' => 'core, move, {{}}'
             ],
-            'single blacklist multiple matches nested 2' => [
-                'blacklist' => ['js'],
+            'single disallowed multiple matches nested 2' => [
+                'disallowed' => ['js'],
                 'input' => 'core, move, {{ # js }} some nasty JS {{ # js }} some nasty JS {{ /  js }}{{ /  js }}',
                 'expected' => 'core, move, {{}}'
             ],
-            'multiple blacklist 1' => [
-                'blacklist' => ['js', 'foo'],
+            'multiple disallowed 1' => [
+                'disallowed' => ['js', 'foo'],
                 'input' => 'core, move, {{#js}} some nasty JS {{/js}}',
                 'expected' => 'core, move, {{}}'
             ],
-            'multiple blacklist 2' => [
-                'blacklist' => ['js', 'foo'],
+            'multiple disallowed 2' => [
+                'disallowed' => ['js', 'foo'],
                 'input' => 'core, {{#foo}} blah {{/foo}}, {{#js}} js {{/js}}',
                 'expected' => 'core, {{}}, {{}}'
             ],
-            'multiple blacklist 3' => [
-                'blacklist' => ['js', 'foo'],
+            'multiple disallowed 3' => [
+                'disallowed' => ['js', 'foo'],
                 'input' => '{{#foo}} blah {{/foo}}, {{#foo}} blah {{/foo}}, {{#js}} js {{/js}}',
                 'expected' => '{{}}, {{}}'
             ],
-            'multiple blacklist 4' => [
-                'blacklist' => ['js', 'foo'],
+            'multiple disallowed 4' => [
+                'disallowed' => ['js', 'foo'],
                 'input' => '{{#foo}} blah {{/foo}}, {{#js}} js {{/js}}, {{#foo}} blah {{/foo}}',
                 'expected' => '{{}}'
             ],
-            'multiple blacklist 4' => [
-                'blacklist' => ['js', 'foo'],
+            'multiple disallowed 4' => [
+                'disallowed' => ['js', 'foo'],
                 'input' => 'core, move, {{#js}} JS {{#foo}} blah {{/foo}} {{/js}}',
                 'expected' => 'core, move, {{}}'
             ],
@@ -126,29 +126,29 @@ class core_output_mustache_helper_collection_testcase extends advanced_testcase
 
     /**
      * Test that the mustache_helper_collection class correctly strips
-     * @dataProvider get_strip_blacklisted_helpers_testcases()
-     * @param string[] $blacklist The list of helpers to strip
+     * @dataProvider get_strip_disallowed_helpers_testcases()
+     * @param string[] $disallowed The list of helpers to strip
      * @param string $input The input string for the helper
-     * @param string $expected The expected output of the string after blacklist strip
+     * @param string $expected The expected output of the string after disallowed strip
      */
-    public function test_strip_blacklisted_helpers($blacklist, $input, $expected) {
-        $collection = new mustache_helper_collection(null, $blacklist);
-        $this->assertEquals($expected, $collection->strip_blacklisted_helpers($blacklist, $input));
+    public function test_strip_disallowed_helpers($disallowed, $input, $expected) {
+        $collection = new mustache_helper_collection(null, $disallowed);
+        $this->assertEquals($expected, $collection->strip_disallowed_helpers($disallowed, $input));
     }
 
     /**
-     * Test that the blacklisted helpers are disabled during the execution of other
+     * Test that the disallowed helpers are disabled during the execution of other
      * helpers.
      *
-     * Any non-blacklisted helper should still be available to call during the
+     * Any allowed helper should still be available to call during the
      * execution of a helper.
      */
-    public function test_blacklisted_helpers_disabled_during_execution() {
+    public function test_disallowed_helpers_disabled_during_execution() {
         $engine = new \Mustache_Engine();
         $context = new \Mustache_Context();
         $lambdahelper = new \Mustache_LambdaHelper($engine, $context);
-        $blacklist = ['bad'];
-        $collection = new mustache_helper_collection(null, $blacklist);
+        $disallowed = ['bad'];
+        $collection = new mustache_helper_collection(null, $disallowed);
         $badcalled = false;
         $goodcalled = false;
 
@@ -174,4 +174,16 @@ class core_output_mustache_helper_collection_testcase extends advanced_testcase
         $this->assertTrue($goodcalled);
         $this->assertFalse($badcalled);
     }
+
+    /**
+     * Test that calling deprecated method strip_blacklisted_helpers() still works and shows developer debugging.
+     */
+    public function test_deprecated_strip_blacklisted_helpers() {
+
+        $collection = new mustache_helper_collection(null, ['js']);
+        $stripped = $collection->strip_blacklisted_helpers(['js'], '{{#js}} JS {{/js}}');
+        $this->assertEquals('{{}}', $stripped);
+        $this->assertDebuggingCalled('mustache_helper_collection::strip_blacklisted_helpers() is deprecated. ' .
+            'Please use mustache_helper_collection::strip_disallowed_helpers() instead.', DEBUG_DEVELOPER);
+    }
 }
index 1e1f294..c5fe41f 100644 (file)
@@ -48,6 +48,10 @@ information provided here is intended especially for developers.
 * The ZipStream-PHP library has been added to Moodle core in /lib/zipstream.
 * The php-enum library has been added to Moodle core in /lib/php-enum.
 * The http-message library has been added to Moodle core in /lib/http-message.
+* Methods `filetypes_util::is_whitelisted()` and `filetypes_util::get_not_whitelisted()` have been deprecated and
+  renamed to `is_listed()` and `get_not_listed()` respectively.
+* Method `mustache_helper_collection::strip_blacklisted_helpers()` has been deprecated and renamed to
+  `strip_disallowed_helpers()`.
 
 === 3.9 ===
 * Following function has been deprecated, please use \core\task\manager::run_from_cli().
index 62931e7..4b3ee39 100644 (file)
@@ -456,6 +456,11 @@ function upgrade_stale_php_files_present() {
     global $CFG;
 
     $someexamplesofremovedfiles = array(
+        // Removed in 3.10.
+        '/grade/grading/classes/privacy/gradingform_provider.php',
+        '/lib/coursecatlib.php',
+        '/lib/form/htmleditor.php',
+        '/message/classes/output/messagearea/contact.php',
         // Removed in 3.9.
         '/course/classes/output/modchooser_item.php',
         '/course/yui/build/moodle-course-modchooser/moodle-course-modchooser-min.js',
index 23d1bbc..bcc3261 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js and b/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js differ
index 23d1bbc..bcc3261 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js and b/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js differ
index 5b7d9ed..6e00d19 100644 (file)
@@ -22,16 +22,16 @@ EXCEPTION = function(c) {
     config.width = config.width || (M.cfg.developerdebug) ? Math.floor(Y.one(document.body).get('winWidth') / 3) + 'px' : null;
     config.closeButton = true;
 
-    // We need to whitelist some properties which are part of the exception
+    // We need to allow some properties which are part of the exception
     // prototype, otherwise AttributeCore filters them during value normalisation.
-    var whitelist = [
+    var allowlist = [
         'message',
         'name',
         'fileName',
         'lineNumber',
         'stack'
     ];
-    Y.Array.each(whitelist, function(k) {
+    Y.Array.each(allowlist, function(k) {
         config[k] = c[k];
     });
 
index 0358c40..32504c6 100644 (file)
@@ -1002,8 +1002,8 @@ class core_message_external extends external_api {
         return new external_single_structure(
             array(
                 'id' => new external_value(PARAM_INT, 'The conversation id'),
-                'name' => new external_value(PARAM_TEXT, 'The conversation name, if set', VALUE_DEFAULT, null),
-                'subname' => new external_value(PARAM_TEXT, 'A subtitle for the conversation name, if set', VALUE_DEFAULT, null),
+                'name' => new external_value(PARAM_RAW, 'The conversation name, if set', VALUE_DEFAULT, null),
+                'subname' => new external_value(PARAM_RAW, 'A subtitle for the conversation name, if set', VALUE_DEFAULT, null),
                 'imageurl' => new external_value(PARAM_URL, 'A link to the conversation picture, if set', VALUE_DEFAULT, null),
                 'type' => new external_value(PARAM_INT, 'The type of the conversation (1=individual,2=group,3=self)'),
                 'membercount' => new external_value(PARAM_INT, 'Total number of conversation members'),
@@ -1063,7 +1063,7 @@ class core_message_external extends external_api {
             array(
                 'id' => new external_value(PARAM_INT, 'Conversations id'),
                 'type' => new external_value(PARAM_INT, 'Conversation type: private or public'),
-                'name' => new external_value(PARAM_TEXT, 'Multilang compatible conversation name'. VALUE_OPTIONAL),
+                'name' => new external_value(PARAM_RAW, 'Multilang compatible conversation name'. VALUE_OPTIONAL),
                 'timecreated' => new external_value(PARAM_INT, 'The timecreated timestamp for the conversation'),
             ), 'information about conversation', VALUE_OPTIONAL),
             'Conversations between users', VALUE_OPTIONAL
index 406465c..03a7a09 100644 (file)
@@ -729,7 +729,7 @@ function core_message_can_edit_message_profile($user) {
 }
 
 /**
- * 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()}
  *
index 35f3150..4fd7e6e 100644 (file)
@@ -4242,6 +4242,54 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->assertCount(0, $conversations);
     }
 
+    /**
+     * Test that group conversations containing MathJax don't break the WebService.
+     */
+    public function test_get_conversations_group_with_mathjax() {
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        // Enable MathJax filter in content and headings.
+        $this->configure_filters([
+            ['name' => 'mathjaxloader', 'state' => TEXTFILTER_ON, 'move' => -1, 'applytostrings' => true],
+        ]);
+
+        // Create some users, a course and a group with a linked conversation.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $coursename = 'Course $$(a+b)=2$$';
+        $groupname = 'Group $$(a+b)=2$$';
+        $course1 = $this->getDataGenerator()->create_course(['shortname' => $coursename]);
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
+        $group1 = $this->getDataGenerator()->create_group([
+            'name' => $groupname,
+            'courseid' => $course1->id,
+            'enablemessaging' => 1,
+        ]);
+
+        // Add users to group1.
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user1->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user2->id));
+
+        // Call the WebService.
+        $result = core_message_external::get_conversations($user1->id, 0, 20, null, false);
+        $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
+ &nb