Merge branch 'MDL-65074' of https://github.com/hitteshahuja/moodle into master
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 5 Oct 2020 21:12:01 +0000 (23:12 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 5 Oct 2020 21:12:01 +0000 (23:12 +0200)
171 files changed:
admin/cli/svgtool.php
admin/settings/server.php
admin/tool/dataprivacy/classes/external.php
admin/tool/dataprivacy/classes/output/renderer.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
contentbank/classes/contentbank.php
contentbank/edit.php
contentbank/index.php
contentbank/tests/contentbank_test.php
contentbank/upload.php
course/classes/category.php
course/classes/local/service/content_item_service.php
course/externallib.php
course/lib.php
course/tests/externallib_test.php
course/upgrade.txt
enrol/externallib.php
enrol/tests/externallib_test.php
filter/algebra/filter.php
filter/tex/lib.php
grade/report/singleview/classes/local/screen/screen.php
grade/report/singleview/js/singleview.js
grade/report/singleview/lang/en/gradereport_singleview.php
h5p/ajax.php
h5p/classes/editor_framework.php
h5p/classes/framework.php
h5p/classes/output/renderer.php
h5p/classes/player.php
h5p/tests/framework_test.php
install/lang/ar/error.php
install/lang/ar/moodle.php
lang/en/admin.php
lang/en/completion.php
lang/en/contentbank.php
lang/en/deprecated.txt
lang/en/form.php
lang/en/moodle.php
lang/en/repository.php
lib/adminlib.php
lib/amd/build/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/book/db/install.xml
mod/book/db/upgrade.php
mod/book/version.php
mod/data/index.php
mod/data/tests/behat/data_activities.feature [new file with mode: 0644]
mod/folder/lib.php
mod/forum/externallib.php
mod/lesson/essay.php
mod/lesson/locallib.php
mod/lesson/mod_form.php
mod/lesson/pagetypes/essay.php
mod/lesson/pagetypes/matching.php
mod/lesson/pagetypes/multichoice.php
mod/lesson/pagetypes/numerical.php
mod/lesson/pagetypes/shortanswer.php
mod/lesson/pagetypes/truefalse.php
mod/lesson/tests/behat/lesson_navigation.feature
mod/lesson/tests/locallib_test.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/url/lib.php
mod/workshop/locallib.php
question/type/essay/backup/moodle2/backup_qtype_essay_plugin.class.php
question/type/essay/db/install.xml
question/type/essay/db/upgrade.php
question/type/essay/edit_essay_form.php
question/type/essay/lang/en/qtype_essay.php
question/type/essay/question.php
question/type/essay/questiontype.php
question/type/essay/renderer.php
question/type/essay/styles.css
question/type/essay/tests/behat/max_file_size.feature [new file with mode: 0644]
question/type/essay/tests/helper.php
question/type/essay/version.php
repository/draftfiles_ajax.php
theme/boost/scss/moodle/admin.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
user/editlib.php
version.php
webservice/lib.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 341bc8a..8032906 100644 (file)
@@ -63,7 +63,6 @@ class renderer extends plugin_renderer_base {
         $params = [
             'data-action' => 'contactdpo',
             'data-replytoemail' => $replytoemail,
-            'class' => 'contactdpo'
         ];
         return html_writer::link('#', get_string('contactdataprotectionofficer', 'tool_dataprivacy'), $params);
     }
index f37f62a..eb0eaba 100644 (file)
@@ -142,7 +142,7 @@ function tool_dbtransfer_get_drivers() {
         $dblibrary = $matches[2];
 
         if ($dbtype === 'sqlite3') {
-            // Blacklist unfinished drivers.
+            // The sqlite3 driver is not fully working yet and should not be returned.
             continue;
         }
 
index 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 1e5c934..9152067 100644 (file)
@@ -36,6 +36,10 @@ use context;
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class contentbank {
+
+    /** @var array All the context levels allowed in the content bank */
+    private const ALLOWED_CONTEXT_LEVELS = [CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE];
+
     /** @var array Enabled content types. */
     private $enabledcontenttypes = null;
 
@@ -348,4 +352,14 @@ class contentbank {
         $contentclass = "\\$record->contenttype\\content";
         return new $contentclass($record);
     }
+
+    /**
+     * Whether the context is allowed.
+     *
+     * @param context $context Context to check.
+     * @return bool
+     */
+    public function is_context_allowed(context $context): bool {
+        return in_array($context->contextlevel, self::ALLOWED_CONTEXT_LEVELS);
+    }
 }
index cdddcd4..6d0c58d 100644 (file)
@@ -30,6 +30,12 @@ $contextid = required_param('contextid', PARAM_INT);
 $pluginname = required_param('plugin', PARAM_PLUGIN);
 $id = optional_param('id', null, PARAM_INT);
 $context = context::instance_by_id($contextid, MUST_EXIST);
+
+$cb = new \core_contentbank\contentbank();
+if (!$cb->is_context_allowed($context)) {
+    print_error('contextnotallowed', 'core_contentbank');
+}
+
 require_capability('moodle/contentbank:access', $context);
 
 $returnurl = new \moodle_url('/contentbank/view.php', ['id' => $id]);
index 33eff29..c4d2242 100644 (file)
@@ -30,6 +30,11 @@ $contextid    = optional_param('contextid', \context_system::instance()->id, PAR
 $search = optional_param('search', '', PARAM_CLEAN);
 $context = context::instance_by_id($contextid, MUST_EXIST);
 
+$cb = new \core_contentbank\contentbank();
+if (!$cb->is_context_allowed($context)) {
+    print_error('contextnotallowed', 'core_contentbank');
+}
+
 require_capability('moodle/contentbank:access', $context);
 
 $statusmsg = optional_param('statusmsg', '', PARAM_ALPHANUMEXT);
@@ -47,7 +52,6 @@ $PAGE->set_heading($title);
 $PAGE->set_pagetype('contentbank');
 
 // Get all contents managed by active plugins where the user has permission to render them.
-$cb = new \core_contentbank\contentbank();
 $contenttypes = [];
 $enabledcontenttypes = $cb->get_enabled_content_types();
 foreach ($enabledcontenttypes as $contenttypename) {
index 3d6a703..9ca729a 100644 (file)
@@ -631,4 +631,76 @@ class core_contentbank_testcase extends advanced_testcase {
         $this->expectException(Exception::class);
         $cb->get_content_from_id(0);
     }
+
+    /**
+     * Test the behaviour of is_context_allowed().
+     *
+     * @dataProvider context_provider
+     * @param  \Closure $getcontext Get the context to check.
+     * @param  bool $expectedresult Expected result.
+     *
+     * @covers ::is_context_allowed
+     */
+    public function test_is_context_allowed(\Closure $getcontext, bool $expectedresult): void {
+        $this->resetAfterTest();
+
+        $cb = new contentbank();
+        $context = $getcontext();
+        $this->assertEquals($expectedresult, $cb->is_context_allowed($context));
+    }
+
+    /**
+     * Data provider for test_is_context_allowed().
+     *
+     * @return array
+     */
+    public function context_provider(): array {
+
+        return [
+            'System context' => [
+                function (): \context {
+                    return \context_system::instance();
+                },
+                true,
+            ],
+            'User context' => [
+                function (): \context {
+                    $user = $this->getDataGenerator()->create_user();
+                    return \context_user::instance($user->id);
+                },
+                false,
+            ],
+            'Course category context' => [
+                function (): \context {
+                    $coursecat = $this->getDataGenerator()->create_category();
+                    return \context_coursecat::instance($coursecat->id);
+                },
+                true,
+            ],
+            'Course context' => [
+                function (): \context {
+                    $course = $this->getDataGenerator()->create_course();
+                    return \context_course::instance($course->id);
+                },
+                true,
+            ],
+            'Module context' => [
+                function (): \context {
+                    $course = $this->getDataGenerator()->create_course();
+                    $module = $this->getDataGenerator()->create_module('page', ['course' => $course->id]);
+                    return \context_module::instance($module->cmid);
+                },
+                false,
+            ],
+            'Block context' => [
+                function (): \context {
+                    $course = $this->getDataGenerator()->create_course();
+                    $coursecontext = context_course::instance($course->id);
+                    $block = $this->getDataGenerator()->create_block('online_users', ['parentcontextid' => $coursecontext->id]);
+                    return \context_block::instance($block->id);
+                },
+                false,
+            ],
+        ];
+    }
 }
index 4410de4..81a7870 100644 (file)
@@ -32,9 +32,12 @@ require_login();
 $contextid = optional_param('contextid', \context_system::instance()->id, PARAM_INT);
 $context = context::instance_by_id($contextid, MUST_EXIST);
 
-require_capability('moodle/contentbank:upload', $context);
-
 $cb = new \core_contentbank\contentbank();
+if (!$cb->is_context_allowed($context)) {
+    print_error('contextnotallowed', 'core_contentbank');
+}
+
+require_capability('moodle/contentbank:upload', $context);
 
 $id = optional_param('id', null, PARAM_INT);
 if ($id) {
index 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 20209ae..ca60efb 100644 (file)
@@ -138,8 +138,12 @@ class content_item_service {
             // Add any subplugins to the list of item types.
             $subplugins = $pluginmanager->get_subplugins_of_plugin('mod_' . $plugin->name);
             foreach ($subplugins as $subpluginname => $subplugininfo) {
-                if (component_callback_exists($subpluginname, 'get_course_content_items')) {
-                    $itemtypes[] = $prefix . $subpluginname;
+                try {
+                    if (component_callback_exists($subpluginname, 'get_course_content_items')) {
+                        $itemtypes[] = $prefix . $subpluginname;
+                    }
+                } catch (\moodle_exception $e) {
+                    debugging('Cannot get_course_content_items: ' . $e->getMessage(), DEBUG_DEVELOPER);
                 }
             }
         }
index 5a896e7..1f631b2 100644 (file)
@@ -261,6 +261,7 @@ class core_course_external extends external_api {
                         $module['id'] = $cm->id;
                         $module['name'] = external_format_string($cm->name, $modcontext->id);
                         $module['instance'] = $cm->instance;
+                        $module['contextid'] = $modcontext->id;
                         $module['modname'] = (string) $cm->modname;
                         $module['modplural'] = (string) $cm->modplural;
                         $module['modicon'] = $cm->get_icon_url()->out(false);
@@ -426,7 +427,7 @@ class core_course_external extends external_api {
             new external_single_structure(
                 array(
                     'id' => new external_value(PARAM_INT, 'Section ID'),
-                    'name' => new external_value(PARAM_TEXT, 'Section name'),
+                    'name' => new external_value(PARAM_RAW, 'Section name'),
                     'visible' => new external_value(PARAM_INT, 'is the section visible', VALUE_OPTIONAL),
                     'summary' => new external_value(PARAM_RAW, 'Section description'),
                     'summaryformat' => new external_format_value('summary'),
@@ -442,6 +443,7 @@ class core_course_external extends external_api {
                                     'url' => new external_value(PARAM_URL, 'activity url', VALUE_OPTIONAL),
                                     'name' => new external_value(PARAM_RAW, 'activity module name'),
                                     'instance' => new external_value(PARAM_INT, 'instance id', VALUE_OPTIONAL),
+                                    'contextid' => new external_value(PARAM_INT, 'Activity context id.', VALUE_OPTIONAL),
                                     'description' => new external_value(PARAM_RAW, 'activity description', VALUE_OPTIONAL),
                                     'visible' => new external_value(PARAM_INT, 'is the module visible', VALUE_OPTIONAL),
                                     'uservisible' => new external_value(PARAM_BOOL, 'Is the module visible for the user?',
@@ -609,6 +611,7 @@ class core_course_external extends external_api {
                     $courseinfo['customfields'][] = [
                         'type' => $data->get_type(),
                         'value' => $data->get_value(),
+                        'valueraw' => $data->get_data_controller()->get_value(),
                         'name' => $data->get_name(),
                         'shortname' => $data->get_shortname()
                     ];
@@ -669,12 +672,12 @@ class core_course_external extends external_api {
                 new external_single_structure(
                         array(
                             'id' => new external_value(PARAM_INT, 'course id'),
-                            'shortname' => new external_value(PARAM_TEXT, 'course short name'),
+                            'shortname' => new external_value(PARAM_RAW, 'course short name'),
                             'categoryid' => new external_value(PARAM_INT, 'category id'),
                             'categorysortorder' => new external_value(PARAM_INT,
                                     'sort order into the category', VALUE_OPTIONAL),
-                            'fullname' => new external_value(PARAM_TEXT, 'full name'),
-                            'displayname' => new external_value(PARAM_TEXT, 'course display name'),
+                            'fullname' => new external_value(PARAM_RAW, 'full name'),
+                            'displayname' => new external_value(PARAM_RAW, 'course display name'),
                             'idnumber' => new external_value(PARAM_RAW, 'id number', VALUE_OPTIONAL),
                             'summary' => new external_value(PARAM_RAW, 'summary'),
                             'summaryformat' => new external_format_value('summary'),
@@ -729,10 +732,11 @@ 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...'),
+                                     'valueraw' => new external_value(PARAM_RAW, 'The raw value of the custom field'),
                                      'value' => new external_value(PARAM_RAW, 'The value of the custom field')]
                                 ), 'Custom fields and associated values', VALUE_OPTIONAL),
                         ), 'course'
@@ -939,7 +943,7 @@ class core_course_external extends external_api {
             new external_single_structure(
                 array(
                     'id'       => new external_value(PARAM_INT, 'course id'),
-                    'shortname' => new external_value(PARAM_TEXT, 'short name'),
+                    'shortname' => new external_value(PARAM_RAW, 'short name'),
                 )
             )
         );
@@ -1485,7 +1489,7 @@ class core_course_external extends external_api {
         return new external_single_structure(
             array(
                 'id'       => new external_value(PARAM_INT, 'course id'),
-                'shortname' => new external_value(PARAM_TEXT, 'short name'),
+                'shortname' => new external_value(PARAM_RAW, 'short name'),
             )
         );
     }
@@ -1963,7 +1967,7 @@ class core_course_external extends external_api {
             new external_single_structure(
                 array(
                     'id' => new external_value(PARAM_INT, 'category id'),
-                    'name' => new external_value(PARAM_TEXT, 'category name'),
+                    'name' => new external_value(PARAM_RAW, 'category name'),
                     'idnumber' => new external_value(PARAM_RAW, 'category id number', VALUE_OPTIONAL),
                     'description' => new external_value(PARAM_RAW, 'category description'),
                     'descriptionformat' => new external_format_value('description'),
@@ -2069,7 +2073,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'),
                 )
             )
         );
@@ -2487,6 +2491,7 @@ class core_course_external extends external_api {
                 $coursereturns['customfields'][] = [
                     'type' => $data->get_type(),
                     'value' => $data->get_value(),
+                    'valueraw' => $data->get_data_controller()->get_value(),
                     'name' => $data->get_name(),
                     'shortname' => $data->get_shortname()
                 ];
@@ -2607,11 +2612,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'),
@@ -2638,6 +2643,7 @@ class core_course_external extends external_api {
                             'The shortname of the custom field - to be able to build the field class in the code'),
                         'type'  => new external_value(PARAM_ALPHANUMEXT,
                             'The type of the custom field - text field, checkbox...'),
+                        'valueraw' => new external_value(PARAM_RAW, 'The raw value of the custom field'),
                         'value' => new external_value(PARAM_RAW, 'The value of the custom field'),
                     )
                 ), 'Custom fields', VALUE_OPTIONAL),
@@ -2861,7 +2867,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 57e6108..dcd48ee 100644 (file)
@@ -775,8 +775,16 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
                     array('name' => 'coursedisplay', 'value' => $dbcourse->coursedisplay),
                 ));
             }
-            if ($dbcourse->id == 4) {
-                $this->assertEquals($course['customfields'], [array_merge($customfield, $customfieldvalue)]);
+
+            // Assert custom field that we previously added to test course 4.
+            if ($dbcourse->id == $course4->id) {
+                $this->assertEquals([
+                    'shortname' => $customfield['shortname'],
+                    'name' => $customfield['name'],
+                    'type' => $customfield['type'],
+                    'value' => $customfieldvalue['value'],
+                    'valueraw' => $customfieldvalue['value'],
+                ], $course['customfields'][0]);
             }
         }
 
@@ -789,6 +797,49 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals($DB->count_records('course'), count($courses));
     }
 
+    /**
+     * Test retrieving courses returns custom field data
+     */
+    public function test_get_courses_customfields(): void {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
+        $datefield = $this->getDataGenerator()->create_custom_field([
+            'categoryid' => $fieldcategory->get('id'),
+            'shortname' => 'mydate',
+            'name' => 'My date',
+            'type' => 'date',
+        ]);
+
+        $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
+            [
+                'shortname' => $datefield->get('shortname'),
+                'value' => 1580389200, // 30/01/2020 13:00 GMT.
+            ],
+        ]]);
+
+        $courses = external_api::clean_returnvalue(
+            core_course_external::get_courses_returns(),
+            core_course_external::get_courses(['ids' => [$newcourse->id]])
+        );
+
+        $this->assertCount(1, $courses);
+        $course = reset($courses);
+
+        $this->assertArrayHasKey('customfields', $course);
+        $this->assertCount(1, $course['customfields']);
+
+        // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
+        $this->assertEquals([
+            'name' => $datefield->get('name'),
+            'shortname' => $datefield->get('shortname'),
+            'type' => $datefield->get('type'),
+            'value' => userdate(1580389200),
+            'valueraw' => 1580389200,
+        ], reset($course['customfields']));
+    }
+
     /**
      * Test get_courses without capability
      */
@@ -912,6 +963,49 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $results = core_course_external::search_courses('blocklist', $blockid);
     }
 
+    /**
+     * Test searching for courses returns custom field data
+     */
+    public function test_search_courses_customfields(): void {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
+        $datefield = $this->getDataGenerator()->create_custom_field([
+            'categoryid' => $fieldcategory->get('id'),
+            'shortname' => 'mydate',
+            'name' => 'My date',
+            'type' => 'date',
+        ]);
+
+        $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
+            [
+                'shortname' => $datefield->get('shortname'),
+                'value' => 1580389200, // 30/01/2020 13:00 GMT.
+            ],
+        ]]);
+
+        $result = external_api::clean_returnvalue(
+            core_course_external::search_courses_returns(),
+            core_course_external::search_courses('search', $newcourse->shortname)
+        );
+
+        $this->assertCount(1, $result['courses']);
+        $course = reset($result['courses']);
+
+        $this->assertArrayHasKey('customfields', $course);
+        $this->assertCount(1, $course['customfields']);
+
+        // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
+        $this->assertEquals([
+            'name' => $datefield->get('name'),
+            'shortname' => $datefield->get('shortname'),
+            'type' => $datefield->get('type'),
+            'value' => userdate(1580389200),
+            'valueraw' => 1580389200,
+        ], reset($course['customfields']));
+    }
+
     /**
      * Create a course with contents
      * @return array A list with the course object and course modules objects
@@ -1015,6 +1109,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
                     array('noclean' => true, 'para' => false, 'filter' => false));
                 $this->assertEquals($formattedtext, $module['description']);
                 $this->assertEquals($forumcm->instance, $module['instance']);
+                $this->assertEquals(context_module::instance($forumcm->id)->id, $module['contextid']);
                 $this->assertContains('1 unread post', $module['afterlink']);
                 $this->assertFalse($module['noviewlink']);
                 $this->assertNotEmpty($module['description']);  // Module showdescription is on.
@@ -1025,6 +1120,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
                     array('noclean' => true, 'para' => false, 'filter' => false));
                 $this->assertEquals($formattedtext, $module['description']);
                 $this->assertEquals($labelcm->instance, $module['instance']);
+                $this->assertEquals(context_module::instance($labelcm->id)->id, $module['contextid']);
                 $this->assertTrue($module['noviewlink']);
                 $this->assertNotEmpty($module['description']);  // Label always prints the description.
                 $testexecuted = $testexecuted + 1;
@@ -2451,8 +2547,13 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $this->assertCount(1, $result['courses']);
         $this->assertEquals($course2->id, $result['courses'][0]['id']);
         // Check custom fields properly returned.
-        unset($customfield['categoryid']);
-        $this->assertEquals([array_merge($customfield, $customfieldvalue)], $result['courses'][0]['customfields']);
+        $this->assertEquals([
+            'shortname' => $customfield['shortname'],
+            'name' => $customfield['name'],
+            'type' => $customfield['type'],
+            'value' => $customfieldvalue['value'],
+            'valueraw' => $customfieldvalue['value'],
+        ], $result['courses'][0]['customfields'][0]);
 
         $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
@@ -2581,6 +2682,49 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $this->assertCount(0, $result['courses']);
     }
 
+    /**
+     * Test retrieving courses by field returns custom field data
+     */
+    public function test_get_courses_by_field_customfields(): void {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
+        $datefield = $this->getDataGenerator()->create_custom_field([
+            'categoryid' => $fieldcategory->get('id'),
+            'shortname' => 'mydate',
+            'name' => 'My date',
+            'type' => 'date',
+        ]);
+
+        $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
+            [
+                'shortname' => $datefield->get('shortname'),
+                'value' => 1580389200, // 30/01/2020 13:00 GMT.
+            ],
+        ]]);
+
+        $result = external_api::clean_returnvalue(
+            core_course_external::get_courses_by_field_returns(),
+            core_course_external::get_courses_by_field('id', $newcourse->id)
+        );
+
+        $this->assertCount(1, $result['courses']);
+        $course = reset($result['courses']);
+
+        $this->assertArrayHasKey('customfields', $course);
+        $this->assertCount(1, $course['customfields']);
+
+        // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
+        $this->assertEquals([
+            'name' => $datefield->get('name'),
+            'shortname' => $datefield->get('shortname'),
+            'type' => $datefield->get('type'),
+            'value' => userdate(1580389200),
+            'valueraw' => 1580389200,
+        ], reset($course['customfields']));
+    }
+
     public function test_get_courses_by_field_invalid_field() {
         $this->expectException('invalid_parameter_exception');
         $result = core_course_external::get_courses_by_field('zyx', 'x');
index 4af9f85..69bf923 100644 (file)
@@ -4,6 +4,9 @@ information provided here is intended especially for developers.
 === 3.10 ===
 
 * The function make_categories_options() has now been deprecated. Please use \core_course_category::make_categories_list() instead.
+* External function core_course_external::get_course_contents now returns a new field contextid with the module context id.
+* The core_course_external class methods get_courses(), get_courses_by_field() and search_courses() now return a "valueraw" property
+  for each custom course field, which contains the original/unformatted version of the custom field value.
 
 === 3.9 ===
 
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 891dce8..5be42fd 100644 (file)
@@ -268,6 +268,7 @@ abstract class screen {
             'requires' => array('base', 'dom', 'event', 'event-simulate', 'io-base')
         );
 
+        $PAGE->requires->string_for_js('overridenoneconfirm', 'gradereport_singleview');
         $PAGE->requires->js_init_call('M.gradereport_singleview.init', array(), false, $module);
     }
 
index 7ee45eb..e6f6a08 100644 (file)
@@ -1,6 +1,11 @@
 M.gradereport_singleview = {};
 
 M.gradereport_singleview.init = function(Y) {
+    if (this.initialised) {
+        return;
+    }
+    this.initialised = true;
+
     var getColumnIndex = function(cell) {
         var rowNode = cell.ancestor('tr');
         if (!rowNode || !cell) {
@@ -104,7 +109,24 @@ M.gradereport_singleview.init = function(Y) {
 
         link.on('click', function(e) {
             e.preventDefault();
-            Y.all('input[name^=' + type + ']').each(toggle(link.hasClass('all')));
+            var selectall = link.hasClass('all');
+            var self = this;
+            if ((type === 'override') && !selectall) {
+                Y.use('moodle-core-notification-confirm', function() {
+                    var confirm = new M.core.confirm({
+                        title:      M.util.get_string('confirm', 'moodle'),
+                        question:   M.util.get_string('overridenoneconfirm', 'gradereport_singleview'),
+                    });
+                    confirm.on('complete-yes', function() {
+                        confirm.hide();
+                        confirm.destroy();
+                        Y.all('input[name^=' + type + ']').each(toggle(link.hasClass('all')));
+                    }, self);
+                    confirm.show();
+                });
+            } else {
+                Y.all('input[name^=' + type + ']').each(toggle(link.hasClass('all')));
+            }
         });
     });
 
index e6a913b..d27feb9 100644 (file)
@@ -50,6 +50,7 @@ $string['override'] = 'Override';
 $string['overrideall'] = 'Override all grades';
 $string['overridefor'] = 'Override for {$a}';
 $string['overridenone'] = 'Do not override any grades';
+$string['overridenoneconfirm'] = 'You are trying to disable all grade overrides. After saving, all the previously overridden grades will be lost. Do you want to continue?';
 $string['pluginname'] = 'Single view';
 $string['privacy:metadata'] = 'The Grade single view report only shows data stored in other locations.';
 $string['savegrades'] = 'Saving grades';
index 2291e2f..929d518 100644 (file)
@@ -44,6 +44,9 @@ $action = required_param('action', PARAM_ALPHA);
 $factory = new factory();
 $editor = $factory->get_editor();
 
+// Set context to default system context.
+$PAGE->set_context(null);
+
 switch ($action) {
     // Load list of libraries or details for library.
     case 'libraries':
index 8d7f9b4..8f97a36 100644 (file)
@@ -265,7 +265,22 @@ class editor_framework implements H5peditorStorage {
      *     minorVersion as properties.
      */
     public function alterLibraryFiles(&$files, $libraries): void {
-        // This is to be implemented when the renderer is used.
+        global $PAGE;
+
+        // Refactor dependency list.
+        $librarylist = [];
+        foreach ($libraries as $dependency) {
+            $librarylist[$dependency['machineName']] = [
+                'majorVersion' => $dependency['majorVersion'],
+                'minorVersion' => $dependency['minorVersion']
+            ];
+        }
+
+        $renderer = $PAGE->get_renderer('core_h5p');
+
+        $embedtype = 'editor';
+        $renderer->h5p_alter_scripts($files['scripts'], $librarylist, $embedtype);
+        $renderer->h5p_alter_styles($files['styles'], $librarylist, $embedtype);
     }
 
     /**
index 4b05422..fa8fa6b 100644 (file)
@@ -1093,20 +1093,10 @@ class framework implements \H5PFrameworkInterface {
      * @param int $minorversion The library's minor version
      */
     public function alterLibrarySemantics(&$semantics, $name, $majorversion, $minorversion) {
-        global $DB;
+        global $PAGE;
 
-        $library = $DB->get_record('h5p_libraries',
-            array(
-                'machinename' => $name,
-                'majorversion' => $majorversion,
-                'minorversion' => $minorversion,
-            )
-        );
-
-        if ($library) {
-            $library->semantics = json_encode($semantics);
-            $DB->update_record('h5p_libraries', $library);
-        }
+        $renderer = $PAGE->get_renderer('core_h5p');
+        $renderer->h5p_alter_semantics($semantics, $name, $majorversion, $minorversion);
     }
 
     /**
index e5720c5..0d25f2a 100644 (file)
@@ -37,4 +37,49 @@ use plugin_renderer_base;
  */
 class renderer extends plugin_renderer_base {
 
-}
\ No newline at end of file
+    /**
+     * Alter which stylesheets are loaded for H5P.
+     * This is useful for adding custom styles or replacing existing ones.
+     *
+     * @param array|object $scripts List of stylesheets that will be loaded
+     * @param array $libraries Array of libraries indexed by the library's machineName
+     * @param string $embedtype Possible values: div, iframe, external, editor
+     */
+    public function h5p_alter_styles(&$scripts, array $libraries, string $embedtype) {
+    }
+
+    /**
+     * Alter which scripts are loaded for H5P.
+     * This is useful for adding custom scripts or replacing existing ones.
+     *
+     * @param array|object $scripts List of JavaScripts that will be loaded
+     * @param array $libraries Array of libraries indexed by the library's machineName
+     * @param string $embedtype Possible values: div, iframe, external, editor
+     */
+    public function h5p_alter_scripts(&$scripts, array $libraries, string $embedtype) {
+    }
+
+    /**
+     * Alter semantics before they are processed. This is useful for changing
+     * how the editor looks and how content parameters are filtered.
+     *
+     * @param object|object $semantics Semantics as object
+     * @param string $name Machine name of library
+     * @param int $majorversion Major version of library
+     * @param int $minorversion Minor version of library
+     */
+    public function h5p_alter_semantics(&$semantics, $name, $majorversion, $minorversion) {
+    }
+
+    /**
+     * Alter parameters of H5P content after it has been filtered through semantics.
+     * This is useful for adapting the content to the current context.
+     *
+     * @param array|object $parameters The content parameters for the library
+     * @param string $name The machine readable name of the library
+     * @param int $majorversion Major version of the library
+     * @param int $minorversion Minor version of the library
+     */
+    public function h5p_alter_filtered_parameters(&$parameters, string $name, int $majorversion, int $minorversion) {
+    }
+}
index a94d9b1..6647594 100644 (file)
@@ -357,7 +357,7 @@ class player {
         $cid = $this->get_cid();
         // The filterParameters function should be called before getting the dependencyfiles because it rebuild content
         // dependency cache and export file.
-        $settings['contents'][$cid]['jsonContent'] = $this->core->filterParameters($this->content);
+        $settings['contents'][$cid]['jsonContent'] = $this->get_filtered_parameters();
 
         $files = $this->get_dependency_files();
         if ($this->embedtype === 'div') {
@@ -398,15 +398,44 @@ class player {
         return $settings;
     }
 
+    /**
+     * Get filtered parameters, modifying them by the renderer if the theme implements the h5p_alter_filtered_parameters function.
+     *
+     * @return string Filtered parameters.
+     */
+    private function get_filtered_parameters(): string {
+        global $PAGE;
+
+        $safeparams = $this->core->filterParameters($this->content);
+        $decodedparams = json_decode($safeparams);
+        $h5poutput = $PAGE->get_renderer('core_h5p');
+        $h5poutput->h5p_alter_filtered_parameters(
+            $decodedparams,
+            $this->content['library']['name'],
+            $this->content['library']['majorVersion'],
+            $this->content['library']['minorVersion']
+        );
+        $safeparams = json_encode($decodedparams);
+
+        return $safeparams;
+    }
+
     /**
      * Finds library dependencies of view
      *
      * @return array Files that the view has dependencies to
      */
     private function get_dependency_files(): array {
+        global $PAGE;
+
         $preloadeddeps = $this->core->loadContentDependencies($this->h5pid, 'preloaded');
         $files = $this->core->getDependenciesFiles($preloadeddeps);
 
+        // Add additional asset files if required.
+        $h5poutput = $PAGE->get_renderer('core_h5p');
+        $h5poutput->h5p_alter_scripts($files['scripts'], $preloadeddeps, $this->embedtype);
+        $h5poutput->h5p_alter_styles($files['styles'], $preloadeddeps, $this->embedtype);
+
         return $files;
     }
 
index de4be46..1a2c18b 100644 (file)
@@ -1457,8 +1457,8 @@ class framework_testcase extends \advanced_testcase {
         // Get the semantics of 'Library1' from the DB.
         $currentsemantics = $DB->get_field('h5p_libraries', 'semantics', array('id' => $library1->id));
 
-        // The semantics for Library1 should be successfully updated.
-        $this->assertEquals(json_encode($updatedsemantics), $currentsemantics);
+        // The semantics for Library1 shouldn't be updated.
+        $this->assertEquals($semantics, $currentsemantics);
     }
 
     /**
index ceb7522..22a7ea1 100644 (file)
@@ -35,7 +35,7 @@ $string['cannotcreatedboninstall'] = '<p> لا يمكن إنشاء قاعدة ا
 <p> المسؤول عن الموقع يجب أن يتحقق من إعدادات قاعدة بيانات. </p>';
 $string['cannotcreatelangdir'] = 'لا يمكن إنشاء مجلد اللغة';
 $string['cannotcreatetempdir'] = 'لا يمكن إنشاء المجلد المؤقت';
-$string['cannotdownloadcomponents'] = 'لم يتم تحميل العناصر';
+$string['cannotdownloadcomponents'] = 'تعذر تنزيل المُكونات';
 $string['cannotdownloadzipfile'] = 'لم يتم تحميل الملف المضغوط';
 $string['cannotfindcomponent'] = 'لم يتم العثور على المكون';
 $string['cannotsavemd5file'] = 'لم يتم حفظ ملف  md5';
index 7c0ccda..8207932 100644 (file)
@@ -34,4 +34,4 @@ $string['language'] = 'اللغة';
 $string['moodlelogo'] = 'شعار مودل';
 $string['next'] = 'التالي';
 $string['previous'] = 'السابق';
-$string['reload'] = 'إعادة تحميل';
+$string['reload'] = 'إعادة Ø§Ù\84تحÙ\85Ù\8aÙ\84';
index 6cbb336..455a2cd 100644 (file)
@@ -499,6 +499,15 @@ $string['disabled'] = 'Disabled';
 $string['disableuserimages'] = 'Disable user profile images';
 $string['displayerrorswarning'] = 'Enabling the PHP setting <em>display_errors</em> is not recommended on production sites because some error messages may reveal sensitive information about your server.';
 $string['displayloginfailures'] = 'Display login failures';
+$string['divertallemails'] = 'Email diverting';
+$string['divertallemailsdetail'] = 'Used as a safeguard in development environments when testing emails and should not be used in production.';
+$string['divertallemailsexcept'] = 'Email diversion exceptions';
+$string['divertallemailsexcept_desc'] = 'A list of email exception rules separated by either commas or new lines. Each rule is interpreted as a regular expression, eg<pre>simone@acme.com
+.*@acme.com
+fred(\\+.*)?@acme.com
+</pre>';
+$string['divertallemailsto'] = 'Divert all emails';
+$string['divertallemailsto_desc'] = 'If set then all emails will be diverted to this single email address instead.';
 $string['dndallowtextandlinks'] = 'Drag and drop upload of text/links';
 $string['doclang'] = 'Language for docs';
 $string['docroot'] = 'Moodle Docs document root';
index 4091898..5a84a6f 100644 (file)
@@ -122,6 +122,7 @@ $string['courseaggregation_any'] = 'ANY selected courses to be completed';
 $string['coursealreadycompleted'] = 'You have already completed this course';
 $string['coursecomplete'] = 'Course complete';
 $string['coursecompleted'] = 'Course completed';
+$string['coursecompletedmessage'] = '<p>Congratulations!</p><p>You just completed the following course: <a href="{$a->courselink}">{$a->coursename}</a>.</p>';
 $string['coursecompletion'] = 'Course completion';
 $string['coursecompletioncondition'] = 'Condition: {$a}';
 $string['coursegrade'] = 'Course grade';
index 4e073a6..acbfc0d 100644 (file)
@@ -33,6 +33,7 @@ $string['contentrenamed'] = 'The content has been renamed.';
 $string['contentsmoved'] = 'Content bank contents moved to {$a}.';
 $string['contenttypenoaccess'] = 'You cannot view this {$a} instance.';
 $string['contenttypenoedit'] = 'You can not edit this content';
+$string['contextnotallowed'] = 'Context is not allowed';
 $string['emptynamenotallowed'] = 'Empty name is not allowed';
 $string['eventcontentcreated'] = 'Content created';
 $string['eventcontentdeleted'] = 'Content deleted';
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 7f33390..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;
         }
@@ -1217,6 +1217,9 @@ class external_settings {
     /** @var string The session lang */
     private $lang = '';
 
+    /** @var string The timezone to use during this WS request */
+    private $timezone = '';
+
     /**
      * Constructor - protected - can not be instanciated
      */
@@ -1337,6 +1340,24 @@ class external_settings {
     public function get_lang() {
         return $this->lang;
     }
+
+    /**
+     * Set timezone
+     *
+     * @param string $timezone
+     */
+    public function set_timezone($timezone) {
+        $this->timezone = $timezone;
+    }
+
+    /**
+     * Get timezone
+     *
+     * @return string
+     */
+    public function get_timezone() {
+        return $this->timezone;
+    }
 }
 
 /**
index fd0c43e..2ea5d61 100644 (file)
@@ -2234,23 +2234,27 @@ function readfile_accel($file, $mimetype, $accelerate) {
         }
     }
 
-    if ($filesize > 10000000) {
-        // for large files try to flush and close all buffers to conserve memory
-        while(@ob_get_level()) {
-            if (!@ob_end_flush()) {
-                break;
-            }
-        }
-    }
-
-    // Send this header after we have flushed the buffers so that if we fail
-    // later can remove this because it wasn't sent.
     header('Content-Length: ' . $filesize);
 
     if (!empty($_SERVER['REQUEST_METHOD']) and $_SERVER['REQUEST_METHOD'] === 'HEAD') {
         exit;
     }
 
+    while (ob_get_level()) {
+        $handlerstack = ob_list_handlers();
+        $activehandler = array_pop($handlerstack);
+        if ($activehandler === 'default output handler') {
+            // We do not expect any content in the buffer when we are serving files.
+            $buffercontents = ob_get_clean();
+            if ($buffercontents !== '') {
+                error_log('Non-empty default output handler buffer detected while serving the file ' . $file);
+            }
+        } else {
+            // Some handlers such as zlib output compression may have file signature buffered - flush it.
+            ob_end_flush();
+        }
+    }
+
     // send the whole file content
     if (is_object($file)) {
         $file->readfile();
@@ -3018,7 +3022,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 +3575,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 +3585,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 +3609,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 {
     }