Merge branch 'install_master' of https://git.in.moodle.com/amosbot/moodle-install
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Fri, 10 Jul 2020 18:21:38 +0000 (20:21 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Fri, 10 Jul 2020 18:21:38 +0000 (20:21 +0200)
83 files changed:
.travis.yml
admin/settings/security.php
admin/tasklogs.php
admin/tool/log/store/legacy/tests/store_test.php
admin/tool/lp/amd/build/dialogue.min.js
admin/tool/lp/amd/build/dialogue.min.js.map
admin/tool/lp/amd/build/grade_dialogue.min.js
admin/tool/lp/amd/build/grade_dialogue.min.js.map
admin/tool/lp/amd/build/grade_user_competency_inline.min.js
admin/tool/lp/amd/build/grade_user_competency_inline.min.js.map
admin/tool/lp/amd/src/dialogue.js
admin/tool/lp/amd/src/grade_dialogue.js
admin/tool/lp/amd/src/grade_user_competency_inline.js
admin/tool/mobile/classes/api.php
admin/tool/mobile/lang/en/tool_mobile.php
auth/oauth2/classes/auth.php
blocks/blog_menu/block_blog_menu.php
cache/upgrade.txt
calendar/amd/build/calendar_filter.min.js
calendar/amd/build/calendar_filter.min.js.map
calendar/amd/src/calendar_filter.js
enrol/manual/ajax.php
error/index.php
error/plainpage.php [new file with mode: 0644]
lang/en/admin.php
lang/en/error.php
lang/en/role.php
lib/adminlib.php
lib/behat/behat_base.php
lib/classes/form/error_feedback.php [new file with mode: 0644]
lib/classes/lock/db_record_lock_factory.php
lib/classes/lock/file_lock_factory.php
lib/classes/lock/installation_lock_factory.php
lib/classes/lock/lock.php
lib/classes/lock/lock_factory.php
lib/classes/lock/mysql_lock_factory.php
lib/classes/lock/postgres_lock_factory.php
lib/classes/output/icon_system.php
lib/classes/output/icon_system_fontawesome.php
lib/db/access.php
lib/db/renamedclasses.php
lib/deprecatedlib.php
lib/form/htmleditor.php [deleted file]
lib/formslib.php
lib/setuplib.php
lib/tablelib.php
lib/tests/behat/behat_deprecated.php
lib/tests/behat/behat_hooks.php
lib/tests/behat/behat_navigation.php
lib/tests/event_test.php
lib/tests/lock_test.php
lib/tests/setuplib_test.php
lib/upgrade.txt
lib/weblib.php
message/externallib.php
message/tests/externallib_test.php
message/tests/messagelib_test.php
mod/assign/feedback/editpdf/classes/task/convert_submissions.php
mod/lti/templates/tool_card.mustache
mod/scorm/report/basic/classes/report.php
mod/scorm/report/graphs/classes/report.php
mod/scorm/report/interactions/classes/report.php
mod/scorm/report/objectives/classes/report.php
question/type/ddimageortext/amd/build/question.min.js
question/type/ddimageortext/amd/build/question.min.js.map
question/type/ddimageortext/amd/src/question.js
question/type/ddmarker/amd/build/question.min.js
question/type/ddmarker/amd/build/question.min.js.map
question/type/ddmarker/amd/src/question.js
question/type/ddwtos/amd/build/ddwtos.min.js
question/type/ddwtos/amd/build/ddwtos.min.js.map
question/type/ddwtos/amd/src/ddwtos.js
report/competency/amd/build/grading_popup.min.js
report/competency/amd/build/grading_popup.min.js.map
report/competency/amd/build/user_course_navigation.min.js
report/competency/amd/build/user_course_navigation.min.js.map
report/competency/amd/src/grading_popup.js
report/competency/amd/src/user_course_navigation.js
theme/boost/amd/build/pending.min.js
theme/boost/amd/build/pending.min.js.map
theme/boost/amd/src/pending.js
theme/yui_combo.php
version.php

index f60032e..d751251 100644 (file)
@@ -15,6 +15,7 @@ dist: xenial
 
 services:
     - mysql
+    - docker
 
 php:
     # We only run the highest and lowest supported versions to reduce the load on travis-ci.org.
@@ -61,8 +62,9 @@ cache:
       - $HOME/.npm
 
 before_install:
+    - docker run -d -p 127.0.0.1:8080:80 --name exttests moodlehq/moodle-exttests
     # Avoid IPv6 default binding as service (causes redis not to start).
-    sudo service redis-server start --bind 127.0.0.1
+    sudo service redis-server start --bind 127.0.0.1
 
 install:
     - >
@@ -169,6 +171,10 @@ before_script:
           -e "/require_once/i \\\$CFG->phpunit_dataroot = '\/home\/travis\/roots\/phpunit';" \
           -e "/require_once/i \\\$CFG->phpunit_prefix = 'p_';" \
           config.php ;
+        # Enable test external resources
+        sed -i \
+          -e "/require_once/i \\define('TEST_EXTERNAL_FILES_HTTP_URL', 'http://127.0.0.1:8080');" \
+          config.php ;
         # Redis cache store tests
         sed -i \
           -e "/require_once/i \\define('TEST_CACHESTORE_REDIS_TESTSERVERS', '127.0.0.1');" \
@@ -248,6 +254,8 @@ script:
       if [ "$TASK" = 'PHPUNIT' ];
       then
         vendor/bin/phpunit --fail-on-risky --disallow-test-output --verbose;
+        EXTTESTS_HITS=$(docker logs exttests 2>&1 | grep -Fv -e 'AH00558' -e '[pid 1]' | wc -l)
+        echo -e "\nTest local resources number of hits: ${EXTTESTS_HITS}.\n"
       fi
 
     - >
index d8dc48a..77dfb42 100644 (file)
@@ -160,6 +160,23 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $temp->add(new admin_setting_configportlist('curlsecurityallowedport',
                new lang_string('curlsecurityallowedport', 'admin'),
                new lang_string('curlsecurityallowedportsyntax', 'admin'), ""));
+
+    // HTTP Header referrer policy settings.
+    $referreroptions = [
+        'default' => get_string('referrernone', 'admin'),
+        'no-referrer' => 'no-referrer',
+        'no-referrer-when-downgrade' => 'no-referrer-when-downgrade',
+        'origin' => 'origin',
+        'origin-when-cross-origin' => 'origin-when-cross-origin',
+        'same-origin' => 'same-origin',
+        'strict-origin' => 'strict-origin',
+        'strict-origin-when-cross-origin' => 'strict-origin-when-cross-origin',
+        'unsafe-url' => 'unsafe-url',
+    ];
+    $temp->add(new admin_setting_configselect('referrerpolicy',
+            new lang_string('referrerpolicy', 'admin'),
+            new lang_string('referrerpolicydesc', 'admin'), 'default', $referreroptions));
+
     $ADMIN->add('security', $temp);
 
     // "notifications" settingpage
index 796f323..a91faa3 100644 (file)
@@ -66,7 +66,7 @@ echo $OUTPUT->header();
 // Output the search form.
 echo $OUTPUT->render_from_template('core_admin/tasklogs', (object) [
     'action' => $pageurl->out(),
-    'filter' => $filter,
+    'filter' => htmlentities($filter),
     'resultfilteroptions' => [
         (object) [
             'value' => -1,
index a2aeb98..f0a95a3 100644 (file)
@@ -34,11 +34,9 @@ class logstore_legacy_store_testcase extends advanced_testcase {
 
         $this->setAdminUser();
         $user1 = $this->getDataGenerator()->create_user();
-        $user2 = $this->getDataGenerator()->create_user();
         $course1 = $this->getDataGenerator()->create_course();
         $module1 = $this->getDataGenerator()->create_module('resource', array('course' => $course1));
         $course2 = $this->getDataGenerator()->create_course();
-        $module2 = $this->getDataGenerator()->create_module('resource', array('course' => $course2));
 
         // Enable legacy logging plugin.
         set_config('enabled_stores', 'logstore_legacy', 'tool_log');
@@ -68,15 +66,8 @@ class logstore_legacy_store_testcase extends advanced_testcase {
             array('context' => context_course::instance($course2->id), 'other' => array('sample' => 6, 'xx' => 11)));
         $event2->trigger();
 
-        $this->setUser($user2);
-        add_to_log($course1->id, 'xxxx', 'yyyy', '', '7', 0, 0);
-        $this->assertDebuggingCalled();
-
-        add_to_log($course2->id, 'aaa', 'bbb', 'info.php', '666', $module2->cmid, $user1->id);
-        $this->assertDebuggingCalled();
-
         $logs = $DB->get_records('log', array(), 'id ASC');
-        $this->assertCount(4, $logs);
+        $this->assertCount(2, $logs);
 
         $log = array_shift($logs);
         $this->assertNotEmpty($log->id);
@@ -104,34 +95,6 @@ class logstore_legacy_store_testcase extends advanced_testcase {
         $this->assertSame('unittest.php?id=6', $log->url);
         $this->assertSame('bbb', $log->info);
 
-        $oldlogid = $log->id;
-        $log = array_shift($logs);
-        $this->assertGreaterThan($oldlogid, $log->id);
-        $this->assertNotEmpty($log->id);
-        $this->assertTimeCurrent($log->time);
-        $this->assertEquals($user2->id, $log->userid);
-        $this->assertSame('0.0.0.0', $log->ip);
-        $this->assertEquals($course1->id, $log->course);
-        $this->assertSame('xxxx', $log->module);
-        $this->assertEquals(0, $log->cmid);
-        $this->assertSame('yyyy', $log->action);
-        $this->assertSame('', $log->url);
-        $this->assertSame('7', $log->info);
-
-        $oldlogid = $log->id;
-        $log = array_shift($logs);
-        $this->assertGreaterThan($oldlogid, $log->id);
-        $this->assertNotEmpty($log->id);
-        $this->assertTimeCurrent($log->time);
-        $this->assertEquals($user1->id, $log->userid);
-        $this->assertSame('0.0.0.0', $log->ip);
-        $this->assertEquals($course2->id, $log->course);
-        $this->assertSame('aaa', $log->module);
-        $this->assertEquals($module2->cmid, $log->cmid);
-        $this->assertSame('bbb', $log->action);
-        $this->assertSame('info.php', $log->url);
-        $this->assertSame('666', $log->info);
-
         // Test if disabling works.
         set_config('enabled_stores', 'logstore_legacy', 'tool_log');
         set_config('loglegacy', 0, 'logstore_legacy');
@@ -142,9 +105,7 @@ class logstore_legacy_store_testcase extends advanced_testcase {
 
         \logstore_legacy\event\unittest_executed::create(
             array('context' => \context_system::instance(), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
-        add_to_log($course1->id, 'xxxx', 'yyyy', '', '7', 0, 0);
-        $this->assertDebuggingCalled();
-        $this->assertEquals(4, $DB->count_records('log'));
+        $this->assertEquals(2, $DB->count_records('log'));
 
         // Another way to disable legacy completely.
         set_config('enabled_stores', 'logstore_standard', 'tool_log');
@@ -153,9 +114,7 @@ class logstore_legacy_store_testcase extends advanced_testcase {
 
         \logstore_legacy\event\unittest_executed::create(
             array('context' => \context_system::instance(), 'other' => array('sample' => 5, 'xx' => 10)))->trigger();
-        add_to_log($course1->id, 'xxxx', 'yyyy', '', '7', 0, 0);
-        $this->assertDebuggingCalled();
-        $this->assertEquals(4, $DB->count_records('log'));
+        $this->assertEquals(2, $DB->count_records('log'));
         // Set everything back.
         set_config('enabled_stores', '', 'tool_log');
         set_config('loglegacy', 0, 'logstore_legacy');
index b2f77e0..6e0c86c 100644 (file)
Binary files a/admin/tool/lp/amd/build/dialogue.min.js and b/admin/tool/lp/amd/build/dialogue.min.js differ
index 7045a6f..881dda1 100644 (file)
Binary files a/admin/tool/lp/amd/build/dialogue.min.js.map and b/admin/tool/lp/amd/build/dialogue.min.js.map differ
index 806eeeb..9637bc9 100644 (file)
Binary files a/admin/tool/lp/amd/build/grade_dialogue.min.js and b/admin/tool/lp/amd/build/grade_dialogue.min.js differ
index 6af7b51..472ffa8 100644 (file)
Binary files a/admin/tool/lp/amd/build/grade_dialogue.min.js.map and b/admin/tool/lp/amd/build/grade_dialogue.min.js.map differ
index ade6899..45c0800 100644 (file)
Binary files a/admin/tool/lp/amd/build/grade_user_competency_inline.min.js and b/admin/tool/lp/amd/build/grade_user_competency_inline.min.js differ
index 1095c0f..4cc60b6 100644 (file)
Binary files a/admin/tool/lp/amd/build/grade_user_competency_inline.min.js.map and b/admin/tool/lp/amd/build/grade_user_competency_inline.min.js.map differ
index 04a570f..b96e262 100644 (file)
@@ -35,6 +35,8 @@ define(['core/yui'], function(Y) {
      * @param {Boolean} wide Specify we want an extra wide dialogue (the size is standard, but wider than the default).
      */
     var dialogue = function(title, content, afterShow, afterHide, wide) {
+        M.util.js_pending('tool_lp/dialogue:dialogue');
+
         this.yuiDialogue = null;
         var parent = this;
 
@@ -59,6 +61,10 @@ define(['core/yui'], function(Y) {
                 width: width
             });
 
+            parent.yuiDialogue.before('visibleChange', function() {
+                M.util.js_pending('tool_lp/dialogue:before:visibleChange');
+            });
+
             parent.yuiDialogue.after('visibleChange', function(e) {
                 if (e.newVal) {
                     // Delay the callback call to the next tick, otherwise it can happen that it is
@@ -67,18 +73,25 @@ define(['core/yui'], function(Y) {
                         Y.soon(function() {
                             afterShow(parent);
                             parent.yuiDialogue.centerDialogue();
+                            M.util.js_complete('tool_lp/dialogue:before:visibleChange');
                         });
+                    } else {
+                        M.util.js_complete('tool_lp/dialogue:before:visibleChange');
                     }
                 } else {
                     if ((typeof afterHide !== 'undefined')) {
                         Y.soon(function() {
                             afterHide(parent);
+                            M.util.js_complete('tool_lp/dialogue:before:visibleChange');
                         });
+                    } else {
+                        M.util.js_complete('tool_lp/dialogue:before:visibleChange');
                     }
                 }
             });
 
             parent.yuiDialogue.show();
+            M.util.js_complete('tool_lp/dialogue:dialogue');
         });
     };
 
index 80ef97c..3b92add 100644 (file)
@@ -102,6 +102,7 @@ define(['jquery',
      * @return {Promise}
      */
     Grade.prototype.display = function() {
+        M.util.js_pending('tool_lp/grade_dialogue:display');
         return $.when(
             Str.get_string('rate', 'tool_lp'),
             this._render()
@@ -110,7 +111,10 @@ define(['jquery',
             this._popup = new Dialogue(
                 title,
                 templateResult[0],
-                this._afterRender.bind(this)
+                function() {
+                    this._afterRender();
+                    M.util.js_complete('tool_lp/grade_dialogue:display');
+                }.bind(this)
             );
 
             return this._popup;
index 81e4f35..9b32017 100644 (file)
@@ -94,6 +94,7 @@ define(['jquery',
         var options = [],
             self = this;
 
+        M.util.js_pending('tool_lp/grade_user_competency_inline:_setUp');
         var promise = ScaleValues.get_values(self._scaleId);
         promise.then(function(scalevalues) {
             options.push({
@@ -134,6 +135,7 @@ define(['jquery',
         .then(function(dialogue) {
             self._dialogue = dialogue;
 
+            M.util.js_complete('tool_lp/grade_user_competency_inline:_setUp');
             return;
         })
         .fail(notification.exception);
index a9dc790..07a51d9 100644 (file)
@@ -489,6 +489,7 @@ class api {
                 '$mmSideMenuDelegate_mmaFiles' => new lang_string('files'),
                 '$mmSideMenuDelegate_website' => new lang_string('webpage'),
                 '$mmSideMenuDelegate_help' => new lang_string('help'),
+                'CoreMainMenuDelegate_QrReader' => new lang_string('scanqrcode', 'tool_mobile'),
             ),
             "$course" => array(
                 'NoDelegate_CourseBlocks' => new lang_string('blocks'),
@@ -527,6 +528,12 @@ class api {
             $features["$remoteaddons"] = $remoteaddonslist;
         }
 
+        if (!empty($availablemods['lti'])) {
+            $ltidisplayname = $availablemods['lti']->displayname;
+            $features["$ltidisplayname"]['CoreCourseModuleDelegate_AddonModLti:openInAppBrowser'] =
+                new lang_string('openusingembeddedbrowser', 'tool_mobile');
+        }
+
         // Display OAuth 2 identity providers.
         if (is_enabled_auth('oauth2')) {
             $identityproviderslist = array();
index e56f280..bc7fc89 100644 (file)
@@ -105,6 +105,7 @@ $string['notificationssentnotifications'] = 'Notifications sent';
 $string['notificationscurrentactivedevices'] = 'Devices receiving notifications this month';
 $string['oauth2identityproviders'] = 'OAuth 2 identity providers';
 $string['offlineuse'] = 'Offline use';
+$string['openusingembeddedbrowser'] = 'Open using embedded browser';
 $string['pluginname'] = 'Moodle app tools';
 $string['pluginnotenabledorconfigured'] = 'Plugin not enabled or configured.';
 $string['qrcodedisabled'] = 'Access via QR code disabled';
@@ -118,6 +119,7 @@ $string['qrcodetypeurl'] = 'QR code with site URL';
 $string['qrcodetypelogin'] = 'QR code with automatic login';
 $string['readingthisemailgettheapp'] = 'Reading this in an email? <a href="{$a}">Download the mobile app and receive notifications on your mobile device</a>.';
 $string['remoteaddons'] = 'Remote add-ons';
+$string['scanqrcode'] = 'Scan QR code';
 $string['selfsignedoruntrustedcertificatewarning'] = 'It seems that the HTTPS certificate is self-signed or not trusted. The mobile app will only work with trusted sites.';
 $string['setuplink'] = 'App download page';
 $string['setuplink_desc'] = 'URL of page with options to download the mobile app from the App Store and Google Play. The app download page link is displayed in the page footer and in a user\'s profile. Leave blank to not display a link.';
index 1d1ff6f..cf6fcfd 100644 (file)
@@ -455,8 +455,9 @@ class auth extends \auth_plugin_base {
             }
         }
 
+        $issuer = $client->get_issuer();
         // First we try and find a defined mapping.
-        $linkedlogin = api::match_username_to_user($userinfo['username'], $client->get_issuer());
+        $linkedlogin = api::match_username_to_user($userinfo['username'], $issuer);
 
         if (!empty($linkedlogin) && empty($linkedlogin->get('confirmtoken'))) {
             $mappeduser = get_complete_user_data('id', $linkedlogin->get('userid'));
@@ -474,7 +475,7 @@ class auth extends \auth_plugin_base {
                 $SESSION->loginerrormsg = get_string('invalidlogin');
                 $client->log_out();
                 redirect(new moodle_url('/login/index.php'));
-            } else if ($mappeduser && $mappeduser->confirmed) {
+            } else if ($mappeduser && ($mappeduser->confirmed || !$issuer->get('requireconfirmation'))) {
                 // Update user fields.
                 $userinfo = $this->update_user($userinfo, $mappeduser);
                 $userwasmapped = true;
@@ -503,7 +504,7 @@ class auth extends \auth_plugin_base {
             redirect(new moodle_url('/login/index.php'));
         }
 
-        $issuer = $client->get_issuer();
+
         if (!$issuer->is_valid_login_domain($oauthemail)) {
             // Trigger login failed event.
             $failurereason = AUTH_LOGIN_UNAUTHORISED;
index 71fd422..7bc53b1 100644 (file)
@@ -99,10 +99,14 @@ class block_blog_menu extends block_base {
         // Prepare the footer for this block
         if (has_capability('moodle/blog:search', context_system::instance())) {
             // Full-text search field
-            $form  = html_writer::tag('label', get_string('search', 'admin'), array('for'=>'blogsearchquery', 'class'=>'accesshide'));
-            $form .= html_writer::empty_tag('input', array('id'=>'blogsearchquery', 'type'=>'text', 'name'=>'search'));
-            $form .= html_writer::empty_tag('input', array('type'=>'submit', 'value'=>get_string('search')));
-            $this->content->footer = html_writer::tag('form', html_writer::tag('div', $form), array('class'=>'blogsearchform', 'method'=>'get', 'action'=>new moodle_url('/blog/index.php')));
+            $form  = html_writer::tag('label', get_string('search', 'admin'), array('for' => 'blogsearchquery',
+                'class' => 'accesshide'));
+            $form .= html_writer::empty_tag('input', array('id' => 'blogsearchquery', 'class' => 'form-control mr-1',
+                'type' => 'text', 'name' => 'search'));
+            $form .= html_writer::empty_tag('input', array('type' => 'submit', 'class' => 'btn btn-secondary',
+                'value' => get_string('search')));
+            $this->content->footer = html_writer::tag('form', html_writer::tag('div', $form), array(
+                'class' => 'blogsearchform form-inline', 'method' => 'get', 'action' => new moodle_url('/blog/index.php')));
         } else {
             // No footer to display
             $this->content->footer = '';
index 076bd53..7563077 100644 (file)
@@ -1,6 +1,12 @@
 This files describes API changes in /cache/stores/* - cache store plugins.
 Information provided here is intended especially for developers.
 
+=== 4.0 ===
+* The function supports_recursion() from the lock_factory interface has been deprecated including the related implementations.
+* 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.
+
 === 3.9 ===
 * The record_cache_hit/miss/set methods now take a cache_store instead of a cache_definition object
 
index 49ead88..9f72034 100644 (file)
Binary files a/calendar/amd/build/calendar_filter.min.js and b/calendar/amd/build/calendar_filter.min.js differ
index 91dd103..5fd1103 100644 (file)
Binary files a/calendar/amd/build/calendar_filter.min.js.map and b/calendar/amd/build/calendar_filter.min.js.map differ
index 41dd448..415b3b3 100644 (file)
@@ -64,6 +64,7 @@ function(
         // Toggle the hidden. We need to render the template before we change the value.
         data.hidden = !data.hidden;
 
+        M.util.js_pending("core_calendar/calendar_filter:toggleFilter");
         return Str.get_string('eventtype' + data.eventtype, 'calendar')
         .then(function(nameStr) {
             data.name = nameStr;
@@ -81,6 +82,7 @@ function(
         })
         .then(function() {
             fireFilterChangedEvent(data);
+            M.util.js_complete("core_calendar/calendar_filter:toggleFilter");
             return;
         });
     };
index 29cb40e..22ccef2 100644 (file)
@@ -100,6 +100,13 @@ switch ($action) {
 
         if (empty($roleid)) {
             $roleid = null;
+        } else {
+            if (!has_capability('moodle/role:assign', $context)) {
+                throw new enrol_ajax_exception('assignnotpermitted');
+            }
+            if (!array_key_exists($roleid, get_assignable_roles($context, ROLENAME_ALIAS, false))) {
+                throw new enrol_ajax_exception('invalidrole');
+            }
         }
 
         if (empty($startdate)) {
index dbb69cd..96b372b 100644 (file)
 <?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/>.
 
-    require('../config.php');
-
-    // Form submitted, do not check referer (original page unknown).
-    if ($form = data_submitted()) {
-        // Only deal with real users.
-        if (!isloggedin()) {
-            redirect($CFG->wwwroot);
-        }
-
-        // Send the message and redirect.
-        $eventdata = new \core\message\message();
-        $eventdata->courseid         = SITEID;
-        $eventdata->component        = 'moodle';
-        $eventdata->name             = 'errors';
-        $eventdata->userfrom          = $USER;
-        $eventdata->userto            = core_user::get_support_user();
-        $eventdata->subject           = 'Error: '. $form->referer .' -> '. $form->requested;
-        $eventdata->fullmessage       = $form->text;
-        $eventdata->fullmessageformat = FORMAT_PLAIN;
-        $eventdata->fullmessagehtml   = '';
-        $eventdata->smallmessage      = '';
-        message_send($eventdata);
-
-        redirect($CFG->wwwroot .'/course/', 'Message sent, thanks', 3);
-        exit;
-    }
+/**
+ * Moodle 404 Error page
+ *
+ * This is for 404 error pages served by the webserver and then passed
+ * to Moodle to be rendered using the site theme.
+ *
+ * ErrorDocument 404 /error/index.php
+ *
+ * @package    core
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
 
-    $site = get_site();
-    $redirecturl = empty($_SERVER['REDIRECT_URL']) ? '' : $_SERVER['REDIRECT_URL'];
-    $httpreferer = get_local_referer(false);
-    $requesturi  = empty($_SERVER['REQUEST_URI'])  ? '' : $_SERVER['REQUEST_URI'];
-
-    header("HTTP/1.0 404 Not Found");
-    header("Status: 404 Not Found");
-
-    $PAGE->set_url('/error/');
-    $PAGE->set_context(context_system::instance());
-    $PAGE->set_title($site->fullname .':Error');
-    $PAGE->set_heading($site->fullname .': Error 404');
-    $PAGE->navbar->add('Error 404 - File not Found');
-    echo $OUTPUT->header();
-    echo $OUTPUT->box(get_string('pagenotexist', 'error'). '<br />'.s($requesturi), 'generalbox boxaligncenter');
-
-    if (isloggedin()) {
-?>
-        <p><?php echo get_string('pleasereport', 'error'); ?>
-        <p><form action="<?php echo $CFG->wwwroot ?>/error/index.php" method="post">
-           <textarea rows="3" cols="50" name="text" id="text" spellcheck="true"></textarea><br />
-           <input type="hidden" name="referer" value="<?php p($httpreferer) ?>">
-           <input type="hidden" name="requested" value="<?php p($requesturi) ?>">
-           <input type="submit" value="<?php echo get_string('sendmessage', 'error'); ?>">
-           </form>
-<?php
-    } else {
-        echo $OUTPUT->continue_button($CFG->wwwroot);
+// @codingStandardsIgnoreStart
+require('../config.php');
+// @codingStandardsIgnoreEnd
+
+$context = context_system::instance();
+$title = get_string('pagenotexisttitle', 'error');
+$PAGE->set_url('/error/index.php');
+$PAGE->set_context($context);
+$PAGE->set_title($title);
+$PAGE->set_heading($title);
+$PAGE->navbar->add($title);
+
+$canmessage = has_capability('moodle/site:senderrormessage', $context);
+
+$supportuser = core_user::get_support_user();
+
+// We can only message support if both the user has the capability
+// and the support user is a real user.
+if ($canmessage) {
+    $canmessage = core_user::is_real_user($supportuser->id);
+}
+
+$mform = new \core\form\error_feedback($CFG->wwwroot . '/error/index.php');
+
+if ($data = $mform->get_data()) {
+
+    if (!$canmessage) {
+        redirect($CFG->wwwroot);
     }
-    echo $OUTPUT->footer();
-?>
+
+    // Send the message and redirect.
+    $message = new \core\message\message();
+    $message->courseid         = SITEID;
+    $message->component        = 'moodle';
+    $message->name             = 'errors';
+    $message->userfrom          = $USER;
+    $message->userto            = core_user::get_support_user();
+    $message->subject           = 'Error: '. $data->referer .' -> '. $data->requested;
+    $message->fullmessage       = $data->text;
+    $message->fullmessageformat = FORMAT_PLAIN;
+    $message->fullmessagehtml   = '';
+    $message->smallmessage      = '';
+    $message->contexturl = $data->requested;
+    message_send($message);
+
+    redirect($CFG->wwwroot, get_string('sendmessagesent', 'error', $data->requested), 5);
+    exit;
+}
+
+echo $OUTPUT->header();
+echo $OUTPUT->notification(get_string('pagenotexist', 'error', s($ME)), 'error');
+
+if (!empty($CFG->supportpage)) {
+    echo \html_writer::tag('h4', get_string('supportpage', 'admin'));
+    $link = \html_writer::link($CFG->supportpage, $CFG->supportpage);
+    echo \html_writer::tag('p', $link);
+}
+if (!empty($CFG->supportemail)) {
+    echo \html_writer::tag('h4', get_string('supportemail', 'admin'));
+    $link = \html_writer::link('mailto:' . $CFG->supportemail, $CFG->supportemail);
+    echo \html_writer::tag('p', $link);
+}
+
+if ($canmessage) {
+    echo \html_writer::tag('h4', get_string('sendmessage', 'error'));
+    $mform->display();
+} else {
+    echo $OUTPUT->continue_button($CFG->wwwroot);
+}
+
+echo $OUTPUT->footer();
+
diff --git a/error/plainpage.php b/error/plainpage.php
new file mode 100644 (file)
index 0000000..3d2cbf5
--- /dev/null
@@ -0,0 +1,92 @@
+<?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/>.
+
+/**
+ * Moodle Generic plain page
+ *
+ * This is used for various pages, usually errors, early in the Moodle
+ * bootstrap. It can be safetly customized by editing this file directly
+ * but it MUST NOT contain any Moodle resources such as theme files generated
+ * by php, it can only contain references to static css and images, and as a
+ * precaution its recommended that everything is inlined rather than
+ * references. This is why this file is located here as it cannot be inside
+ * a Moodle theme.
+ *
+ * @package    core
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// @codingStandardsIgnoreStart
+?>
+<!DOCTYPE html>
+<html <?php echo $htmllang ?>>
+    <head>
+        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+        <?php echo $meta ?>
+        <title><?php echo $title ?></title>
+        <style>
+<?php
+// This is a very small modified subset of the bootstrap / boost css classes.
+?>
+body {
+    margin: 0;
+    font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
+    font-size: .9375rem;
+    font-weight: 400;
+    line-height: 1.5;
+    color: #343a40;
+    text-align: left;
+    background-color: #f2f2f2;
+}
+#page {
+    margin-top: 15px;
+    background: white;
+    max-width: 600px;
+    margin: 0 auto;
+    padding: 15px;
+}
+#region-main {
+    margin: 0 auto;
+    border: 1px solid rgba(0,0,0,.125);
+    padding: 1rem 1.25rem 1.25rem;
+    background-color: #fff;
+}
+h1 {
+    font-size: 2.34rem;
+    margin: 0 0 .5rem;
+    font-weight: 300;
+    line-height: 1.2;
+}
+.alert-danger {
+    color: #6e211e;
+    background-color: #f6d9d8;
+    border-color: #f3c9c8;
+    padding: .75rem 1.25rem;
+}
+    </style>
+    </head>
+    <body>
+        <div id="page">
+            <div id="region-main">
+                <h1><?php echo $title ?></h1>
+                <?php echo $content ?>
+                <?php echo $footer ?>
+            </div>
+        </div>
+    </body>
+</html>
+
index 4080712..9ee6a8b 100644 (file)
@@ -1070,6 +1070,9 @@ $string['purgeselectedcaches'] = 'Purge selected caches';
 $string['purgeselectedcachesfinished'] = 'The selected caches were purged.';
 $string['purgetemplates'] = 'Templates';
 $string['purgethemecache'] = 'Themes';
+$string['referrernone'] = 'Browser default';
+$string['referrerpolicy'] = 'Referrer policy';
+$string['referrerpolicydesc'] = 'Set the referrer policy header to be included with responses from your site.';
 $string['restorecourse'] = 'Restore course';
 $string['restorernewroleid'] = 'Restorers\' role in courses';
 $string['restorernewroleid_help'] = 'If the user does not already have the permission to manage the newly restored course, the user is automatically assigned this role and enrolled if necessary. Select "None" if you do not want restorers to be able to manage every restored course.';
@@ -1159,6 +1162,7 @@ $string['simplexmlrequired'] = 'The SimpleXML PHP extension is now required by M
 $string['sitemenubar'] = 'Site navigation';
 $string['sitemailcharset'] = 'Character set';
 $string['sitemaintenance'] = 'The site is undergoing maintenance and is currently not available';
+$string['sitemaintenancetitle'] = '{$a} under maintenance';
 $string['sitemaintenancemode'] = 'Maintenance mode';
 $string['sitemaintenanceoff'] = 'Maintenance mode has been disabled and the site is running normally again';
 $string['sitemaintenanceon'] = 'Your site is currently in maintenance mode (only admins can log in or use the site).';
index 5a6c463..cd1cc95 100644 (file)
@@ -466,7 +466,8 @@ $string['onlyadmins'] = 'Only administrators can do that';
 $string['onlyeditingteachers'] = 'Only editing teachers can do that';
 $string['onlyeditown'] = 'You can only edit your own information';
 $string['orderidnotfound'] = 'Order ID {$a} not found';
-$string['pagenotexist'] = 'An unusual error occurred (tried to reach a page that does not exist)';
+$string['pagenotexisttitle'] = '404 Error: File not found';
+$string['pagenotexist'] = '<p>An unusual error occurred trying to view a page that does not exist:</p>{$a}';
 $string['pathdoesnotstartslash'] = 'No valid arguments supplied, path does not start with slash!';
 $string['pleasereport'] = 'If you have time, please let us know what you were trying to do when the error occurred:';
 $string['pluginrequirementsnotmet'] = 'Plugin "{$a->pluginname}" ({$a->pluginversion}) could not be installed.  It requires a newer version of Moodle (currently you are using {$a->currentmoodle}, you need {$a->requiremoodle}).';
@@ -498,6 +499,7 @@ $string['rpcerror'] = 'Ooops! Your MNET communication has failed! Here\'s that e
 $string['secretalreadyused'] = 'Change password confirmation link was already used, password was not changed';
 $string['sectionnotexist'] = 'This section does not exist';
 $string['sendmessage'] = 'Send message';
+$string['sendmessagesent'] = 'Thanks for your feedback about:<br>{$a}';
 $string['serverconnection'] = 'Error connecting to the server';
 $string['servicedonotexist'] = 'The service does not exist';
 $string['sessionwaiterr'] = 'Timed out while waiting for session lock.<br />Wait for your current requests to finish and try again later.';
index b284206..06ef6bb 100644 (file)
@@ -428,6 +428,7 @@ $string['site:mnetloginfromremote'] = 'Login from a remote application via MNet'
 $string['site:mnetlogintoremote'] = 'Roam to a remote application via MNet';
 $string['site:readallmessages'] = 'Read all messages on site';
 $string['site:restore'] = 'Restore courses';
+$string['site:senderrormessage'] = 'Send a message to the support user from the error page';
 $string['site:sendmessage'] = 'Send messages to any user';
 $string['site:trustcontent'] = 'Trust submitted content';
 $string['site:uploadusers'] = 'Upload new users from file';
index fd87ec4..8a75d3c 100644 (file)
@@ -628,7 +628,7 @@ function is_dataroot_insecure($fetchtest=false) {
  * Enables CLI maintenance mode by creating new dataroot/climaintenance.html file.
  */
 function enable_cli_maintenance_mode() {
-    global $CFG;
+    global $CFG, $SITE;
 
     if (file_exists("$CFG->dataroot/climaintenance.html")) {
         unlink("$CFG->dataroot/climaintenance.html");
@@ -645,7 +645,7 @@ function enable_cli_maintenance_mode() {
     } else {
         $data = get_string('sitemaintenance', 'admin');
         $data = bootstrap_renderer::early_error_content($data, null, null, null);
-        $data = bootstrap_renderer::plain_page(get_string('sitemaintenance', 'admin'), $data);
+        $data = bootstrap_renderer::plain_page(get_string('sitemaintenancetitle', 'admin', $SITE->fullname), $data);
     }
 
     file_put_contents("$CFG->dataroot/climaintenance.html", $data);
index 729ac1e..c97f492 100644 (file)
@@ -119,6 +119,11 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
      * @return NodeElement
      */
     protected function find($selector, $locator, $exception = false, $node = false, $timeout = false) {
+        if ($selector === 'NodeElement' && is_a($locator, NodeElement::class)) {
+            // Support a NodeElement being passed in for use in step chaining.
+            return $locator;
+        }
+
         // Returns the first match.
         $items = $this->find_all($selector, $locator, $exception, $node, $timeout);
         return count($items) ? reset($items) : null;
diff --git a/lib/classes/form/error_feedback.php b/lib/classes/form/error_feedback.php
new file mode 100644 (file)
index 0000000..b4e3fd6
--- /dev/null
@@ -0,0 +1,58 @@
+<?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/>.
+
+/**
+ * Moodle 404 Error page feedback form
+ *
+ * @package    core
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\form;
+defined('MOODLE_INTERNAL') || die();
+
+use moodleform;
+
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Moodle 404 Error page feedback form
+ *
+ * @package    core
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class error_feedback extends moodleform {
+
+    /**
+     * Error form definition
+     */
+    public function definition() {
+        global $CFG;
+
+        $mform = $this->_form;
+        $mform->addElement('hidden', 'referer', get_local_referer(false));
+        $mform->setType('referer', PARAM_URL);
+
+        $mform->addElement('hidden', 'requested', (empty($_SERVER['REDIRECT_URL']) ? '' : $_SERVER['REDIRECT_URL']));
+        $mform->setType('requested', PARAM_URL);
+
+        $mform->addElement('textarea', 'text', get_string('pleasereport', 'error'), 'wrap="virtual" rows="10" cols="50"');
+        $mform->addElement('submit', 'submitbutton', get_string('sendmessage', 'error'));
+    }
+}
+
index e430bc5..4dfec49 100644 (file)
@@ -91,9 +91,13 @@ class db_record_lock_factory implements lock_factory {
 
     /**
      * Multiple locks for the same resource can be held by a single process.
+     *
+     * @deprecated since Moodle 4.0.
      * @return boolean - False - not process specific.
      */
     public function supports_recursion() {
+        debugging('The function supports_recursion() is deprecated, please do not use it anymore.',
+            DEBUG_DEVELOPER);
         return false;
     }
 
@@ -187,11 +191,16 @@ class db_record_lock_factory implements lock_factory {
 
     /**
      * Extend a lock that was previously obtained with @lock.
+     *
+     * @deprecated since Moodle 4.0.
      * @param lock $lock - a lock obtained from this factory.
      * @param int $maxlifetime - the new lifetime for the lock (in seconds).
      * @return boolean - true if the lock was extended.
      */
     public function extend_lock(lock $lock, $maxlifetime = 86400) {
+        debugging('The function extend_lock() is deprecated, please do not use it anymore.',
+            DEBUG_DEVELOPER);
+
         $now = time();
         $expires = $now + $maxlifetime;
         $params = array('expires' => $expires,
index dd20e9b..b771639 100644 (file)
@@ -108,9 +108,13 @@ class file_lock_factory implements lock_factory {
 
     /**
      * Multiple locks for the same resource cannot be held from a single process.
+     *
+     * @deprecated since Moodle 4.0.
      * @return boolean - False
      */
     public function supports_recursion() {
+        debugging('The function supports_recursion() is deprecated, please do not use it anymore.',
+            DEBUG_DEVELOPER);
         return false;
     }
 
@@ -188,11 +192,15 @@ class file_lock_factory implements lock_factory {
 
     /**
      * Extend a lock that was previously obtained with @lock.
+     *
+     * @deprecated since Moodle 4.0.
      * @param lock $lock - not used
      * @param int $maxlifetime - not used
      * @return boolean - true if the lock was extended.
      */
     public function extend_lock(lock $lock, $maxlifetime = 86400) {
+        debugging('The function extend_lock() is deprecated, please do not use it anymore.',
+            DEBUG_DEVELOPER);
         // Not supported by this factory.
         return false;
     }
index 5200cab..0874ced 100644 (file)
@@ -76,9 +76,12 @@ class installation_lock_factory implements lock_factory {
     /**
      * Multiple locks for the same resource cannot be held from a single process.
      *
+     * @deprecated since Moodle 4.0.
      * @return boolean - False
      */
     public function supports_recursion() {
+        debugging('The function supports_recursion() is deprecated, please do not use it anymore.',
+            DEBUG_DEVELOPER);
         return false;
     }
 
@@ -115,11 +118,14 @@ class installation_lock_factory implements lock_factory {
     /**
      * Extend a lock that was previously obtained with @lock.
      *
+     * @deprecated since Moodle 4.0.
      * @param lock $lock - not used
      * @param int $maxlifetime - not used
      * @return boolean - true if the lock was extended.
      */
     public function extend_lock(lock $lock, $maxlifetime = 86400) {
+        debugging('The function extend_lock() is deprecated, please do not use it anymore.',
+            DEBUG_DEVELOPER);
         // Not supported by this factory.
         return false;
     }
index 7b117c3..f67083c 100644 (file)
@@ -80,10 +80,15 @@ class lock {
 
     /**
      * Extend the lifetime of this lock. Not supported by all factories.
+     *
+     * @deprecated since Moodle 4.0.
      * @param int $maxlifetime - the new lifetime for the lock (in seconds).
      * @return bool
      */
     public function extend($maxlifetime = 86400) {
+        debugging('The function extend() is deprecated, please do not use it anymore.',
+            DEBUG_DEVELOPER);
+
         if ($this->factory) {
             return $this->factory->extend_lock($this, $maxlifetime);
         }
index b03e774..c0d32b0 100644 (file)
@@ -63,6 +63,7 @@ interface lock_factory {
     /**
      * Supports recursion.
      *
+     * @deprecated since Moodle 4.0.
      * @return boolean - True if attempting to get 2 locks on the same resource will "stack"
      */
     public function supports_recursion();
@@ -98,6 +99,7 @@ interface lock_factory {
     /**
      * Extend the timeout on a held lock.
      *
+     * @deprecated since Moodle 4.0.
      * @param lock $lock - lock obtained from this factory
      * @param int $maxlifetime - new max time to hold the lock
      * @return boolean - True if the lock was extended.
index 6f05924..753741b 100644 (file)
@@ -106,9 +106,12 @@ class mysql_lock_factory implements lock_factory {
      * Hard coded to false and workaround inconsistent support in different
      * versions of MySQL / MariaDB.
      *
+     * @deprecated since Moodle 4.0.
      * @return boolean - false
      */
     public function supports_recursion() {
+        debugging('The function supports_recursion() is deprecated, please do not use it anymore.',
+            DEBUG_DEVELOPER);
         return false;
     }
 
@@ -165,11 +168,15 @@ class mysql_lock_factory implements lock_factory {
 
     /**
      * Extend a lock that was previously obtained with @lock.
+     *
+     * @deprecated since Moodle 4.0.
      * @param lock $lock - a lock obtained from this factory.
      * @param int $maxlifetime - the new lifetime for the lock (in seconds).
      * @return boolean - true if the lock was extended.
      */
     public function extend_lock(lock $lock, $maxlifetime = 86400) {
+        debugging('The function extend_lock() is deprecated, please do not use it anymore.',
+            DEBUG_DEVELOPER);
         // Not supported by this factory.
         return false;
     }
index 0d48eaa..1d18967 100644 (file)
@@ -118,11 +118,15 @@ class postgres_lock_factory implements lock_factory {
     }
 
     /**
-     * Multiple locks for the same resource can be held by a single process.
-     * @return boolean - Defer to the DB driver.
+     * Multiple locks for the same resource can NOT be held by a single process.
+     *
+     * @deprecated since Moodle 4.0.
+     * @return boolean - false.
      */
     public function supports_recursion() {
-        return true;
+        debugging('The function supports_recursion() is deprecated, please do not use it anymore.',
+            DEBUG_DEVELOPER);
+        return false;
     }
 
     /**
@@ -145,6 +149,7 @@ class postgres_lock_factory implements lock_factory {
 
     /**
      * Create and get a lock
+     *
      * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
      * @param int $timeout - The number of seconds to wait for a lock before giving up.
      * @param int $maxlifetime - Unused by this lock type.
@@ -155,8 +160,14 @@ class postgres_lock_factory implements lock_factory {
 
         $token = $this->get_index_from_key($this->type . '_' . $resource);
 
-        $params = array('locktype' => $this->dblockid,
-                        'token' => $token);
+        if (isset($this->openlocks[$token])) {
+            return false;
+        }
+
+        $params = [
+            'locktype' => $this->dblockid,
+            'token' => $token
+        ];
 
         $locked = false;
 
@@ -194,11 +205,15 @@ class postgres_lock_factory implements lock_factory {
 
     /**
      * Extend a lock that was previously obtained with @lock.
+     *
+     * @deprecated since Moodle 4.0.
      * @param lock $lock - a lock obtained from this factory.
      * @param int $maxlifetime - the new lifetime for the lock (in seconds).
      * @return boolean - true if the lock was extended.
      */
     public function extend_lock(lock $lock, $maxlifetime = 86400) {
+        debugging('The function extend_lock() is deprecated, please do not use it anymore.',
+            DEBUG_DEVELOPER);
         // Not supported by this factory.
         return false;
     }
index 08e88b5..14e1075 100644 (file)
@@ -72,22 +72,22 @@ abstract class icon_system {
     /**
      * Factory method
      *
-     * @param $type Either a specific type, or null to get the default type.
+     * @param string $type Either a specific type, or null to get the default type.
      * @return \core\output\icon_system
      */
     public final static function instance($type = null) {
         global $PAGE;
 
-        if ($type == null) {
-            if (!empty(self::$instance)) {
-                return self::$instance;
-            }
-            $type = $PAGE->theme->get_icon_system();
-            self::$instance = new $type();
-            // Default one is a singleton.
+        if (empty(self::$instance)) {
+            $icontype = $PAGE->theme->get_icon_system();
+            self::$instance = new $icontype();
+        }
+
+        // If $type is specified we need to make sure that the theme icon system supports this type,
+        // if not, we will return a generic new instance of the $type.
+        if ($type === null || is_a(self::$instance, $type)) {
             return self::$instance;
         } else {
-            // Not a singleton.
             return new $type();
         }
     }
index ba4ccdb..38ad233 100644 (file)
@@ -440,7 +440,10 @@ class icon_system_fontawesome extends icon_system_font {
         if ($this->map === []) {
             $cache = \cache::make('core', 'fontawesomeiconmapping');
 
-            $this->map = $cache->get('mapping');
+            // Create different mapping keys for different icon system classes, there may be several different
+            // themes on the same site.
+            $mapkey = 'mapping_'.preg_replace('/[^a-zA-Z0-9_]/', '_', get_class($this));
+            $this->map = $cache->get($mapkey);
 
             if (empty($this->map)) {
                 $this->map = $this->get_core_icon_map();
@@ -454,7 +457,7 @@ class icon_system_fontawesome extends icon_system_font {
                         }
                     }
                 }
-                $cache->set('mapping', $this->map);
+                $cache->set($mapkey, $this->map);
             }
 
         }
index 0ae1c2a..3de202d 100644 (file)
@@ -119,6 +119,15 @@ $capabilities = array(
         )
     ),
 
+    'moodle/site:senderrormessage' => [
+        'riskbitmask' => RISK_SPAM,
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_SYSTEM,
+        'archetypes' => array(
+            'user' => CAP_ALLOW
+        )
+    ],
+
     'moodle/site:deleteownmessage' => array(
 
         'captype' => 'write',
index cc2304f..d54813c 100644 (file)
@@ -36,9 +36,6 @@ defined('MOODLE_INTERNAL') || die();
 // The old class name is the key, the new class name is the value.
 // The array must be called $renamedclasses.
 $renamedclasses = array(
-    // Since Moodle 3.6.
-    'course_in_list' => 'core_course_list_element',
-    'coursecat' => 'core_course_category',
     // Since Moodle 3.7.
     'core\\analytics\\target\\course_dropout' => 'core_course\\analytics\\target\\course_dropout',
     'core\\analytics\\target\\course_competencies' => 'core_course\\analytics\\target\\course_competencies',
index 1327d93..c28996f 100644 (file)
@@ -33,28 +33,10 @@ defined('MOODLE_INTERNAL') || die();
 /* === Functions that needs to be kept longer in deprecated lib than normal time period === */
 
 /**
- * Add an entry to the legacy log table.
- *
  * @deprecated since 2.7 use new events instead
- *
- * @param    int     $courseid  The course id
- * @param    string  $module  The module name  e.g. forum, journal, resource, course, user etc
- * @param    string  $action  'view', 'update', 'add' or 'delete', possibly followed by another word to clarify.
- * @param    string  $url     The file and parameters used to see the results of the action
- * @param    string  $info    Additional description information
- * @param    int     $cm      The course_module->id if there is one
- * @param    int|stdClass $user If log regards $user other than $USER
- * @return void
- */
-function add_to_log($courseid, $module, $action, $url='', $info='', $cm=0, $user=0) {
-    debugging('add_to_log() has been deprecated, please rewrite your code to the new events API', DEBUG_DEVELOPER);
-
-    // This is a nasty hack that allows us to put all the legacy stuff into legacy storage,
-    // this way we may move all the legacy settings there too.
-    $manager = get_log_manager();
-    if (method_exists($manager, 'legacy_add_to_log')) {
-        $manager->legacy_add_to_log($courseid, $module, $action, $url, $info, $cm, $user);
-    }
+ */
+function add_to_log() {
+    throw new coding_exception('add_to_log() has been removed, please rewrite your code to the new events API');
 }
 
 /**
@@ -766,57 +748,12 @@ function print_side_block() {
 }
 
 /**
- * Prints a basic textarea field.
- *
- * This was 'deprecated' in 2.0, but not properly (there was no alternative) so the
- * debugging message was commented out.
- *
  * @deprecated since Moodle 3.6
- *
- * When using this function, you should
- *
- * @global object
- * @param bool $unused No longer used.
- * @param int $rows Number of rows to display  (minimum of 10 when $height is non-null)
- * @param int $cols Number of columns to display (minimum of 65 when $width is non-null)
- * @param null $width (Deprecated) Width of the element; if a value is passed, the minimum value for $cols will be 65. Value is otherwise ignored.
- * @param null $height (Deprecated) Height of the element; if a value is passe, the minimum value for $rows will be 10. Value is otherwise ignored.
- * @param string $name Name to use for the textarea element.
- * @param string $value Initial content to display in the textarea.
- * @param int $obsolete deprecated
- * @param bool $return If false, will output string. If true, will return string value.
- * @param string $id CSS ID to add to the textarea element.
- * @return string|void depending on the value of $return
- */
-function print_textarea($unused, $rows, $cols, $width, $height, $name, $value='', $obsolete=0, $return=false, $id='') {
-    /// $width and height are legacy fields and no longer used as pixels like they used to be.
-    /// However, you can set them to zero to override the mincols and minrows values below.
-
-    // Disabling because there is not yet a viable $OUTPUT option for cases when mforms can't be used
-    debugging('print_textarea() is deprecated. Please use $OUTPUT->print_textarea() instead.', DEBUG_DEVELOPER);
-
-    global $OUTPUT;
-
-    $mincols = 65;
-    $minrows = 10;
-
-    if ($id === '') {
-        $id = 'edit-'.$name;
-    }
-
-    if ($height && ($rows < $minrows)) {
-        $rows = $minrows;
-    }
-    if ($width && ($cols < $mincols)) {
-        $cols = $mincols;
-    }
-
-    $textarea = $OUTPUT->print_textarea($name, $id, $value, $rows, $cols);
-    if ($return) {
-        return $textarea;
-    }
-
-    echo $textarea;
+ */
+function print_textarea() {
+    throw new coding_exception(
+        'print_textarea() has been removed. Please use $OUTPUT->print_textarea() instead.'
+    );
 }
 
 /**
@@ -2686,542 +2623,133 @@ function message_delete_message() {
 }
 
 /**
- * Get all of the allowed types for all of the courses and groups
- * the logged in user belongs to.
- *
- * The returned array will optionally have 5 keys:
- *      'user' : true if the logged in user can create user events
- *      'site' : true if the logged in user can create site events
- *      'category' : array of course categories that the user can create events for
- *      'course' : array of courses that the user can create events for
- *      'group': array of groups that the user can create events for
- *      'groupcourses' : array of courses that the groups belong to (can
- *                       be different from the list in 'course'.
  * @deprecated since 3.6
- * @return array The array of allowed types.
  */
 function calendar_get_all_allowed_types() {
-    debugging('calendar_get_all_allowed_types() is deprecated. Please use calendar_get_allowed_types() instead.',
-        DEBUG_DEVELOPER);
-
-    global $CFG, $USER, $DB;
-
-    require_once($CFG->libdir . '/enrollib.php');
-
-    $types = [];
-
-    $allowed = new stdClass();
-
-    calendar_get_allowed_types($allowed);
-
-    if ($allowed->user) {
-        $types['user'] = true;
-    }
-
-    if ($allowed->site) {
-        $types['site'] = true;
-    }
-
-    if (core_course_category::has_manage_capability_on_any()) {
-        $types['category'] = core_course_category::make_categories_list('moodle/category:manage');
-    }
-
-    // This function warms the context cache for the course so the calls
-    // to load the course context in calendar_get_allowed_types don't result
-    // in additional DB queries.
-    $courses = calendar_get_default_courses(null, 'id, groupmode, groupmodeforce', true);
-
-    // We want to pre-fetch all of the groups for each course in a single
-    // query to avoid calendar_get_allowed_types from hitting the DB for
-    // each separate course.
-    $groups = groups_get_all_groups_for_courses($courses);
-
-    foreach ($courses as $course) {
-        $coursegroups = isset($groups[$course->id]) ? $groups[$course->id] : null;
-        calendar_get_allowed_types($allowed, $course, $coursegroups);
-
-        if (!empty($allowed->courses)) {
-            $types['course'][$course->id] = $course;
-        }
-
-        if (!empty($allowed->groups)) {
-            $types['groupcourses'][$course->id] = $course;
-
-            if (!isset($types['group'])) {
-                $types['group'] = array_values($allowed->groups);
-            } else {
-                $types['group'] = array_merge($types['group'], array_values($allowed->groups));
-            }
-        }
-    }
+    throw new coding_exception(
+        'calendar_get_all_allowed_types() has been removed. Please use calendar_get_allowed_types() instead.'
+    );
 
-    return $types;
 }
 
 /**
- * Gets array of all groups in a set of course.
- *
- * @category group
- * @param array $courses Array of course objects or course ids.
- * @return array Array of groups indexed by course id.
+ * @deprecated since Moodle 3.6.
  */
-function groups_get_all_groups_for_courses($courses) {
-    global $DB;
-
-    if (empty($courses)) {
-        return [];
-    }
-
-    $groups = [];
-    $courseids = [];
-
-    foreach ($courses as $course) {
-        $courseid = is_object($course) ? $course->id : $course;
-        $groups[$courseid] = [];
-        $courseids[] = $courseid;
-    }
-
-    $groupfields = [
-        'g.id as gid',
-        'g.courseid',
-        'g.idnumber',
-        'g.name',
-        'g.description',
-        'g.descriptionformat',
-        'g.enrolmentkey',
-        'g.picture',
-        'g.hidepicture',
-        'g.timecreated',
-        'g.timemodified'
-    ];
-
-    $groupsmembersfields = [
-        'gm.id as gmid',
-        'gm.groupid',
-        'gm.userid',
-        'gm.timeadded',
-        'gm.component',
-        'gm.itemid'
-    ];
-
-    $concatidsql = $DB->sql_concat_join("'-'", ['g.id', 'COALESCE(gm.id, 0)']) . ' AS uniqid';
-    list($courseidsql, $params) = $DB->get_in_or_equal($courseids);
-    $groupfieldssql = implode(',', $groupfields);
-    $groupmembersfieldssql = implode(',', $groupsmembersfields);
-    $sql = "SELECT {$concatidsql}, {$groupfieldssql}, {$groupmembersfieldssql}
-              FROM {groups} g
-         LEFT JOIN {groups_members} gm
-                ON gm.groupid = g.id
-             WHERE g.courseid {$courseidsql}";
-
-    $results = $DB->get_records_sql($sql, $params);
-
-    // The results will come back as a flat dataset thanks to the left
-    // join so we will need to do some post processing to blow it out
-    // into a more usable data structure.
-    //
-    // This loop will extract the distinct groups from the result set
-    // and add it's list of members to the object as a property called
-    // 'members'. Then each group will be added to the result set indexed
-    // by it's course id.
-    //
-    // The resulting data structure for $groups should be:
-    // $groups = [
-    //      '1' = [
-    //          '1' => (object) [
-    //              'id' => 1,
-    //              <rest of group properties>
-    //              'members' => [
-    //                  '1' => (object) [
-    //                      <group member properties>
-    //                  ],
-    //                  '2' => (object) [
-    //                      <group member properties>
-    //                  ]
-    //              ]
-    //          ],
-    //          '2' => (object) [
-    //              'id' => 2,
-    //              <rest of group properties>
-    //              'members' => [
-    //                  '1' => (object) [
-    //                      <group member properties>
-    //                  ],
-    //                  '3' => (object) [
-    //                      <group member properties>
-    //                  ]
-    //              ]
-    //          ]
-    //      ]
-    // ]
-    //
-    foreach ($results as $key => $result) {
-        $groupid = $result->gid;
-        $courseid = $result->courseid;
-        $coursegroups = $groups[$courseid];
-        $groupsmembersid = $result->gmid;
-        $reducefunc = function($carry, $field) use ($result) {
-            // Iterate over the groups properties and pull
-            // them out into a separate object.
-            list($prefix, $field) = explode('.', $field);
-
-            if (property_exists($result, $field)) {
-                $carry[$field] = $result->{$field};
-            }
-
-            return $carry;
-        };
-
-        if (isset($coursegroups[$groupid])) {
-            $group = $coursegroups[$groupid];
-        } else {
-            $initial = [
-                'id' => $groupid,
-                'members' => []
-            ];
-            $group = (object) array_reduce(
-                $groupfields,
-                $reducefunc,
-                $initial
-            );
-        }
-
-        if (!empty($groupsmembersid)) {
-            $initial = ['id' => $groupsmembersid];
-            $groupsmembers = (object) array_reduce(
-                $groupsmembersfields,
-                $reducefunc,
-                $initial
-            );
-
-            $group->members[$groupsmembers->userid] = $groupsmembers;
-        }
-
-        $coursegroups[$groupid] = $group;
-        $groups[$courseid] = $coursegroups;
-    }
-
-    return $groups;
+function groups_get_all_groups_for_courses() {
+    throw new coding_exception(
+        'groups_get_all_groups_for_courses() has been removed and can not be used anymore.'
+    );
 }
 
 /**
- * Gets the capabilities that have been cached in the database for this
- * component.
  * @deprecated since Moodle 3.6. Please use the Events 2 API.
- * @todo final deprecation. To be removed in Moodle 4.0
- *
- * @access protected To be used from eventslib only
- *
- * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
- * @return array of events
  */
-function events_get_cached($component) {
-    global $DB;
-
-    debugging('Events API using $handlers array has been deprecated in favour of Events 2 API, please use it instead.',
-            DEBUG_DEVELOPER);
-
-    $cachedhandlers = array();
-
-    if ($storedhandlers = $DB->get_records('events_handlers', array('component'=>$component))) {
-        foreach ($storedhandlers as $handler) {
-            $cachedhandlers[$handler->eventname] = array (
-                'id'              => $handler->id,
-                'handlerfile'     => $handler->handlerfile,
-                'handlerfunction' => $handler->handlerfunction,
-                'schedule'        => $handler->schedule,
-                'internal'        => $handler->internal);
-        }
-    }
-
-    return $cachedhandlers;
+function events_get_cached() {
+    throw new coding_exception(
+        'Events API using $handlers array has been removed in favour of Events 2 API, please use it instead.'
+    );
 }
 
 /**
- * Remove all event handlers and queued events
  * @deprecated since Moodle 3.6. Please use the Events 2 API.
- * @todo final deprecation. To be removed in Moodle 4.0
- *
- * @category event
- * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
  */
-function events_uninstall($component) {
-    debugging('Events API using $handlers array has been deprecated in favour of Events 2 API, please use it instead.',
-            DEBUG_DEVELOPER);
-    $cachedhandlers = events_get_cached($component);
-    events_cleanup($component, $cachedhandlers);
-
-    events_get_handlers('reset');
+function events_uninstall() {
+    throw new coding_exception(
+        'Events API using $handlers array has been removed in favour of Events 2 API, please use it instead.'
+    );
 }
 
 /**
- * Deletes cached events that are no longer needed by the component.
  * @deprecated since Moodle 3.6. Please use the Events 2 API.
- * @todo final deprecation. To be removed in Moodle 4.0
- *
- * @access protected To be used from eventslib only
- *
- * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
- * @param array $cachedhandlers array of the cached events definitions that will be
- * @return int number of unused handlers that have been removed
  */
-function events_cleanup($component, $cachedhandlers) {
-    global $DB;
-    debugging('Events API using $handlers array has been deprecated in favour of Events 2 API, please use it instead.',
-            DEBUG_DEVELOPER);
-    $deletecount = 0;
-    foreach ($cachedhandlers as $eventname => $cachedhandler) {
-        if ($qhandlers = $DB->get_records('events_queue_handlers', array('handlerid'=>$cachedhandler['id']))) {
-            //debugging("Removing pending events from queue before deleting of event handler: $component - $eventname");
-            foreach ($qhandlers as $qhandler) {
-                events_dequeue($qhandler);
-            }
-        }
-        $DB->delete_records('events_handlers', array('eventname'=>$eventname, 'component'=>$component));
-        $deletecount++;
-    }
-
-    return $deletecount;
+function events_cleanup() {
+    throw new coding_exception(
+        'Events API using $handlers array has been removed in favour of Events 2 API, please use it instead.'
+    );
 }
 
 /**
- * Removes this queued handler from the events_queued_handler table
- *
- * Removes events_queue record from events_queue if no more references to this event object exists
  * @deprecated since Moodle 3.6. Please use the Events 2 API.
- * @todo final deprecation. To be removed in Moodle 4.0
- *
- * @access protected To be used from eventslib only
- *
- * @param stdClass $qhandler A row from the events_queued_handler table
  */
-function events_dequeue($qhandler) {
-    global $DB;
-    debugging('Events API using $handlers array has been deprecated in favour of Events 2 API, please use it instead.',
-            DEBUG_DEVELOPER);
-    // first delete the queue handler
-    $DB->delete_records('events_queue_handlers', array('id'=>$qhandler->id));
-
-    // if no more queued handler is pointing to the same event - delete the event too
-    if (!$DB->record_exists('events_queue_handlers', array('queuedeventid'=>$qhandler->queuedeventid))) {
-        $DB->delete_records('events_queue', array('id'=>$qhandler->queuedeventid));
-    }
+function events_dequeue() {
+    throw new coding_exception(
+        'Events API using $handlers array has been removed in favour of Events 2 API, please use it instead.'
+    );
 }
 
 /**
- * Returns handlers for given event. Uses caching for better perf.
  * @deprecated since Moodle 3.6. Please use the Events 2 API.
- * @todo final deprecation. To be removed in Moodle 4.0
- *
- * @access protected To be used from eventslib only
- *
- * @staticvar array $handlers
- * @param string $eventname name of event or 'reset'
- * @return array|false array of handlers or false otherwise
  */
-function events_get_handlers($eventname) {
-    global $DB;
-    static $handlers = array();
-    debugging('Events API using $handlers array has been deprecated in favour of Events 2 API, please use it instead.',
-            DEBUG_DEVELOPER);
-
-    if ($eventname === 'reset') {
-        $handlers = array();
-        return false;
-    }
-
-    if (!array_key_exists($eventname, $handlers)) {
-        $handlers[$eventname] = $DB->get_records('events_handlers', array('eventname'=>$eventname));
-    }
-
-    return $handlers[$eventname];
+function events_get_handlers() {
+    throw new coding_exception(
+        'Events API using $handlers array has been removed in favour of Events 2 API, please use it instead.'
+    );
 }
 
 /**
- * This function finds the roles assigned directly to this context only
- * i.e. no roles in parent contexts
- *
  * @deprecated since Moodle 3.6. Please use the get_roles_used_in_context().
- * @todo final deprecation. To be removed in Moodle 4.0
- * @param context $context
- * @return array
  */
-function get_roles_on_exact_context(context $context) {
-    debugging('get_roles_on_exact_context() is deprecated, please use get_roles_used_in_context() instead.',
-        DEBUG_DEVELOPER);
-
-    return get_roles_used_in_context($context, false);
+function get_roles_on_exact_context() {
+    throw new coding_exception(
+        'get_roles_on_exact_context() has been removed, please use get_roles_used_in_context() instead.'
+    );
 }
 
 /**
- * Find out which roles has assignment on this context
- *
  * @deprecated since Moodle 3.6. Please use the get_roles_used_in_context().
- * @todo final deprecation. To be removed in Moodle 4.0
- * @param context $context
- * @return array
  */
-function get_roles_with_assignment_on_context(context $context) {
-    debugging('get_roles_with_assignment_on_context() is deprecated, please use get_roles_used_in_context() instead.',
-        DEBUG_DEVELOPER);
-
-    return get_roles_used_in_context($context, false);
+function get_roles_with_assignment_on_context() {
+    throw new coding_exception(
+        'get_roles_with_assignment_on_context() has been removed, please use get_roles_used_in_context() instead.'
+    );
 }
 
 /**
- * Add the selected user as a contact for the current user
- *
  * @deprecated since Moodle 3.6
- * @param int $contactid the ID of the user to add as a contact
- * @param int $blocked 1 if you wish to block the contact
- * @param int $userid the user ID of the user we want to add the contact for, defaults to current user if not specified.
- * @return bool/int false if the $contactid isnt a valid user id. True if no changes made.
- *                  Otherwise returns the result of update_record() or insert_record()
- */
-function message_add_contact($contactid, $blocked = 0, $userid = 0) {
-    debugging('message_add_contact() is deprecated. Please use \core_message\api::create_contact_request() instead. ' .
+ */
+function message_add_contact() {
+    throw new coding_exception(
+        'message_add_contact() has been removed. Please use \core_message\api::create_contact_request() instead. ' .
         'If you wish to block or unblock a user please use \core_message\api::is_blocked() and ' .
-        '\core_message\api::block_user() or \core_message\api::unblock_user() respectively.', DEBUG_DEVELOPER);
-
-    global $USER, $DB;
-
-    if (!$DB->record_exists('user', array('id' => $contactid))) {
-        return false;
-    }
-
-    if (empty($userid)) {
-        $userid = $USER->id;
-    }
-
-    // Check if a record already exists as we may be changing blocking status.
-    if (\core_message\api::is_contact($userid, $contactid)) {
-        $isblocked = \core_message\api::is_blocked($userid, $contactid);
-        // Check if blocking status has been changed.
-        if ($isblocked != $blocked) {
-            if ($blocked == 1) {
-                if (!$isblocked) {
-                    \core_message\api::block_user($userid, $contactid);
-                }
-            } else {
-                \core_message\api::unblock_user($userid, $contactid);
-            }
-
-            return true;
-        } else {
-            // No change to blocking status.
-            return true;
-        }
-    } else {
-        if ($blocked == 1) {
-            if (!\core_message\api::is_blocked($userid, $contactid)) {
-                \core_message\api::block_user($userid, $contactid);
-            }
-        } else {
-            \core_message\api::unblock_user($userid, $contactid);
-            if (!\core_message\api::does_contact_request_exist($userid, $contactid)) {
-                \core_message\api::create_contact_request($userid, $contactid);
-            }
-        }
-
-        return true;
-    }
+        '\core_message\api::block_user() or \core_message\api::unblock_user() respectively.'
+    );
 }
 
 /**
- * Remove a contact.
- *
  * @deprecated since Moodle 3.6
- * @param int $contactid the user ID of the contact to remove
- * @param int $userid the user ID of the user we want to remove the contacts for, defaults to current user if not specified.
- * @return bool returns the result of delete_records()
  */
-function message_remove_contact($contactid, $userid = 0) {
-    debugging('message_remove_contact() is deprecated. Please use \core_message\api::remove_contact() instead.',
-        DEBUG_DEVELOPER);
-
-    global $USER;
-
-    if (empty($userid)) {
-        $userid = $USER->id;
-    }
-
-    \core_message\api::remove_contact($userid, $contactid);
-
-    return true;
+function message_remove_contact() {
+    throw new coding_exception(
+        'message_remove_contact() has been removed. Please use \core_message\api::remove_contact() instead.'
+    );
 }
 
 /**
- * Unblock a contact.
- *
  * @deprecated since Moodle 3.6
- * @param int $contactid the user ID of the contact to unblock
- * @param int $userid the user ID of the user we want to unblock the contact for, defaults to current user
- *  if not specified.
- * @return bool returns the result of delete_records()
  */
-function message_unblock_contact($contactid, $userid = 0) {
-    debugging('message_unblock_contact() is deprecated. Please use \core_message\api::unblock_user() instead.',
-        DEBUG_DEVELOPER);
-
-    global $DB, $USER;
-
-    if (!$DB->record_exists('user', array('id' => $contactid))) {
-        return false;
-    }
-
-    if (empty($userid)) {
-        $userid = $USER->id;
-    }
-
-    \core_message\api::unblock_user($userid, $contactid);
-
-    return true;
+function message_unblock_contact() {
+    throw new coding_exception(
+        'message_unblock_contact() has been removed. Please use \core_message\api::unblock_user() instead.'
+    );
 }
 
 /**
- * Block a user.
- *
  * @deprecated since Moodle 3.6
- * @param int $contactid the user ID of the user to block
- * @param int $userid the user ID of the user we want to unblock the contact for, defaults to current user
- *  if not specified.
- * @return bool
  */
-function message_block_contact($contactid, $userid = 0) {
-    debugging('message_block_contact() is deprecated. Please use \core_message\api::is_blocked() and ' .
-        '\core_message\api::block_user() instead.', DEBUG_DEVELOPER);
-
-    global $DB, $USER;
-
-    if (!$DB->record_exists('user', array('id' => $contactid))) {
-        return false;
-    }
-
-    if (empty($userid)) {
-        $userid = $USER->id;
-    }
-
-    if (!\core_message\api::is_blocked($userid, $contactid)) {
-        \core_message\api::block_user($userid, $contactid);
-    }
-
-    return true;
+function message_block_contact() {
+    throw new coding_exception(
+        'message_block_contact() has been removed. Please use \core_message\api::is_blocked() and ' .
+        '\core_message\api::block_user() instead.'
+    );
 }
 
 /**
- * Load a user's contact record
- *
  * @deprecated since Moodle 3.6
- * @param int $contactid the user ID of the user whose contact record you want
- * @return array message contacts
  */
-function message_get_contact($contactid) {
-    debugging('message_get_contact() is deprecated. Please use \core_message\api::get_contact() instead.',
-        DEBUG_DEVELOPER);
-
-    global $USER;
-
-    return \core_message\api::get_contact($USER->id, $contactid);
+function message_get_contact() {
+    throw new coding_exception(
+        'message_get_contact() has been removed. Please use \core_message\api::get_contact() instead.'
+    );
 }
 
 /**
diff --git a/lib/form/htmleditor.php b/lib/form/htmleditor.php
deleted file mode 100644 (file)
index 1d6d4b1..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-
-/**
- * htmleditor type form element
- *
- * Contains HTML class for htmleditor type element
- *
- * @deprecated since 3.6
- * @package   core_form
- * @copyright 2006 Jamie Pratt <me@jamiep.org>
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-global $CFG;
-require_once("$CFG->libdir/form/textarea.php");
-
-/**
- * htmleditor type form element
- *
- * HTML class for htmleditor type element
- *
- * @package   core_form
- * @category  form
- * @copyright 2006 Jamie Pratt <me@jamiep.org>
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class MoodleQuickForm_htmleditor extends MoodleQuickForm_textarea{
-    /** @var string defines the type of editor */
-    var $_type;
-
-    /** @var array default options for html editor, which can be overridden */
-    var $_options=array('rows'=>10, 'cols'=>45, 'width'=>0,'height'=>0);
-
-    /**
-     * Constructor
-     *
-     * @param string $elementName (optional) name of the html editor
-     * @param string $elementLabel (optional) editor label
-     * @param array $options set of options to create html editor
-     * @param array $attributes (optional) Either a typical HTML attribute string
-     *              or an associative array
-     */
-    public function __construct($elementName=null, $elementLabel=null, $options=array(), $attributes=null){
-        debugging("The form element 'htmleditor' has been deprecated. Please use the 'editor' element instead.", DEBUG_DEVELOPER);
-
-        parent::__construct($elementName, $elementLabel, $attributes);
-        // set the options, do not bother setting bogus ones
-        if (is_array($options)) {
-            foreach ($options as $name => $value) {
-                if (array_key_exists($name, $this->_options)) {
-                    if (is_array($value) && is_array($this->_options[$name])) {
-                        $this->_options[$name] = @array_merge($this->_options[$name], $value);
-                    } else {
-                        $this->_options[$name] = $value;
-                    }
-                }
-            }
-        }
-        $this->_type='htmleditor';
-
-        editors_head_setup();
-    }
-
-    /**
-     * Old syntax of class constructor. Deprecated in PHP7.
-     *
-     * @deprecated since Moodle 3.1
-     */
-    public function MoodleQuickForm_htmleditor($elementName=null, $elementLabel=null, $options=array(), $attributes=null) {
-        debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
-        self::__construct($elementName, $elementLabel, $options, $attributes);
-    }
-
-    /**
-     * Returns the input field in HTML
-     *
-     * @return string
-     */
-    public function toHtml() {
-        global $OUTPUT;
-
-        if ($this->_flagFrozen) {
-            return $this->getFrozenHtml();
-        } else {
-            $value = preg_replace("/(\r\n|\n|\r)/", '&#010;', $this->getValue());
-
-            return $this->_getTabs() .
-                $OUTPUT->print_textarea($this->getName(), $this->getAttribute('id'), $value, $this->_options['rows'],
-                    $this->_options['cols']);
-        }
-    }
-
-    /**
-     * What to display when element is frozen.
-     *
-     * @return string
-     */
-    function getFrozenHtml()
-    {
-        $html = format_text($this->getValue());
-        return $html . $this->_getPersistantData();
-    }
-}
index 74a7226..879a2aa 100644 (file)
@@ -3373,7 +3373,6 @@ MoodleQuickForm::registerElementType('grading', "$CFG->libdir/form/grading.php",
 MoodleQuickForm::registerElementType('group', "$CFG->libdir/form/group.php", 'MoodleQuickForm_group');
 MoodleQuickForm::registerElementType('header', "$CFG->libdir/form/header.php", 'MoodleQuickForm_header');
 MoodleQuickForm::registerElementType('hidden', "$CFG->libdir/form/hidden.php", 'MoodleQuickForm_hidden');
-MoodleQuickForm::registerElementType('htmleditor', "$CFG->libdir/form/htmleditor.php", 'MoodleQuickForm_htmleditor');
 MoodleQuickForm::registerElementType('listing', "$CFG->libdir/form/listing.php", 'MoodleQuickForm_listing');
 MoodleQuickForm::registerElementType('defaultcustom', "$CFG->libdir/form/defaultcustom.php", 'MoodleQuickForm_defaultcustom');
 MoodleQuickForm::registerElementType('modgrade', "$CFG->libdir/form/modgrade.php", 'MoodleQuickForm_modgrade');
index 4aa3b9a..5a2b1ef 100644 (file)
@@ -551,7 +551,7 @@ function get_exception_info($ex) {
     // Remove some absolute paths from message and debugging info.
     $searches = array();
     $replaces = array();
-    $cfgnames = array('tempdir', 'cachedir', 'localcachedir', 'themedir', 'dataroot', 'dirroot');
+    $cfgnames = array('backuptempdir', 'tempdir', 'cachedir', 'localcachedir', 'themedir', 'dataroot', 'dirroot');
     foreach ($cfgnames as $cfgname) {
         if (property_exists($CFG, $cfgname)) {
             $searches[] = $CFG->$cfgname;
@@ -1968,11 +1968,7 @@ class bootstrap_renderer {
     public static function early_error_content($message, $moreinfourl, $link, $backtrace, $debuginfo = null) {
         global $CFG;
 
-        $content = '<div style="margin-top: 6em; margin-left:auto; margin-right:auto; color:#990000; text-align:center; font-size:large; border-width:1px;
-border-color:black; background-color:#ffffee; border-style:solid; border-radius: 20px; border-collapse: collapse;
-width: 80%; -moz-border-radius: 20px; padding: 15px">
-' . $message . '
-</div>';
+        $content = "<div class='alert-danger'>$message</div>";
         // Check whether debug is set.
         $debug = (!empty($CFG->debug) && $CFG->debug >= DEBUG_DEVELOPER);
         // Also check we have it set in the config file. This occurs if the method to read the config table from the
@@ -2117,6 +2113,8 @@ width: 80%; -moz-border-radius: 20px; padding: 15px">
      * @return string html page
      */
     public static function plain_page($title, $content, $meta = '') {
+        global $CFG;
+
         if (function_exists('get_string') && function_exists('get_html_lang')) {
             $htmllang = get_html_lang();
         } else {
@@ -2131,12 +2129,11 @@ width: 80%; -moz-border-radius: 20px; padding: 15px">
             }
         }
 
-        return '<!DOCTYPE html>
-<html ' . $htmllang . '>
-<head>
-<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-'.$meta.'
-<title>' . $title . '</title>
-</head><body>' . $content . $footer . '</body></html>';
+        ob_start();
+        include($CFG->dirroot . '/error/plainpage.php');
+        $html = ob_get_contents();
+        ob_end_clean();
+
+        return $html;
     }
 }
index 8e28ab4..78bade9 100644 (file)
@@ -1306,8 +1306,7 @@ class flexible_table {
 
             if (array_key_exists($sortby, $sortdata)) {
                 // This key already exists somewhere. Change its sortorder and bring it to the top.
-                //$sortorder = $sortdata[$sortby] = $sortorder;
-                unset($sortdata['sortby'][$sortby]);
+                unset($sortdata[$sortby]);
             }
             $sortdata = array_merge([$sortby => $sortorder], $sortdata);
         }
index e9284f7..8906527 100644 (file)
@@ -41,59 +41,6 @@ use Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
  */
 class behat_deprecated extends behat_base {
 
-    /**
-     * Click link in navigation tree that matches the text in parentnode/s (seperated using greater-than character if more than one)
-     *
-     * @throws ExpectationException
-     * @param string $nodetext navigation node to click.
-     * @param string $parentnodes comma seperated list of parent nodes.
-     * @return void
-     * @deprecated since Moodle 3.6 MDL-57281 - please do not use this definition step any more.
-     * @todo MDL-63004 This will be deleted in Moodle 4.0.
-     */
-    public function i_navigate_to_node_in($nodetext, $parentnodes) {
-        $alternative[] = 'I navigate to "PATH" in current page administration';
-        $alternative[] = 'I navigate to "PATH" in site administration';
-        $alternative[] = 'I navigate to "TAB1 > TAB2" in the course gradebook';
-        $alternative[] = 'I navigate to course participants';
-        $alternative[] = 'If some items are not available without Navigation block at all, one can use combination of:
-                              I add the "Navigation" block if not present
-                              I click on "LINK" "link" in the "Navigation" "block"';
-
-        $this->deprecated_message($alternative);
-
-        $parentnodes = array_map('trim', explode('>', $parentnodes));
-        $nodelist = array_merge($parentnodes, [$nodetext]);
-        $firstnode = array_shift($nodelist);
-
-        if ($firstnode === get_string('administrationsite')) {
-            $this->execute('behat_theme_boost_behat_navigation::i_select_from_flat_navigation_drawer',
-                    array(get_string('administrationsite')));
-            $this->execute('behat_theme_boost_behat_navigation::select_on_administration_page', array($nodelist));
-            return;
-        }
-
-        if ($firstnode === get_string('sitepages')) {
-            if ($nodetext === get_string('calendar', 'calendar')) {
-                $this->execute('behat_theme_boost_behat_navigation::i_select_from_flat_navigation_drawer',
-                        array(($nodetext)));
-            } else {
-                // TODO MDL-57120 other links under "Site pages" are not accessible without navigation block.
-                $this->execute('behat_theme_boost_behat_navigation::select_node_in_navigation',
-                        array($nodetext, $parentnodes));
-            }
-            return;
-        }
-
-        if ($firstnode === get_string('courseadministration')) {
-            // Administration menu is available only on the main course page where settings in Administration
-            // block (original purpose of the step) are available on every course page.
-            $this->execute('behat_theme_boost_behat_navigation::go_to_main_course_page', array());
-        }
-
-        $this->execute('behat_theme_boost_behat_navigation::select_from_administration_menu', array($nodelist));
-    }
-
     /**
      * Docks a block. Editing mode should be previously enabled.
      * @throws ExpectationException
index 2ba7b37..1ffcb14 100644 (file)
@@ -68,6 +68,9 @@ class behat_hooks extends behat_base {
      */
     protected static $initprocessesfinished = false;
 
+    /** @var bool Whether the first javascript scenario has been seen yet */
+    protected static $firstjavascriptscenarioseen = false;
+
     /**
      * @var bool Scenario running
      */
@@ -110,39 +113,22 @@ class behat_hooks extends behat_base {
     protected static $scenariotags;
 
     /**
-     * Hook to capture BeforeSuite event so as to give access to moodle codebase.
-     * This will try and catch any exception and exists if anything fails.
+     * Gives access to moodle codebase, ensures all is ready and sets up the test lock.
+     *
+     * Includes config.php to use moodle codebase with $CFG->behat_* instead of $CFG->prefix and $CFG->dataroot, called
+     * once per suite.
      *
-     * @param BeforeSuiteScope $scope scope passed by event fired before suite.
      * @BeforeSuite
+     * @param BeforeSuiteScope $scope scope passed by event fired before suite.
      */
     public static function before_suite_hook(BeforeSuiteScope $scope) {
+        global $CFG;
+
         // If behat has been initialised then no need to do this again.
-        if (self::$initprocessesfinished) {
+        if (!self::is_first_scenario()) {
             return;
         }
 
-        try {
-            self::before_suite($scope);
-        } catch (behat_stop_exception $e) {
-            echo $e->getMessage() . PHP_EOL;
-            exit(1);
-        }
-    }
-
-    /**
-     * Gives access to moodle codebase, ensures all is ready and sets up the test lock.
-     *
-     * Includes config.php to use moodle codebase with $CFG->behat_*
-     * instead of $CFG->prefix and $CFG->dataroot, called once per suite.
-     *
-     * @param BeforeSuiteScope $scope scope passed by event fired before suite.
-     * @static
-     * @throws behat_stop_exception
-     */
-    public static function before_suite(BeforeSuiteScope $scope) {
-        global $CFG;
-
         // Defined only when the behat CLI command is running, the moodle init setup process will
         // read this value and switch to $CFG->behat_dataroot and $CFG->behat_prefix instead of
         // the normal site.
@@ -170,8 +156,7 @@ class behat_hooks extends behat_base {
         // before each scenario (accidental user deletes) in the BeforeScenario hook.
 
         if (!behat_util::is_test_mode_enabled()) {
-            throw new behat_stop_exception('Behat only can run if test mode is enabled. More info in ' .
-                behat_command::DOCS_URL);
+            self::log_and_stop('Behat only can run if test mode is enabled. More info in ' .  behat_command::DOCS_URL);
         }
 
         // Reset all data, before checking for check_server_status.
@@ -179,20 +164,28 @@ class behat_hooks extends behat_base {
         behat_util::clean_tables_updated_by_scenario_list();
         behat_util::reset_all_data();
 
-        // Check if server is running and using same version for cli and apache.
+        // Check if the web server is running and using same version for cli and apache.
         behat_util::check_server_status();
 
         // Prevents using outdated data, upgrade script would start and tests would fail.
         if (!behat_util::is_test_data_updated()) {
             $commandpath = 'php admin/tool/behat/cli/init.php';
-            throw new behat_stop_exception("Your behat test site is outdated, please run\n\n    " .
-                    $commandpath . "\n\nfrom your moodle dirroot to drop and install the behat test site again.");
+            $message = <<<EOF
+Your behat test site is outdated, please run the following command from your Moodle dirroot to drop, and reinstall the Behat test site.
+
+    {$comandpath}
+
+EOF;
+            self::log_and_stop($message);
         }
+
         // Avoid parallel tests execution, it continues when the previous lock is released.
         test_lock::acquire('behat');
 
         if (!empty($CFG->behat_faildump_path) && !is_writable($CFG->behat_faildump_path)) {
-            throw new behat_stop_exception('You set $CFG->behat_faildump_path to a non-writable directory');
+            self::log_and_stop(
+                "The \$CFG->behat_faildump_path value is set to a non-writable directory ({$CFG->behat_faildump_path})."
+            );
         }
 
         // Handle interrupts on PHP7.
@@ -204,6 +197,25 @@ class behat_hooks extends behat_base {
         }
     }
 
+    /**
+     * Run final tests before running the suite.
+     *
+     * @BeforeSuite
+     * @param BeforeSuiteScope $scope scope passed by event fired before suite.
+     */
+    public static function before_suite_final_checks(BeforeSuiteScope $scope) {
+        $happy = defined('BEHAT_TEST');
+        $happy = $happy && defined('BEHAT_SITE_RUNNING');
+        $happy = $happy && php_sapi_name() == 'cli';
+        $happy = $happy && behat_util::is_test_mode_enabled();
+        $happy = $happy && behat_util::is_test_site();
+
+        if (!$happy) {
+            error_log('Behat only can modify the test database and the test dataroot!');
+            exit(1);
+        }
+    }
+
     /**
      * Gives access to moodle codebase, to keep track of feature start time.
      *
@@ -260,56 +272,108 @@ class behat_hooks extends behat_base {
     }
 
     /**
-     * Hook to capture before scenario event to get scope.
+     * Helper function to restart the Mink session.
+     */
+    protected function restart_session(): void {
+        $session = $this->getSession();
+        if ($session->isStarted()) {
+            $session->restart();
+        } else {
+            $session->start();
+        }
+        if ($this->running_javascript() && $this->getSession()->getDriver()->getWebDriverSessionId() === 'session') {
+            throw new DriverException('Unable to create a valid session');
+        }
+    }
+
+    /**
+     * Restart the session before each non-javascript scenario.
      *
+     * @BeforeScenario @~javascript
      * @param BeforeScenarioScope $scope scope passed by event fired before scenario.
-     * @BeforeScenario
      */
-    public function before_scenario_hook(BeforeScenarioScope $scope) {
-        try {
-            $this->before_scenario($scope);
-        } catch (behat_stop_exception $e) {
-            echo $e->getMessage() . PHP_EOL;
-            exit(1);
+    public function before_goutte_scenarios(BeforeScenarioScope $scope) {
+        if ($this->running_javascript()) {
+            // A bug in the BeforeScenario filtering prevents the @~javascript filter on this hook from working
+            // properly.
+            // See https://github.com/Behat/Behat/issues/1235 for further information.
+            return;
         }
+
+        $this->restart_session();
     }
 
     /**
-     * Resets the test environment.
+     * Start the session before the first javascript scenario.
      *
+     * This is treated slightly differently to try to capture when Selenium is not running at all.
+     *
+     * @BeforeScenario @javascript
      * @param BeforeScenarioScope $scope scope passed by event fired before scenario.
-     * @throws behat_stop_exception If here we are not using the test database it should be because of a coding error
      */
-    public function before_scenario(BeforeScenarioScope $scope) {
-        global $DB, $CFG;
-
-        // As many checks as we can.
-        if (!defined('BEHAT_TEST') ||
-               !defined('BEHAT_SITE_RUNNING') ||
-               php_sapi_name() != 'cli' ||
-               !behat_util::is_test_mode_enabled() ||
-               !behat_util::is_test_site()) {
-            throw new behat_stop_exception('Behat only can modify the test database and the test dataroot!');
+    public function before_first_scenario_start_session(BeforeScenarioScope $scope) {
+        if (!self::is_first_javascript_scenario()) {
+            // The first Scenario has started.
+            // The `before_subsequent_scenario_start_session` function will restart the session instead.
+            return;
         }
+        self::$firstjavascriptscenarioseen = true;
+
+        $docsurl = behat_command::DOCS_URL;
+        $driverexceptionmsg = <<<EOF
+
+The Selenium or WebDriver server is not running. You must start it to run tests that involve Javascript.
+See {$docsurl} for more information.
+
+The following debugging information is available:
+
+EOF;
+
 
-        $moreinfo = 'More info in ' . behat_command::DOCS_URL;
-        $driverexceptionmsg = 'Selenium server is not running, you need to start it to run tests that involve Javascript. ' . $moreinfo;
         try {
-            $session = $this->getSession();
-            if (!$session->isStarted()) {
-                $session->start();
-            }
-        } catch (CurlExec $e) {
-            // Exception thrown by WebDriver, so only @javascript tests will be caugth; in
-            // behat_util::check_server_status() we already checked that the server is running.
-            throw new behat_stop_exception($driverexceptionmsg);
-        } catch (DriverException $e) {
-            throw new behat_stop_exception($driverexceptionmsg);
+            $this->restart_session();
+        } catch (CurlExec | DriverException $e) {
+            // The CurlExec Exception is thrown by WebDriver.
+            self::log_and_stop(
+                $driverexceptionmsg . '. ' .
+                $e->getMessage() . "\n\n" .
+                format_backtrace($e->getTrace(), true)
+            );
         } catch (UnknownError $e) {
             // Generic 'I have no idea' Selenium error. Custom exception to provide more feedback about possible solutions.
-            throw new behat_stop_exception($e->getMessage());
+            self::log_and_stop(
+                $e->getMessage() . "\n\n" .
+                format_backtrace($e->getTrace(), true)
+            );
+        }
+    }
+
+    /**
+     * Start the session before each javascript scenario.
+     *
+     * Note: Before the first scenario the @see before_first_scenario_start_session() function is used instead.
+     *
+     * @BeforeScenario @javascript
+     * @param BeforeScenarioScope $scope scope passed by event fired before scenario.
+     */
+    public function before_subsequent_scenario_start_session(BeforeScenarioScope $scope) {
+        if (self::is_first_javascript_scenario()) {
+            // The initial init has not yet finished.
+            // The `before_first_scenario_start_session` function will have started the session instead.
+            return;
         }
 
+        $this->restart_session();
+    }
+
+    /**
+     * Resets the test environment.
+     *
+     * @BeforeScenario
+     * @param BeforeScenarioScope $scope scope passed by event fired before scenario.
+     */
+    public function before_scenario_hook(BeforeScenarioScope $scope) {
+        global $DB;
         $suitename = $scope->getSuite()->getName();
 
         // Register behat selectors for theme, if suite is changed. We do it for every suite change.
@@ -343,9 +407,6 @@ class behat_hooks extends behat_base {
 
         }
 
-        // Reset mink session between the scenarios.
-        $session->reset();
-
         // Reset $SESSION.
         \core\session\manager::init_empty_session();
 
@@ -698,6 +759,15 @@ class behat_hooks extends behat_base {
         return !(self::$initprocessesfinished);
     }
 
+    /**
+     * Returns whether the first scenario of the suite is running
+     *
+     * @return bool
+     */
+    protected static function is_first_javascript_scenario(): bool {
+        return !self::$firstjavascriptscenarioseen;
+    }
+
     /**
      * Register a set of component selectors.
      *
@@ -737,20 +807,19 @@ class behat_hooks extends behat_base {
      * @param BeforeStepScope $scope
      * @BeforeStep
      */
-    public function first_step_setup_complete(BeforeStepScope $scope) {
+    public function first_step_setup_complete(BeforeStepScope $scope): void {
         self::$initprocessesfinished = true;
     }
 
-}
+    /**
+     * Log a notification, and then exit.
+     *
+     * @param   string $message The content to dispaly
+     */
+    protected static function log_and_stop(string $message): void {
+        error_log($message);
+
+        exit(1);
+    }
 
-/**
- * Behat stop exception
- *
- * This exception is thrown from before suite or scenario if any setup problem found.
- *
- * @package    core_test
- * @copyright  2016 Rajesh Taneja <rajesh@moodle.com>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class behat_stop_exception extends \Exception {
 }
index 3eed52f..052fe30 100644 (file)
@@ -194,7 +194,7 @@ class behat_navigation extends behat_base {
             // We just want to expand the node, we don't want to follow it.
             $node = $node->getParent();
         }
-        $node->click();
+        $this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
     }
 
     /**
@@ -218,7 +218,7 @@ class behat_navigation extends behat_base {
             // We just want to expand the node, we don't want to follow it.
             $node = $node->getParent();
         }
-        $node->click();
+        $this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
     }
 
     /**
@@ -245,7 +245,7 @@ class behat_navigation extends behat_base {
                 // don't wait, it is non-JS and we already waited for the DOM.
                 $siteadminlink = $this->getSession()->getPage()->find('named_exact', array('link', "'" . $siteadminstr . "'"));
                 if ($siteadminlink) {
-                    $siteadminlink->click();
+                    $this->execute('behat_general::i_click_on', [$siteadminlink, 'NodeElement']);
                 }
             }
         }
@@ -302,7 +302,7 @@ class behat_navigation extends behat_base {
             throw new ExpectationException('Navigation node "' . $nodetext . '" not found under "' .
                 implode(' > ', $parentnodes) . '"', $this->getSession());
         }
-        $nodetoclick->click();
+        $this->execute('behat_general::i_click_on', [$nodetoclick, 'NodeElement']);
     }
 
     /**
@@ -791,9 +791,8 @@ class behat_navigation extends behat_base {
         $node = $this->find('xpath', $xpath);
         $expanded = $node->getAttribute('aria-expanded');
         if ($expanded === 'false') {
-            $node->click();
+            $this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
             $this->ensure_node_attribute_is_set($node, 'aria-expanded', 'true');
-            $this->wait_for_pending_js();
         }
     }
 
@@ -812,8 +811,7 @@ class behat_navigation extends behat_base {
         $node = $this->find('xpath', $xpath);
         $expanded = $node->getAttribute('aria-expanded');
         if ($expanded === 'true') {
-            $node->click();
-            $this->wait_for_pending_js();
+            $this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
         }
     }
 
@@ -835,8 +833,8 @@ class behat_navigation extends behat_base {
     protected function go_to_main_course_page() {
         $url = $this->getSession()->getCurrentUrl();
         if (!preg_match('|/course/view.php\?id=[\d]+$|', $url)) {
-            $this->find('xpath', '//header//div[@id=\'page-navbar\']//a[contains(@href,\'/course/view.php?id=\')]')->click();
-            $this->execute('behat_general::wait_until_the_page_is_ready');
+            $node = $this->find('xpath', '//header//div[@id=\'page-navbar\']//a[contains(@href,\'/course/view.php?id=\')]');
+            $this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
         }
     }
 
@@ -856,8 +854,8 @@ class behat_navigation extends behat_base {
             $tabxpath = '//ul[@role=\'tablist\']/li/a[contains(normalize-space(.), ' . $tabname . ')]';
             if ($node = $this->getSession()->getPage()->find('xpath', $tabxpath)) {
                 if ($this->running_javascript()) {
+                    $this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
                     // Click on the tab and add 'active' tab to the xpath.
-                    $node->click();
                     $xpath .= '//div[contains(@class,\'active\')]';
                 } else {
                     // Add the tab content selector to the xpath.
@@ -881,8 +879,7 @@ class behat_navigation extends behat_base {
         if (!$node = $this->getSession()->getPage()->find('xpath', $xpath)) {
             throw new ElementNotFoundException($this->getSession(), 'Link "' . join(' > ', $nodelist) . '"');
         }
-        $node->click();
-        $this->wait_for_pending_js();
+        $this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
     }
 
     /**
@@ -929,8 +926,8 @@ class behat_navigation extends behat_base {
             $menuxpath = $this->find_header_administration_menu() ?: $this->find_page_administration_menu();
         }
         if ($menuxpath && $this->running_javascript()) {
-            $this->find('xpath', $menuxpath . '//a[@data-toggle=\'dropdown\']')->click();
-            $this->wait_for_pending_js();
+            $node = $this->find('xpath', $menuxpath . '//a[@data-toggle=\'dropdown\']');
+            $this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
         }
     }
 
@@ -952,15 +949,14 @@ class behat_navigation extends behat_base {
             $isheader = false;
         }
 
-        $this->toggle_page_administration_menu($menuxpath);
+        $this->execute('behat_navigation::toggle_page_administration_menu', [$menuxpath]);
 
         if (!$isheader || count($nodelist) == 1) {
             $lastnode = end($nodelist);
             $linkname = behat_context_helper::escape($lastnode);
             $link = $this->getSession()->getPage()->find('xpath', $menuxpath . '//a[contains(normalize-space(.), ' . $linkname . ')]');
             if ($link) {
-                $link->click();
-                $this->wait_for_pending_js();
+                $this->execute('behat_general::i_click_on', [$link, 'NodeElement']);
                 return;
             }
         }
@@ -970,8 +966,7 @@ class behat_navigation extends behat_base {
             $linkname = behat_context_helper::escape(get_string('morenavigationlinks'));
             $link = $this->getSession()->getPage()->find('xpath', $menuxpath . '//a[contains(normalize-space(.), ' . $linkname . ')]');
             if ($link) {
-                $link->click();
-                $this->execute('behat_general::wait_until_the_page_is_ready');
+                $this->execute('behat_general::i_click_on', [$link, 'NodeElement']);
                 $this->select_on_administration_page($nodelist);
                 return;
             }
index 8c094f3..9e71470 100644 (file)
@@ -596,7 +596,7 @@ class core_event_testcase extends advanced_testcase {
         events_update_definition('unittest');
 
         $DB->delete_records_select('events_handlers', "component <> 'unittest'");
-        events_get_handlers('reset');
+
         $this->assertDebuggingCalled(self::DEBUGGING_MSG, DEBUG_DEVELOPER);
         $this->assertEquals(3, $DB->count_records('events_handlers'));
         set_config('loglifetime', 60*60*24*5);
index 47a9fec..2064621 100644 (file)
@@ -70,35 +70,35 @@ class lock_testcase extends advanced_testcase {
             $this->assertNotEmpty($lock1, 'Get a lock');
 
             if ($lockfactory->supports_timeout()) {
-                if ($lockfactory->supports_recursion()) {
-                    $lock2 = $lockfactory->get_lock('abc', 2);
+                // Attempt to obtain a lock within a 2 sec timeout.
+                $durationlock2 = -microtime(true);
+                $lock2 = $lockfactory->get_lock('abc', 2);
+                $durationlock2 += microtime(true);
+
+                if (!$lock2) { // If the lock was not obtained.
+                    $this->assertFalse($lock2, 'Cannot get a stacked lock');
+                    // This should timeout after 2 seconds.
+                    $this->assertTrue($durationlock2 < 2.5, 'Lock should timeout after no more than 2 seconds');
+                } else {
                     $this->assertNotEmpty($lock2, 'Get a stacked lock');
                     $this->assertTrue($lock2->release(), 'Release a stacked lock');
+                }
+
+                // Attempt to obtain a lock within a 0 sec timeout.
+                $durationlock2 = -microtime(true);
+                $lock2 = $lockfactory->get_lock('abc', 0);
+                $durationlock2 += microtime(true);
 
+                if (!$lock2) { // If the lock was not obtained.
+                    // This should timeout almost instantly.
+                    $this->assertTrue($durationlock2 < 0.100, 'Lock should timeout almost instantly < 100ms');
+                } else {
                     // This stacked lock should be gained almost instantly.
-                    $duration = -microtime(true);
-                    $lock3 = $lockfactory->get_lock('abc', 0);
-                    $duration += microtime(true);
-                    $lock3->release();
-                    $this->assertTrue($duration < 0.100, 'Lock should be gained almost instantly');
+                    $this->assertTrue($durationlock2 < 0.100, 'Lock should be gained almost instantly');
+                    $lock2->release();
 
                     // We should also assert that locks fail instantly if locked
                     // from another process but this is hard to unit test.
-
-                } else {
-                    // This should timeout after 2 seconds.
-                    $duration = -microtime(true);
-                    $lock2 = $lockfactory->get_lock('abc', 2);
-                    $duration += microtime(true);
-                    $this->assertFalse($lock2, 'Cannot get a stacked lock');
-                    $this->assertTrue($duration < 2.5, 'Lock should timeout after no more than 2 seconds');
-
-                    // This should timeout almost instantly.
-                    $duration = -microtime(true);
-                    $lock2 = $lockfactory->get_lock('abc', 0);
-                    $duration += microtime(true);
-                    $this->assertFalse($lock2, 'Cannot get a stacked lock');
-                    $this->assertTrue($duration < 0.100, 'Lock should timeout almost instantly < 100ms');
                 }
             }
             // Release the lock.
index 6e3ce5b..1e6c3b3 100644 (file)
@@ -79,7 +79,7 @@ class core_setuplib_testcase extends advanced_testcase {
         global $CFG;
 
         // This doesn't test them all possible ones, but these are set for unit tests.
-        $cfgnames = array('dataroot', 'dirroot', 'tempdir', 'cachedir', 'localcachedir');
+        $cfgnames = array('dataroot', 'dirroot', 'tempdir', 'backuptempdir', 'cachedir', 'localcachedir');
 
         $fixture  = '';
         $expected = '';
index fd1f97d..aa64123 100644 (file)
@@ -8,6 +8,27 @@ information provided here is intended especially for developers.
   of 'scrollable' is true.
 * The `$CFG->behat_retart_browser_after` configuration setting has been removed.
   The browser session is now restarted between all tests.
+* add_to_log() has been through final deprecation, please rewrite your code to the new events API.
+* The following functions have been finally deprecated and can not be used anymore:
+  - print_textarea
+  - calendar_get_all_allowed_types
+  - groups_get_all_groups_for_courses
+  - events_get_cached
+  - events_uninstall
+  - events_cleanup
+  - events_dequeue
+  - events_get_handlers
+  - get_roles_on_exact_context
+  - get_roles_with_assignment_on_context
+  - message_add_contact
+  - message_remove_contact
+  - message_unblock_contact
+  - message_block_contact
+  - message_get_contact
+* The following renamed classes have been completely removed:
+    - course_in_list (now: core_course_list_element)
+    - coursecat (now: core_course_category)
+* The form element 'htmleditor', which was deprecated in 3.6, has been removed.
 
 === 3.9 ===
 * Following function has been deprecated, please use \core\task\manager::run_from_cli().
index c7d3d2b..29ef0e7 100644 (file)
@@ -2285,6 +2285,11 @@ function send_headers($contenttype, $cacheable = true) {
     if (empty($CFG->allowframembedding) && !core_useragent::is_moodle_app()) {
         @header('X-Frame-Options: sameorigin');
     }
+
+    // If referrer policy is set, add a referrer header.
+    if (!empty($CFG->referrerpolicy) && ($CFG->referrerpolicy !== 'default')) {
+        @header('Referrer-Policy: ' . $CFG->referrerpolicy);
+    }
 }
 
 /**
index 3e101d5..d942821 100644 (file)
@@ -292,6 +292,7 @@ class core_message_external extends external_api {
      * Create contacts.
      *
      * @deprecated since Moodle 3.6
+     * TODO: MDL-63261
      * @param array $userids array of user IDs.
      * @param int $userid The id of the user we are creating the contacts for
      * @return external_description
@@ -323,7 +324,7 @@ class core_message_external extends external_api {
 
         $warnings = array();
         foreach ($params['userids'] as $id) {
-            if (!message_add_contact($id, 0, $params['userid'])) {
+            if (!\core_message\api::create_contact_request($params['userid'], $id)) {
                 $warnings[] = array(
                     'item' => 'user',
                     'itemid' => $id,
@@ -682,6 +683,7 @@ class core_message_external extends external_api {
      * Block contacts.
      *
      * @deprecated since Moodle 3.6
+     * TODO: MDL-63261
      * @param array $userids array of user IDs.
      * @param int $userid The id of the user we are blocking the contacts for
      * @return external_description
@@ -713,7 +715,7 @@ class core_message_external extends external_api {
 
         $warnings = array();
         foreach ($params['userids'] as $id) {
-            if (!message_block_contact($id, $params['userid'])) {
+            if (!\core_message\api::block_user($params['userid'], $id)) {
                 $warnings[] = array(
                     'item' => 'user',
                     'itemid' => $id,
@@ -798,7 +800,7 @@ class core_message_external extends external_api {
         }
 
         foreach ($params['userids'] as $id) {
-            message_unblock_contact($id, $params['userid']);
+            core_message\api::unblock_user($params['userid'], $id);
         }
 
         return null;
index 1f14399..9a61444 100644 (file)
@@ -340,6 +340,8 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
     /**
      * Test create_contacts.
+     *
+     * TODO: MDL-63261
      */
     public function test_create_contacts() {
         $this->resetAfterTest(true);
@@ -351,41 +353,17 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $user5 = self::getDataGenerator()->create_user();
         $this->setUser($user1);
 
-        // Adding a contact.
-        $return = core_message_external::create_contacts(array($user2->id));
-        $this->assertDebuggingCalled();
-        $return = external_api::clean_returnvalue(core_message_external::create_contacts_returns(), $return);
-        $this->assertEquals(array(), $return);
-
         // Adding a contact who is already a contact.
         $return = core_message_external::create_contacts(array($user2->id));
-        $this->assertDebuggingCalled();
         $return = external_api::clean_returnvalue(core_message_external::create_contacts_returns(), $return);
         $this->assertEquals(array(), $return);
 
         // Adding multiple contacts.
         $return = core_message_external::create_contacts(array($user3->id, $user4->id));
-        $this->assertDebuggingCalledCount(2);
         $return = external_api::clean_returnvalue(core_message_external::create_contacts_returns(), $return);
         $this->assertEquals(array(), $return);
 
-        // Adding a non-existing user.
-        $return = core_message_external::create_contacts(array(99999));
-        $this->assertDebuggingCalled();
-        $return = external_api::clean_returnvalue(core_message_external::create_contacts_returns(), $return);
-        $this->assertCount(1, $return);
-        $return = array_pop($return);
-        $this->assertEquals($return['warningcode'], 'contactnotcreated');
-        $this->assertEquals($return['itemid'], 99999);
-
-        // Adding contacts with valid and invalid parameters.
-        $return = core_message_external::create_contacts(array($user5->id, 99999));
-        $this->assertDebuggingCalledCount(2);
-        $return = external_api::clean_returnvalue(core_message_external::create_contacts_returns(), $return);
-        $this->assertCount(1, $return);
-        $return = array_pop($return);
-        $this->assertEquals($return['warningcode'], 'contactnotcreated');
-        $this->assertEquals($return['itemid'], 99999);
+        // Note: We should add real user checks in api L:2656.
 
         // Try to add a contact to another user, should throw an exception.
         // All assertions must be added before this point.
@@ -455,36 +433,36 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         \core_message\api::add_contact($user1->id, $user4->id);
         \core_message\api::add_contact($user1->id, $user5->id);
 
-        // Blocking a contact.
-        $return = core_message_external::block_contacts(array($user2->id));
-        $this->assertDebuggingCalled();
-        $return = external_api::clean_returnvalue(core_message_external::block_contacts_returns(), $return);
-        $this->assertEquals(array(), $return);
-
         // Blocking a contact who is already a contact.
         $return = core_message_external::block_contacts(array($user2->id));
-        $this->assertDebuggingCalled();
         $return = external_api::clean_returnvalue(core_message_external::block_contacts_returns(), $return);
-        $this->assertEquals(array(), $return);
+        $this->assertEquals(array(array(
+            'item' => 'user',
+            'itemid' => $user2->id,
+            'warningcode' => 'contactnotblocked',
+            'message' => 'The contact could not be blocked'
+        )), $return);
 
         // Blocking multiple contacts.
         $return = core_message_external::block_contacts(array($user3->id, $user4->id));
-        $this->assertDebuggingCalledCount(2);
         $return = external_api::clean_returnvalue(core_message_external::block_contacts_returns(), $return);
-        $this->assertEquals(array(), $return);
+        $this->assertEquals(array(
+            array(
+                'item' => 'user',
+                'itemid' => $user3->id,
+                'warningcode' => 'contactnotblocked',
+                'message' => 'The contact could not be blocked'
+            ),
+            array(
+                'item' => 'user',
+                'itemid' => $user4->id,
+                'warningcode' => 'contactnotblocked',
+                'message' => 'The contact could not be blocked'
+            )
+        ), $return);
 
         // Blocking a non-existing user.
         $return = core_message_external::block_contacts(array(99999));
-        $this->assertDebuggingCalled();
-        $return = external_api::clean_returnvalue(core_message_external::block_contacts_returns(), $return);
-        $this->assertCount(1, $return);
-        $return = array_pop($return);
-        $this->assertEquals($return['warningcode'], 'contactnotblocked');
-        $this->assertEquals($return['itemid'], 99999);
-
-        // Blocking contacts with valid and invalid parameters.
-        $return = core_message_external::block_contacts(array($user5->id, 99999));
-        $this->assertDebuggingCalledCount(2);
         $return = external_api::clean_returnvalue(core_message_external::block_contacts_returns(), $return);
         $this->assertCount(1, $return);
         $return = array_pop($return);
@@ -518,34 +496,28 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Removing a non-contact.
         $return = core_message_external::unblock_contacts(array($user2->id));
-        $this->assertDebuggingCalled();
         $this->assertNull($return);
 
         // Removing one contact.
         $return = core_message_external::unblock_contacts(array($user3->id));
-        $this->assertDebuggingCalled();
         $this->assertNull($return);
 
         // Removing multiple contacts.
         $return = core_message_external::unblock_contacts(array($user4->id, $user5->id));
-        $this->assertDebuggingCalledCount(2);
         $this->assertNull($return);
 
         // Removing contact from unexisting user.
         $return = core_message_external::unblock_contacts(array(99999));
-        $this->assertDebuggingCalled();
         $this->assertNull($return);
 
         // Removing mixed valid and invalid data.
         $return = core_message_external::unblock_contacts(array($user6->id, 99999));
-        $this->assertDebuggingCalledCount(2);
         $this->assertNull($return);
 
         // Try to unblock a contact of another user contact list, should throw an exception.
         // All assertions must be added before this point.
         $this->expectException('required_capability_exception');
         core_message_external::unblock_contacts(array($user2->id), $user3->id);
-        $this->assertDebuggingCalled();
     }
 
     /**
@@ -1392,7 +1364,6 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->assertCount(1, $contacts['online']);
         $this->assertCount(3, $contacts['strangers']);
         core_message_external::block_contacts(array($user_blocked->id));
-        $this->assertDebuggingCalled();
         $contacts = core_message_external::get_contacts();
         $contacts = external_api::clean_returnvalue(core_message_external::get_contacts_returns(), $contacts);
         $this->assertCount(3, $contacts['offline']);
@@ -1822,7 +1793,6 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Block the $userblocked and retrieve again the list.
         core_message_external::block_contacts(array($userblocked->id));
-        $this->assertDebuggingCalled();
         $blockedusers = core_message_external::get_blocked_users($user1->id);
         $blockedusers = external_api::clean_returnvalue(core_message_external::get_blocked_users_returns(), $blockedusers);
         $this->assertCount(1, $blockedusers['users']);
index 8b0e8fd..c02a610 100644 (file)
@@ -223,102 +223,6 @@ class core_message_messagelib_testcase extends advanced_testcase {
         $this->assertEquals(0, message_count_unread_messages($userfrom));
     }
 
-    /**
-     * Test message_add_contact.
-     */
-    public function test_message_add_contact() {
-        global $DB, $USER;
-
-        // Set this user as the admin.
-        $this->setAdminUser();
-
-        // Create a user to add to the admin's contact list.
-        $user1 = $this->getDataGenerator()->create_user();
-        $user2 = $this->getDataGenerator()->create_user();
-
-        message_add_contact($user1->id);
-        $this->assertDebuggingCalled();
-        $this->assertEquals(1, $DB->count_records('message_contact_requests'));
-
-        message_add_contact($user2->id, 1);
-        $this->assertDebuggingCalled();
-        $this->assertEquals(1, $DB->count_records('message_users_blocked'));
-
-        message_add_contact($user2->id, 0);
-        $this->assertDebuggingCalled();
-        $this->assertEquals(0, $DB->count_records('message_users_blocked'));
-    }
-
-    /**
-     * Test message_remove_contact.
-     */
-    public function test_message_remove_contact() {
-        global $USER;
-
-        // Set this user as the admin.
-        $this->setAdminUser();
-
-        // Create a user to add to the admin's contact list.
-        $user = $this->getDataGenerator()->create_user();
-
-        // Add the user to the admin's contact list.
-        \core_message\api::add_contact($USER->id, $user->id);
-
-        // Remove user from admin's contact list.
-        message_remove_contact($user->id);
-        $this->assertDebuggingCalled();
-        $this->assertEquals(false, message_get_contact($user->id));
-        $this->assertDebuggingCalled();
-    }
-
-    /**
-     * Test message_block_contact.
-     */
-    public function test_message_block_contact() {
-        global $USER;
-
-        // Set this user as the admin.
-        $this->setAdminUser();
-
-        // Create a user to add to the admin's contact list.
-        $user1 = $this->getDataGenerator()->create_user();
-        $user2 = $this->getDataGenerator()->create_user();
-
-        // Add users to the admin's contact list.
-        \core_message\api::add_contact($USER->id, $user1->id);
-        \core_message\api::add_contact($USER->id, $user2->id);
-
-        $this->assertEquals(0, \core_message\api::count_blocked_users());
-
-        // Block 1 user.
-        message_block_contact($user2->id);
-        $this->assertDebuggingCalled();
-        $this->assertEquals(1, \core_message\api::count_blocked_users());
-
-    }
-
-    /**
-     * Test message_unblock_contact.
-     */
-    public function test_message_unblock_contact() {
-        global $USER;
-
-        // Set this user as the admin.
-        $this->setAdminUser();
-
-        // Create a user to add to the admin's contact list.
-        $user1 = $this->getDataGenerator()->create_user();
-
-        // Add users to the admin's blocked list.
-        \core_message\api::block_user($USER->id, $user1->id);
-        $this->assertEquals(1, \core_message\api::count_blocked_users());
-
-        // Unblock user.
-        message_unblock_contact($user1->id);
-        $this->assertDebuggingCalled();
-        $this->assertEquals(0, \core_message\api::count_blocked_users());
-    }
-
     /**
      * Test message_search_users.
      */
index be6b0a1..86b4151 100644 (file)
@@ -98,6 +98,7 @@ class convert_submissions extends scheduled_task {
             }
 
             mtrace('Convert ' . count($users) . ' submission attempt(s) for assignment ' . $assignmentid);
+            $conversionrequirespolling = false;
 
             foreach ($users as $userid) {
                 try {
@@ -107,6 +108,7 @@ class convert_submissions extends scheduled_task {
                         case combined_document::STATUS_READY_PARTIAL:
                         case combined_document::STATUS_PENDING_INPUT:
                             // The document has not been converted yet or is somehow still ready.
+                            $conversionrequirespolling = true;
                             continue 2;
                     }
                     document_services::get_page_images_for_attempt(
@@ -127,7 +129,9 @@ class convert_submissions extends scheduled_task {
             }
 
             // Remove from queue.
-            $DB->delete_records('assignfeedback_editpdf_queue', array('id' => $record->id));
+            if (!$conversionrequirespolling) {
+                $DB->delete_records('assignfeedback_editpdf_queue', array('id' => $record->id));
+            }
 
         }
     }
index 56b2dbc..8676816 100644 (file)
     * none
 
     Context variables required for this template:
+    * id
     * hascapabilitygroups
     * state
     ** pending|configured|rejected|unknown
     ** text
     * courseid
     * urls
-    ** course
+    ** course - optional, required if courseid > 0
     ** icon
+    ** edit
     ** publickeyset
     ** accesstoken
     ** authrequest
     * deploymentid
     * instancecount
 
+    Example context (json):
+    {
+        "id": 1,
+        "name": "Example Tool",
+        "description": "This is an example tool",
+        "platformid": "https:\/\/moodle.example.com",
+        "courseid": 0,
+        "hascapabilitygroups": false,
+        "state": {
+            "text": "Active",
+            "pending": false,
+            "configured": true,
+            "rejected": false,
+            "unknown": false
+        },
+        "urls": {
+            "icon": "https://www.example.com/icon1.png",
+            "edit": "https://www.example.com/mod/lti/typessettings.php",
+            "publickeyset": "https:\/\/moodle.example.com\/mod\/lti\/certs.php",
+            "accesstoken": "https:\/\/moodle.example.com\/mod\/lti\/token.php",
+            "authrequest": "https:\/\/moodle.example.com\/mod\/lti\/auth.php"
+        },
+        "clientid": null,
+        "deploymentid": 1,
+        "instancecount": 0
+    }
+
 }}
 <div class="tool-card" data-type-id="{{id}}" data-uniqid="{{uniqid}}"
     data-modaltitle="{{#str}} tooldetailsmodaltitle, mod_lti {{/str}}"
                     <a class="delete" href="#" title="{{#str}} delete {{/str}}">{{#pix}} t/delete, core, {{#str}} delete {{/str}}{{/pix}}</a>
                 </div>
             </div>
-            <img class="tool-card-icon" src="{{{urls.icon}}}" alt="{{name}}"></img>
+            <img class="tool-card-icon" src="{{{urls.icon}}}" alt="{{name}}">
             <div class="contenteditable-container">
                 <h4 class="name" contenteditable="true">{{name}}</h4>
                 <div class="overlay-container">{{> mod_lti/loader }}</div>
index 5c1cfb3..d5f1285 100644 (file)
@@ -81,27 +81,19 @@ class report extends \mod_scorm\report {
                         && ($attemptsmode != SCORM_REPORT_ATTEMPTS_STUDENTS_WITH_NO);
         // Select the students.
         $nostudents = false;
-
+        list($allowedlistsql, $params) = get_enrolled_sql($contextmodule, 'mod/scorm:savetrack', (int) $currentgroup);
         if (empty($currentgroup)) {
             // All users who can attempt scoes.
-            if (!$students = get_users_by_capability($contextmodule, 'mod/scorm:savetrack', 'u.id', '', '', '', '', '', false)) {
+            if (!$DB->record_exists_sql($allowedlistsql, $params)) {
                 echo $OUTPUT->notification(get_string('nostudentsyet'));
                 $nostudents = true;
-                $allowedlist = '';
-            } else {
-                $allowedlist = array_keys($students);
             }
-            unset($students);
         } else {
             // All users who can attempt scoes and who are in the currently selected group.
-            if (!$groupstudents = get_users_by_capability($contextmodule, 'mod/scorm:savetrack', 'u.id', '', '', '',
-                                                            $currentgroup, '', false)) {
+            if (!$DB->record_exists_sql($allowedlistsql, $params)) {
                 echo $OUTPUT->notification(get_string('nostudentsingroup'));
                 $nostudents = true;
-                $groupstudents = array();
             }
-            $allowedlist = array_keys($groupstudents);
-            unset($groupstudents);
         }
 
         if ( !$nostudents ) {
@@ -273,8 +265,6 @@ class report extends \mod_scorm\report {
                 $csvexport->set_filename($filename, ".txt");
                 $csvexport->add_data($headers);
             }
-            $params = array();
-            list($usql, $params) = $DB->get_in_or_equal($allowedlist, SQL_PARAMS_NAMED);
             // Construct the SQL.
             $select = 'SELECT DISTINCT '.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(st.attempt, 0)').' AS uniqueid, ';
             $select .= 'st.scormid AS scormid, st.attempt AS attempt, ' .
@@ -287,15 +277,15 @@ class report extends \mod_scorm\report {
             switch ($attemptsmode) {
                 case SCORM_REPORT_ATTEMPTS_STUDENTS_WITH:
                     // Show only students with attempts.
-                    $where = ' WHERE u.id ' .$usql. ' AND st.userid IS NOT NULL';
+                    $where = " WHERE u.id IN ({$allowedlistsql}) AND st.userid IS NOT NULL";
                     break;
                 case SCORM_REPORT_ATTEMPTS_STUDENTS_WITH_NO:
                     // Show only students without attempts.
-                    $where = ' WHERE u.id ' .$usql. ' AND st.userid IS NULL';
+                    $where = " WHERE u.id IN ({$allowedlistsql}) AND st.userid IS NULL";
                     break;
                 case SCORM_REPORT_ATTEMPTS_ALL_STUDENTS:
                     // Show all students with or without attempts.
-                    $where = ' WHERE u.id ' .$usql. ' AND (st.userid IS NOT NULL OR st.userid IS NULL)';
+                    $where = " WHERE u.id IN ({$allowedlistsql}) AND (st.userid IS NOT NULL OR st.userid IS NULL)";
                     break;
             }
 
index 4971d5c..b295f00 100644 (file)
@@ -50,18 +50,15 @@ class report extends \mod_scorm\report {
      * Get the data for the report.
      *
      * @param int $scoid The sco ID.
-     * @param array $allowedlist The list of user IDs allowed to be displayed.
+     * @param array $allowedlist The SQL and params to get the userlist.
      * @return array of data indexed per bar.
      */
-    protected function get_data($scoid, $allowedlist = []) {
+    protected function get_data($scoid, $allowedlistsql) {
         global $DB;
         $data = array_fill(0, self::BANDS, 0);
-        if (empty($allowedlist)) {
-            return $data;
-        }
 
-        list($usql, $params) = $DB->get_in_or_equal($allowedlist);
-        $params[] = $scoid;
+        list($allowedlist, $params) = $allowedlistsql;
+        $params = array_merge($params, ['scoid' => $scoid]);
 
         // Construct the SQL.
         $sql = "SELECT DISTINCT " . $DB->sql_concat('st.userid', '\'#\'', 'COALESCE(st.attempt, 0)') . " AS uniqueid,
@@ -70,7 +67,7 @@ class report extends \mod_scorm\report {
                        st.attempt AS attempt,
                        st.scoid AS scoid
                   FROM {scorm_scoes_track} st
-                 WHERE st.userid $usql AND st.scoid = ?";
+                 WHERE st.userid IN ({$allowedlist}) AND st.scoid = :scoid";
         $attempts = $DB->get_records_sql($sql, $params);
 
         $usergrades = [];
@@ -144,15 +141,7 @@ class report extends \mod_scorm\report {
 
         // Find out current restriction.
         $group = groups_get_activity_group($cm, true);
-        if (empty($group)) {
-            // All users who can attempt scoes.
-            $students = get_users_by_capability($contextmodule, 'mod/scorm:savetrack', 'u.id' , '', '', '', '', '', false);
-            $allowedlist = empty($students) ? array() : array_keys($students);
-        } else {
-            // All users who can attempt scoes and who are in the currently selected group.
-            $groupstudents = get_users_by_capability($contextmodule, 'mod/scorm:savetrack', 'u.id', '', '', '', $group, '', false);
-            $allowedlist = empty($groupstudents) ? array() : array_keys($groupstudents);
-        }
+        $allowedlistsql = get_enrolled_sql($contextmodule, 'mod/scorm:savetrack', (int) $group);
 
         // Labels.
         $labels = [get_string('invaliddata', 'scormreport_graphs')];
@@ -164,7 +153,7 @@ class report extends \mod_scorm\report {
             foreach ($scoes as $sco) {
                 if ($sco->launch != '') {
 
-                    $data = $this->get_data($sco->id, $allowedlist);
+                    $data = $this->get_data($sco->id, $allowedlistsql);
                     $series = new chart_series($sco->title, $data);
 
                     $chart = new chart_bar();
index 1deef30..1bc5afb 100644 (file)
@@ -98,28 +98,19 @@ class report extends \mod_scorm\report {
                 && ($attemptsmode != SCORM_REPORT_ATTEMPTS_STUDENTS_WITH_NO);
         // Select the students.
         $nostudents = false;
-
+        list($allowedlistsql, $params) = get_enrolled_sql($contextmodule, 'mod/scorm:savetrack', (int) $currentgroup);
         if (empty($currentgroup)) {
             // All users who can attempt scoes.
-            if (!$students = get_users_by_capability($contextmodule, 'mod/scorm:savetrack', 'u.id', '', '', '', '', '', false)) {
+            if (!$DB->record_exists_sql($allowedlistsql, $params)) {
                 echo $OUTPUT->notification(get_string('nostudentsyet'));
                 $nostudents = true;
-                $allowedlist = '';
-            } else {
-                $allowedlist = array_keys($students);
             }
-            unset($students);
         } else {
             // All users who can attempt scoes and who are in the currently selected group.
-            if (!$groupstudents = get_users_by_capability($contextmodule,
-                                                            'mod/scorm:savetrack', 'u.id', '', '', '',
-                                                            $currentgroup, '', false)) {
+            if (!$DB->record_exists_sql($allowedlistsql, $params)) {
                 echo $OUTPUT->notification(get_string('nostudentsingroup'));
                 $nostudents = true;
-                $groupstudents = array();
             }
-            $allowedlist = array_keys($groupstudents);
-            unset($groupstudents);
         }
         if ( !$nostudents ) {
             // Now check if asked download of data.
@@ -163,8 +154,6 @@ class report extends \mod_scorm\report {
                 }
             }
 
-            $params = array();
-            list($usql, $params) = $DB->get_in_or_equal($allowedlist, SQL_PARAMS_NAMED);
             // Construct the SQL.
             $select = 'SELECT DISTINCT '.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(st.attempt, 0)').' AS uniqueid, ';
             $select .= 'st.scormid AS scormid, st.attempt AS attempt, ' .
@@ -177,15 +166,15 @@ class report extends \mod_scorm\report {
             switch ($attemptsmode) {
                 case SCORM_REPORT_ATTEMPTS_STUDENTS_WITH:
                     // Show only students with attempts.
-                    $where = ' WHERE u.id ' .$usql. ' AND st.userid IS NOT NULL';
+                    $where = " WHERE u.id IN ({$allowedlistsql}) AND st.userid IS NOT NULL";
                     break;
                 case SCORM_REPORT_ATTEMPTS_STUDENTS_WITH_NO:
                     // Show only students without attempts.
-                    $where = ' WHERE u.id ' .$usql. ' AND st.userid IS NULL';
+                    $where = " WHERE u.id IN ({$allowedlistsql}) AND st.userid IS NULL";
                     break;
                 case SCORM_REPORT_ATTEMPTS_ALL_STUDENTS:
                     // Show all students with or without attempts.
-                    $where = ' WHERE u.id ' .$usql. ' AND (st.userid IS NOT NULL OR st.userid IS NULL)';
+                    $where = " WHERE u.id IN ({$allowedlistsql}) AND (st.userid IS NOT NULL OR st.userid IS NULL)";
                     break;
             }
 
index 85488c7..4e2ad62 100644 (file)
@@ -92,28 +92,19 @@ class report extends \mod_scorm\report {
                 && ($attemptsmode != SCORM_REPORT_ATTEMPTS_STUDENTS_WITH_NO);
         // Select the students.
         $nostudents = false;
-
+        list($allowedlistsql, $params) = get_enrolled_sql($contextmodule, 'mod/scorm:savetrack', (int) $currentgroup);
         if (empty($currentgroup)) {
             // All users who can attempt scoes.
-            if (!$students = get_users_by_capability($contextmodule, 'mod/scorm:savetrack', 'u.id', '', '', '', '', '', false)) {
+            if (!$DB->record_exists_sql($allowedlistsql, $params)) {
                 echo $OUTPUT->notification(get_string('nostudentsyet'));
                 $nostudents = true;
-                $allowedlist = '';
-            } else {
-                $allowedlist = array_keys($students);
             }
-            unset($students);
         } else {
             // All users who can attempt scoes and who are in the currently selected group.
-            $groupstudents = get_users_by_capability($contextmodule, 'mod/scorm:savetrack',
-                                                     'u.id', '', '', '', $currentgroup, '', false);
-            if (!$groupstudents) {
+            if (!$DB->record_exists_sql($allowedlistsql, $params)) {
                 echo $OUTPUT->notification(get_string('nostudentsingroup'));
                 $nostudents = true;
-                $groupstudents = array();
             }
-            $allowedlist = array_keys($groupstudents);
-            unset($groupstudents);
         }
         if ( !$nostudents ) {
             // Now check if asked download of data.
@@ -157,8 +148,6 @@ class report extends \mod_scorm\report {
                 }
             }
 
-            $params = array();
-            list($usql, $params) = $DB->get_in_or_equal($allowedlist, SQL_PARAMS_NAMED);
             // Construct the SQL.
             $select = 'SELECT DISTINCT '.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(st.attempt, 0)').' AS uniqueid, ';
             $select .= 'st.scormid AS scormid, st.attempt AS attempt, ' .
@@ -171,15 +160,15 @@ class report extends \mod_scorm\report {
             switch ($attemptsmode) {
                 case SCORM_REPORT_ATTEMPTS_STUDENTS_WITH:
                     // Show only students with attempts.
-                    $where = ' WHERE u.id ' .$usql. ' AND st.userid IS NOT NULL';
+                    $where = " WHERE u.id IN ({$allowedlistsql}) AND st.userid IS NOT NULL";
                     break;
                 case SCORM_REPORT_ATTEMPTS_STUDENTS_WITH_NO:
                     // Show only students without attempts.
-                    $where = ' WHERE u.id ' .$usql. ' AND st.userid IS NULL';
+                    $where = " WHERE u.id IN ({$allowedlistsql}) AND st.userid IS NULL";
                     break;
                 case SCORM_REPORT_ATTEMPTS_ALL_STUDENTS:
                     // Show all students with or without attempts.
-                    $where = ' WHERE u.id ' .$usql. ' AND (st.userid IS NOT NULL OR st.userid IS NULL)';
+                    $where = " WHERE u.id IN ({$allowedlistsql}) AND (st.userid IS NOT NULL OR st.userid IS NULL)";
                     break;
             }
 
index 9e1953f..d315b9d 100644 (file)
Binary files a/question/type/ddimageortext/amd/build/question.min.js and b/question/type/ddimageortext/amd/build/question.min.js differ
index 1a2e53b..28b05d3 100644 (file)
Binary files a/question/type/ddimageortext/amd/build/question.min.js.map and b/question/type/ddimageortext/amd/build/question.min.js.map differ
index 248c56f..8d031bf 100644 (file)
@@ -336,6 +336,7 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
                         cloneDrag.removeClass('beingdragged');
                         cloneDrag.removeAttr('tabindex');
                         hiddenDrag.after(cloneDrag);
+                        questionManager.addEventHandlersToDrag(cloneDrag);
                         drag.offset(cloneDrag.offset());
                     } else {
                         hiddenDrag.addClass('active');
@@ -529,6 +530,7 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
                         cloneDrag.removeClass('beingdragged');
                         cloneDrag.removeAttr('tabindex');
                         hiddenDrag.after(cloneDrag);
+                        questionManager.addEventHandlersToDrag(cloneDrag);
                         nextDrag.offset(cloneDrag.offset());
                     } else {
                         hiddenDrag.addClass('active');
@@ -623,7 +625,7 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
             {
                 duration: 'fast',
                 done: function() {
-                    $('body').trigger('dragmoved', [drag, target, thisQ]);
+                    $('body').trigger('qtype_ddimageortext-dragmoved', [drag, target, thisQ]);
                     M.util.js_complete('qtype_ddimageortext-animate-' + thisQ.containerId);
                 }
             }
@@ -962,17 +964,16 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
          * Set up the event handlers that make this question type work. (Done once per page.)
          */
         setupEventHandlers: function() {
+            // We do not use the body event here to prevent the other event on Mobile device, such as scroll event.
+            questionManager.addEventHandlersToDrag($('.que.ddimageortext:not(.qtype_ddimageortext-readonly) .draghome'));
             $('body')
-                .on('mousedown touchstart',
-                    '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .draghome',
-                    questionManager.handleDragStart)
                 .on('keydown',
                     '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .dropzones .dropzone',
                     questionManager.handleKeyPress)
                 .on('keydown',
                     '.que.ddimageortext:not(.qtype_ddimageortext-readonly) .draghome.placed:not(.beingdragged)',
                     questionManager.handleKeyPress)
-                .on('dragmoved', questionManager.handleDragMoved);
+                .on('qtype_ddimageortext-dragmoved', questionManager.handleDragMoved);
             $(window).on('resize', function() {
                 questionManager.handleWindowResize(false);
             });
@@ -989,6 +990,15 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
             }, 100);
         },
 
+        /**
+         * Binding the drag/touch event again for newly created element.
+         *
+         * @param {jQuery} element Element to bind the event
+         */
+        addEventHandlersToDrag: function(element) {
+            element.on('mousedown touchstart', questionManager.handleDragStart);
+        },
+
         /**
          * Handle mouse down / touch start events on drags.
          * @param {Event} e the DOM event.
index 7f86df9..9b4d45e 100644 (file)
Binary files a/question/type/ddmarker/amd/build/question.min.js and b/question/type/ddmarker/amd/build/question.min.js differ
index 4d3ac96..8562ce1 100644 (file)
Binary files a/question/type/ddmarker/amd/build/question.min.js.map and b/question/type/ddmarker/amd/build/question.min.js.map differ
index 7dfa8ff..5d516de 100644 (file)
@@ -568,6 +568,7 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
             this.getDragClone(drag)
                 .removeClass('active')
                 .after(dragclone);
+            questionManager.addEventHandlersToMarker(dragclone);
         }
     };
 
@@ -678,31 +679,9 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
          * Set up the event handlers that make this question type work. (Done once per page.)
          */
         setupEventHandlers: function() {
-            $('body')
-                .on('mousedown touchstart',
-                    '.que.ddmarker:not(.qtype_ddmarker-readonly) div.draghomes .marker', questionManager.handleDragStart)
-                .on('mousedown touchstart',
-                    '.que.ddmarker:not(.qtype_ddmarker-readonly) div.droparea .marker', questionManager.handleDragStart)
-                .on('keydown keypress',
-                    '.que.ddmarker:not(.qtype_ddmarker-readonly) div.draghomes .marker', questionManager.handleKeyPress)
-                .on('keydown keypress',
-                    '.que.ddmarker:not(.qtype_ddmarker-readonly) div.droparea .marker', questionManager.handleKeyPress)
-                .on('focusin',
-                    '.que.ddmarker:not(.qtype_ddmarker-readonly) div.draghomes .marker', function(e) {
-                        questionManager.handleKeyboardFocus(e, true);
-                    })
-                .on('focusin',
-                    '.que.ddmarker:not(.qtype_ddmarker-readonly) div.droparea .marker', function(e) {
-                        questionManager.handleKeyboardFocus(e, true);
-                    })
-                .on('focusout',
-                    '.que.ddmarker:not(.qtype_ddmarker-readonly) div.draghomes .marker', function(e) {
-                        questionManager.handleKeyboardFocus(e, false);
-                    })
-                .on('focusout',
-                    '.que.ddmarker:not(.qtype_ddmarker-readonly) div.droparea .marker', function(e) {
-                        questionManager.handleKeyboardFocus(e, false);
-                    });
+            // We do not use the body event here to prevent the other event on Mobile device, such as scroll event.
+            questionManager.addEventHandlersToMarker($('.que.ddmarker:not(.qtype_ddmarker-readonly) div.draghomes .marker'));
+            questionManager.addEventHandlersToMarker($('.que.ddmarker:not(.qtype_ddmarker-readonly) div.droparea .marker'));
             $(window).on('resize', function() {
                 questionManager.handleWindowResize(false);
             });
@@ -719,6 +698,23 @@ define(['jquery', 'core/dragdrop', 'qtype_ddmarker/shapes', 'core/key_codes'], f
             }, 100);
         },
 
+        /**
+         * Binding the event again for newly created element.
+         *
+         * @param {jQuery} element Element to bind the event
+         */
+        addEventHandlersToMarker: function(element) {
+            element
+                .on('mousedown touchstart', questionManager.handleDragStart)
+                .on('keydown keypress', questionManager.handleKeyPress)
+                .focusin(function(e) {
+                    questionManager.handleKeyboardFocus(e, true);
+                })
+                .focusout(function(e) {
+                    questionManager.handleKeyboardFocus(e, false);
+                });
+        },
+
         /**
          * Handle mouse down / touch start events on markers.
          * @param {Event} e the DOM event.
index d526782..5aa26ef 100644 (file)
Binary files a/question/type/ddwtos/amd/build/ddwtos.min.js and b/question/type/ddwtos/amd/build/ddwtos.min.js differ
index 6e2a847..9f6a640 100644 (file)
Binary files a/question/type/ddwtos/amd/build/ddwtos.min.js.map and b/question/type/ddwtos/amd/build/ddwtos.min.js.map differ
index 2fb2eec..bad4324 100644 (file)
@@ -212,6 +212,7 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
                         var cloneDrag = drag.clone();
                         cloneDrag.removeClass('beingdragged');
                         hiddenDrag.after(cloneDrag);
+                        questionManager.addEventHandlersToDrag(cloneDrag);
                         drag.offset(cloneDrag.offset());
                     } else {
                         hiddenDrag.addClass('active');
@@ -404,6 +405,7 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
                         cloneDrag.removeClass('beingdragged');
                         cloneDrag.removeAttr('tabindex');
                         hiddenDrag.after(cloneDrag);
+                        questionManager.addEventHandlersToDrag(cloneDrag);
                         nextDrag.offset(cloneDrag.offset());
                     } else {
                         hiddenDrag.addClass('active');
@@ -498,7 +500,7 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
             {
                 duration: 'fast',
                 done: function() {
-                    $('body').trigger('dragmoved', [drag, target, thisQ]);
+                    $('body').trigger('qtype_ddwtos-dragmoved', [drag, target, thisQ]);
                     M.util.js_complete('qtype_ddwtos-animate-' + thisQ.containerId);
                 }
             }
@@ -738,16 +740,25 @@ define(['jquery', 'core/dragdrop', 'core/key_codes'], function($, dragDrop, keys
          * Set up the event handlers that make this question type work. (Done once per page.)
          */
         setupEventHandlers: function() {
-            $('body').on('mousedown touchstart',
-                '.que.ddwtos:not(.qtype_ddwtos-readonly) span.draghome',
-                questionManager.handleDragStart)
+            // We do not use the body event here to prevent the other event on Mobile device, such as scroll event.
+            questionManager.addEventHandlersToDrag($('.que.ddwtos:not(.qtype_ddwtos-readonly) span.draghome'));
+            $('body')
                 .on('keydown',
                     '.que.ddwtos:not(.qtype_ddwtos-readonly) span.drop',
                     questionManager.handleKeyPress)
                 .on('keydown',
                     '.que.ddwtos:not(.qtype_ddwtos-readonly) span.draghome.placed:not(.beingdragged)',
                     questionManager.handleKeyPress)
-                .on('dragmoved', questionManager.handleDragMoved);
+                .on('qtype_ddwtos-dragmoved', questionManager.handleDragMoved);
+        },
+
+        /**
+         * Binding the drag/touch event again for newly created element.
+         *
+         * @param {jQuery} element Element to bind the event
+         */
+        addEventHandlersToDrag: function(element) {
+            element.on('mousedown touchstart', questionManager.handleDragStart);
         },
 
         /**
index d8d20a9..6a20c2d 100644 (file)
Binary files a/report/competency/amd/build/grading_popup.min.js and b/report/competency/amd/build/grading_popup.min.js differ
index f32b8c2..c7f243c 100644 (file)
Binary files a/report/competency/amd/build/grading_popup.min.js.map and b/report/competency/amd/build/grading_popup.min.js.map differ
index 43984a4..7aeba24 100644 (file)
Binary files a/report/competency/amd/build/user_course_navigation.min.js and b/report/competency/amd/build/user_course_navigation.min.js differ
index f2dcf64..c971332 100644 (file)
Binary files a/report/competency/amd/build/user_course_navigation.min.js.map and b/report/competency/amd/build/user_course_navigation.min.js.map differ
index e395d51..a38fd92 100644 (file)
@@ -59,10 +59,9 @@ define(['jquery', 'core/notification', 'core/str', 'core/ajax', 'core/log', 'cor
             args: {userid: userId, competencyid: competencyId, courseid: courseId},
         }]);
 
-        $.when.apply($, requests).then(function(context) {
-            this._contextLoaded.bind(this)(context);
-            return;
-        }.bind(this)).catch(notification.exception);
+        $.when(requests[0], requests[1])
+        .then(this._contextLoaded.bind(this))
+        .catch(notification.exception);
     };
 
     /**
@@ -70,23 +69,37 @@ define(['jquery', 'core/notification', 'core/str', 'core/ajax', 'core/log', 'cor
      *
      * @method _contextLoaded
      * @param {Object} context
+     * @returns {Promise}
      */
     GradingPopup.prototype._contextLoaded = function(context) {
-        var self = this;
-
         // We have to display user info in popup.
         context.displayuser = true;
-        templates.render('tool_lp/user_competency_summary_in_course', context).done(function(html, js) {
-            str.get_string('usercompetencysummary', 'report_competency').done(function(title) {
-                (new Dialogue(title, html, templates.runTemplateJS.bind(templates, js), self._refresh.bind(self), true));
-            }).fail(notification.exception);
-        }).fail(notification.exception);
+
+        M.util.js_pending('report_competency/grading_popup:_contextLoaded');
+
+        return $.when(
+            str.get_string('usercompetencysummary', 'report_competency'),
+            templates.render('tool_lp/user_competency_summary_in_course', context)
+        )
+        .then(function(title, templateData) {
+            return new Dialogue(
+                title,
+                templateData[0],
+                function() {
+                    templates.runTemplateJS(templateData[1]);
+                    M.util.js_complete('report_competency/grading_popup:_contextLoaded');
+                },
+                this._refresh.bind(this),
+                true
+            );
+        }.bind(this));
     };
 
     /**
      * Refresh the page.
      *
      * @method _refresh
+     * @returns {Promise}
      */
     GradingPopup.prototype._refresh = function() {
         var region = $(this._regionSelector);
@@ -99,7 +112,7 @@ define(['jquery', 'core/notification', 'core/str', 'core/ajax', 'core/log', 'cor
             moduleId = 0;
         }
 
-        ajax.call([{
+        return ajax.call([{
             methodname: 'report_competency_data_for_report',
             args: {courseid: courseId, userid: userId, moduleid: moduleId},
             done: this._pageContextLoaded.bind(this),
@@ -114,10 +127,13 @@ define(['jquery', 'core/notification', 'core/str', 'core/ajax', 'core/log', 'cor
      * @param {Object} context
      */
     GradingPopup.prototype._pageContextLoaded = function(context) {
-        var self = this;
-        templates.render('report_competency/report', context).done(function(html, js) {
-            templates.replaceNode(self._regionSelector, html, js);
-        }).fail(notification.exception);
+        templates.render('report_competency/report', context)
+        .then(function(html, js) {
+            templates.replaceNode(this._regionSelector, html, js);
+
+            return;
+        }.bind(this))
+        .catch(notification.exception);
     };
 
     /** @type {String} The selector for the region with the user competencies */
index ae2bbc5..146094b 100644 (file)
@@ -50,6 +50,8 @@ define(['jquery'], function($) {
      * @param {Event} e the event
      */
     UserCourseNavigation.prototype._userChanged = function(e) {
+        // Note: This change causes a page reload and is intentionally not paired with a js_complete call.
+        M.util.js_pending('report_competency/user_course_navigation:_userChanged');
         var newUserId = $(e.target).val();
         var queryStr = '?user=' + newUserId + '&id=' + this._courseId + '&mod=' + this._moduleId;
         document.location = this._baseUrl + queryStr;
@@ -62,6 +64,8 @@ define(['jquery'], function($) {
      * @param {Event} e the event
      */
     UserCourseNavigation.prototype._moduleChanged = function(e) {
+        // Note: This change causes a page reload and is intentionally not paired with a js_complete call.
+        M.util.js_pending('report_competency/user_course_navigation:_moduleChanged');
         var newModuleId = $(e.target).val();
         var queryStr = '?mod=' + newModuleId + '&id=' + this._courseId + '&user=' + this._userId;
         document.location = this._baseUrl + queryStr;
index 3630be6..8aff844 100644 (file)
Binary files a/theme/boost/amd/build/pending.min.js and b/theme/boost/amd/build/pending.min.js differ
index 09b04e7..1d1b9b9 100644 (file)
Binary files a/theme/boost/amd/build/pending.min.js.map and b/theme/boost/amd/build/pending.min.js.map differ
index 4b3a548..ad036d4 100644 (file)
@@ -121,13 +121,13 @@ export default () => {
         pairs.forEach(pair => {
             const eventStart = `${pair.start}.bs.${key}`;
             const eventEnd = `${pair.end}.bs.${key}`;
-            jQuery(document.body).on(eventStart, () => {
+            jQuery(document.body).on(eventStart, e => {
                 M.util.js_pending(eventEnd);
+                jQuery(e.target).one(eventEnd, () => {
+                    M.util.js_complete(eventEnd);
+                });
             });
 
-            jQuery(document.body).on(eventEnd, () => {
-                M.util.js_complete(eventEnd);
-            });
         });
     });
 };
index acbd6c0..b74f3bd 100644 (file)
@@ -38,9 +38,21 @@ if (!$parts) {
     combo_not_found();
 }
 
-$etag = sha1($parts);
 $parts = trim($parts, '&');
 
+// Remove any duplicate parts, since each file only needs to be loaded once (which also helps reduce total file size).
+$parts = implode('&', array_unique(explode('&', $parts)));
+
+// Limit length of parts to match the YUI loader limit of 1024, to prevent loading an arbitrary number of files.
+if (strlen($parts) > 1024) {
+    $parts = substr($parts, 0, 1024);
+
+    // If the shortened $parts has been cut off mid-way through a filename, trim back to the end of the previous filename.
+    if (substr($parts, -3) !== '.js' && substr($parts, -4) !== '.css') {
+        $parts = substr($parts, 0, strrpos($parts, '&'));
+    }
+}
+
 // find out what we are serving - only one type per request
 $content = '';
 if (substr($parts, -3) === '.js') {
@@ -51,6 +63,8 @@ if (substr($parts, -3) === '.js') {
     combo_not_found();
 }
 
+$etag = sha1($parts);
+
 // if they are requesting a revision that's not -1, and they have supplied an
 // If-Modified-Since header, we can send back a 304 Not Modified since the
 // content never changes (the rev number is increased any time the content changes)
index 002a769..4216ca0 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2020070400.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2020070400.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 $release  = '4.0dev (Build: 20200704)'; // Human-friendly version name