Merge branch 'MDL-68148-master' of git://github.com/rezaies/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Wed, 8 Apr 2020 03:20:39 +0000 (11:20 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Wed, 8 Apr 2020 03:20:39 +0000 (11:20 +0800)
179 files changed:
.eslintignore
.stylelintignore
admin/settings/badges.php
admin/tasklogs.php
admin/templates/tasklogs.mustache
admin/tool/analytics/classes/output/renderer.php
admin/tool/task/classes/edit_scheduled_task_form.php
admin/tool/task/clear_fail_delay.php
admin/tool/task/lang/en/tool_task.php
admin/tool/task/renderer.php
admin/tool/task/schedule_task.php
admin/tool/task/scheduledtasks.php
admin/tool/task/tests/behat/clear_fail_delay.feature
admin/tool/task/tests/behat/manage_tasks.feature
auth/none/classes/check/noauth.php [new file with mode: 0644]
auth/none/lang/en/auth_none.php
auth/none/lib.php [new file with mode: 0644]
auth/none/version.php
backup/util/ui/renderer.php
badges/classes/assertion.php
badges/classes/badge.php
badges/issuer_json.php
badges/renderer.php
badges/tests/badgeslib_test.php
badges/tests/behat/add_badge.feature
badges/tests/behat/award_badge.feature
badges/tests/behat/award_badge_groups.feature
badges/tests/behat/criteria_activity.feature
badges/tests/behat/criteria_cohort.feature
badges/tests/behat/criteria_competency.feature
badges/tests/behat/criteria_profile.feature
badges/tests/behat/role_visibility.feature
badges/upgrade.txt
badges/upgradelib.php
blocks/admin_bookmarks/tests/behat/bookmark_admin_pages.feature
blocks/badges/block_badges.php
blocks/badges/tests/behat/block_badges_course.feature
blocks/badges/tests/behat/block_badges_dashboard.feature
blocks/badges/tests/behat/block_badges_frontpage.feature
blocks/comments/block_comments.php
blocks/private_files/block_private_files.php
blocks/rss_client/block_rss_client.php
blocks/settings/block_settings.php
course/amd/build/activitychooser.min.js
course/amd/build/activitychooser.min.js.map
course/amd/build/local/activitychooser/dialogue.min.js
course/amd/build/local/activitychooser/dialogue.min.js.map
course/amd/build/local/activitychooser/selectors.min.js
course/amd/build/local/activitychooser/selectors.min.js.map
course/amd/src/activitychooser.js
course/amd/src/local/activitychooser/dialogue.js
course/amd/src/local/activitychooser/selectors.js
course/classes/management_renderer.php
course/format/renderer.php
course/format/topics/format.js
course/format/topics/renderer.php
course/format/upgrade.txt
course/format/weeks/format.js
course/templates/activity_list.mustache
course/templates/activitychooser.mustache
course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-debug.js
course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-min.js
course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop.js
course/yui/src/dragdrop/js/resource.js
course/yui/src/dragdrop/js/section.js
grade/grading/form/guide/renderer.php
grade/report/user/renderer.php
h5p/classes/core.php
lang/en/moodle.php
lib/badgeslib.php
lib/classes/check/access/defaultuserrole.php [new file with mode: 0644]
lib/classes/check/access/frontpagerole.php [new file with mode: 0644]
lib/classes/check/access/guestrole.php [new file with mode: 0644]
lib/classes/check/access/riskadmin.php [new file with mode: 0644]
lib/classes/check/access/riskbackup.php [new file with mode: 0644]
lib/classes/check/access/riskbackup_result.php [new file with mode: 0644]
lib/classes/check/access/riskxss.php [new file with mode: 0644]
lib/classes/check/access/riskxss_result.php [new file with mode: 0644]
lib/classes/check/check.php [new file with mode: 0644]
lib/classes/check/environment/configrw.php [new file with mode: 0644]
lib/classes/check/environment/displayerrors.php [new file with mode: 0644]
lib/classes/check/environment/nodemodules.php [new file with mode: 0644]
lib/classes/check/environment/preventexecpath.php [new file with mode: 0644]
lib/classes/check/environment/unsecuredataroot.php [new file with mode: 0644]
lib/classes/check/environment/vendordir.php [new file with mode: 0644]
lib/classes/check/http/cookiesecure.php [new file with mode: 0644]
lib/classes/check/manager.php [new file with mode: 0644]
lib/classes/check/result.php [new file with mode: 0644]
lib/classes/check/security/crawlers.php [new file with mode: 0644]
lib/classes/check/security/emailchangeconfirmation.php [new file with mode: 0644]
lib/classes/check/security/embed.php [new file with mode: 0644]
lib/classes/check/security/mediafilterswf.php [new file with mode: 0644]
lib/classes/check/security/openprofiles.php [new file with mode: 0644]
lib/classes/check/security/passwordpolicy.php [new file with mode: 0644]
lib/classes/check/security/webcron.php [new file with mode: 0644]
lib/classes/task/manager.php
lib/classes/task/scheduled_task.php
lib/completionlib.php
lib/db/upgrade.php
lib/mdn-polyfills/readme_moodle.txt [deleted file]
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/polyfills/polyfill.js [moved from lib/mdn-polyfills/polyfill.js with 64% similarity]
lib/polyfills/readme_moodle.txt [new file with mode: 0644]
lib/table/amd/build/dynamic.min.js
lib/table/amd/build/dynamic.min.js.map
lib/table/amd/build/local/dynamic/repository.min.js
lib/table/amd/build/local/dynamic/repository.min.js.map
lib/table/amd/build/local/dynamic/selectors.min.js
lib/table/amd/build/local/dynamic/selectors.min.js.map
lib/table/amd/src/dynamic.js
lib/table/amd/src/local/dynamic/repository.js
lib/table/amd/src/local/dynamic/selectors.js
lib/table/classes/external/dynamic/fetch.php
lib/tablelib.php
lib/templates/check/result.mustache [new file with mode: 0644]
lib/templates/check/result/critical.mustache [new file with mode: 0644]
lib/templates/check/result/error.mustache [new file with mode: 0644]
lib/templates/check/result/info.mustache [new file with mode: 0644]
lib/templates/check/result/na.mustache [new file with mode: 0644]
lib/templates/check/result/ok.mustache [new file with mode: 0644]
lib/templates/check/result/unknown.mustache [new file with mode: 0644]
lib/templates/check/result/warning.mustache [new file with mode: 0644]
lib/templates/initials_bar.mustache
lib/tests/check_test.php [new file with mode: 0644]
lib/tests/completionlib_test.php
lib/tests/tablelib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/yui/build/moodle-core-blocks/moodle-core-blocks-debug.js
lib/yui/build/moodle-core-blocks/moodle-core-blocks-min.js
lib/yui/build/moodle-core-blocks/moodle-core-blocks.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop.js
lib/yui/src/blocks/js/manager.js
lib/yui/src/dragdrop/js/dragdrop.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/book/tool/print/classes/output/renderer.php
mod/book/view.php
mod/choice/renderer.php
mod/data/field/multimenu/field.class.php
mod/forum/report/summary/classes/event/report_downloaded.php
mod/forum/report/summary/classes/event/report_viewed.php
mod/forum/report/summary/classes/output/filters.php
mod/forum/report/summary/classes/summary_table.php
mod/forum/report/summary/index.php
mod/forum/report/summary/lang/en/forumreport_summary.php
mod/forum/report/summary/renderer.php
mod/forum/report/summary/templates/filters.mustache
mod/forum/report/summary/tests/behat/bulk_message.feature
mod/forum/report/summary/tests/behat/course_summary.feature [new file with mode: 0644]
mod/forum/report/summary/tests/behat/private_replies.feature
mod/forum/report/summary/tests/behat/summary_data_access.feature
mod/forum/report/summary/tests/behat/summary_data_attachments.feature
mod/forum/report/summary/tests/behat/summary_data_post_dates.feature
mod/forum/report/summary/tests/behat/summary_filter_groups.feature
mod/forum/report/summary/tests/behat/summary_filter_no_groups.feature
mod/lesson/renderer.php
mod/wiki/renderer.php
mod/workshop/renderer.php
question/type/ddimageortext/rendererbase.php
question/type/ddmarker/renderer.php
question/type/ddwtos/renderer.php
report/eventlist/classes/renderer.php
report/insights/classes/output/renderer.php
report/security/classes/event/report_viewed.php [new file with mode: 0644]
report/security/index.php
report/security/lang/en/report_security.php
report/security/locallib.php [deleted file]
report/security/settings.php
report/security/version.php
rss/renderer.php
tag/classes/renderer.php
user/renderer.php
version.php

index c111d11..a13efd1 100644 (file)
@@ -62,7 +62,7 @@ lib/amd/src/popper.js
 lib/geopattern-php/
 lib/php-jwt/
 lib/babel-polyfill/
-lib/mdn-polyfills/
+lib/polyfills/
 lib/emoji-data/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
index a828212..ac09a33 100644 (file)
@@ -63,7 +63,7 @@ lib/amd/src/popper.js
 lib/geopattern-php/
 lib/php-jwt/
 lib/babel-polyfill/
-lib/mdn-polyfills/
+lib/polyfills/
 lib/emoji-data/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
index 0ea5193..2451390 100644 (file)
@@ -100,10 +100,11 @@ if (($hassiteconfig || has_any_capability(array(
             new lang_string('allowexternalbackpack', 'badges'),
             new lang_string('allowexternalbackpack_desc', 'badges'), 1));
 
+    $bp = $DB->get_record('badge_external_backpack', ['backpackweburl' => BADGRIO_BACKPACKWEBURL]);
     $backpacksettings->add(new admin_setting_configselect('badges_site_backpack',
             new lang_string('sitebackpack', 'badges'),
             new lang_string('sitebackpack_help', 'badges'),
-            1, $choices));
+            $bp->id, $choices));
 
     $warning = badges_verify_site_backpack();
     if (!empty($warning)) {
index 0fb5206..796f323 100644 (file)
@@ -62,6 +62,8 @@ if (null !== $logid) {
 $renderer = $PAGE->get_renderer('tool_task');
 
 echo $OUTPUT->header();
+
+// Output the search form.
 echo $OUTPUT->render_from_template('core_admin/tasklogs', (object) [
     'action' => $pageurl->out(),
     'filter' => $filter,
@@ -84,6 +86,7 @@ echo $OUTPUT->render_from_template('core_admin/tasklogs', (object) [
     ],
 ]);
 
+// Output any matching logs.
 $table = new \core_admin\task_log_table($filter, $result);
 $table->baseurl = $pageurl;
 $table->out(100, false);
index c3180a2..bb180b6 100644 (file)
@@ -17,7 +17,7 @@
 {{!
     @template core_admin/tasklogs
 
-    Task Logs template.
+    This is the template for the search form which appears above the task logs report.
 }}
 <form class="form-inline" method="GET" action="{{{action}}}">
     <label class="sr-only" for="tasklog-filter">{{#str}}filter{{/str}}</label>
index edd165d..0855ed1 100644 (file)
@@ -27,8 +27,7 @@ namespace tool_analytics\output;
 defined('MOODLE_INTERNAL') || die();
 
 use plugin_renderer_base;
-use templatable;
-use renderable;
+
 
 /**
  * Renderer class.
@@ -74,14 +73,12 @@ class renderer extends plugin_renderer_base {
      * @return string HTML
      */
     public function render_evaluate_results($results, $logs = array()) {
-        global $OUTPUT;
-
         $output = '';
 
         foreach ($results as $timesplittingid => $result) {
 
             if (!CLI_SCRIPT) {
-                $output .= $OUTPUT->box_start('generalbox mb-3');
+                $output .= $this->output->box_start('generalbox mb-3');
             }
 
             // Check that the array key is a string, not all results depend on time splitting methods (e.g. general errors).
@@ -90,47 +87,48 @@ class renderer extends plugin_renderer_base {
                 $langstrdata = (object)array('name' => $timesplitting->get_name(), 'id' => $timesplittingid);
 
                 if (CLI_SCRIPT) {
-                    $output .= $OUTPUT->heading(get_string('scheduledanalysisresultscli', 'tool_analytics', $langstrdata), 3);
+                    $output .= $this->output->heading(get_string('scheduledanalysisresultscli', 'tool_analytics', $langstrdata), 3);
                 } else {
-                    $output .= $OUTPUT->heading(get_string('scheduledanalysisresults', 'tool_analytics', $langstrdata), 3);
+                    $output .= $this->output->heading(get_string('scheduledanalysisresults', 'tool_analytics', $langstrdata), 3);
                 }
             }
 
             if ($result->status == 0) {
-                $output .= $OUTPUT->notification(get_string('goodmodel', 'tool_analytics'),
+                $output .= $this->output->notification(get_string('goodmodel', 'tool_analytics'),
                     \core\output\notification::NOTIFY_SUCCESS);
             } else if ($result->status === \core_analytics\model::NO_DATASET) {
-                $output .= $OUTPUT->notification(get_string('nodatatoevaluate', 'tool_analytics'),
+                $output .= $this->output->notification(get_string('nodatatoevaluate', 'tool_analytics'),
                     \core\output\notification::NOTIFY_WARNING);
             }
 
             if (isset($result->score)) {
                 // Score.
-                $output .= $OUTPUT->heading(get_string('accuracy', 'tool_analytics') . ': ' .
+                $output .= $this->output->heading(get_string('accuracy', 'tool_analytics') . ': ' .
                     round(floatval($result->score), 4) * 100  . '%', 4);
             }
 
             if (!empty($result->info)) {
                 foreach ($result->info as $message) {
-                    $output .= $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING);
+                    $output .= $this->output->notification($message, \core\output\notification::NOTIFY_WARNING);
                 }
             }
 
             if (!CLI_SCRIPT) {
-                $output .= $OUTPUT->box_end();
+                $output .= $this->output->box_end();
             }
         }
 
         // Info logged during evaluation.
         if (!empty($logs) && debugging()) {
-            $output .= $OUTPUT->heading(get_string('extrainfo', 'tool_analytics'), 3);
+            $output .= $this->output->heading(get_string('extrainfo', 'tool_analytics'), 3);
             foreach ($logs as $log) {
-                $output .= $OUTPUT->notification($log, \core\output\notification::NOTIFY_WARNING);
+                $output .= $this->output->notification($log, \core\output\notification::NOTIFY_WARNING);
             }
         }
 
         if (!CLI_SCRIPT) {
-            $output .= $OUTPUT->single_button(new \moodle_url('/admin/tool/analytics/index.php'), get_string('continue'), 'get');
+            $output .= $this->output->single_button(new \moodle_url('/admin/tool/analytics/index.php'),
+                    get_string('continue'), 'get');
         }
 
         return $output;
@@ -147,62 +145,68 @@ class renderer extends plugin_renderer_base {
      * @return string HTML
      */
     public function render_get_predictions_results($trainresults = false, $trainlogs = array(), $predictresults = false, $predictlogs = array()) {
-        global $OUTPUT;
-
         $output = '';
 
         if ($trainresults || (!empty($trainlogs) && debugging())) {
-            $output .= $OUTPUT->heading(get_string('trainingresults', 'tool_analytics'), 3);
+            $output .= $this->output->heading(get_string('trainingresults', 'tool_analytics'), 3);
         }
 
         if ($trainresults) {
             if ($trainresults->status == 0) {
-                $output .= $OUTPUT->notification(get_string('trainingprocessfinished', 'tool_analytics'),
+                $output .= $this->output->notification(
+                        get_string('trainingprocessfinished', 'tool_analytics'),
                     \core\output\notification::NOTIFY_SUCCESS);
             } else if ($trainresults->status === \core_analytics\model::NO_DATASET ||
                     $trainresults->status === \core_analytics\model::NOT_ENOUGH_DATA) {
-                $output .= $OUTPUT->notification(get_string('nodatatotrain', 'tool_analytics'),
+                $output .= $this->output->notification(
+                        get_string('nodatatotrain', 'tool_analytics'),
                     \core\output\notification::NOTIFY_WARNING);
             } else {
-                $output .= $OUTPUT->notification(get_string('generalerror', 'tool_analytics', $trainresults->status),
+                $output .= $this->output->notification(
+                        get_string('generalerror', 'tool_analytics', $trainresults->status),
                     \core\output\notification::NOTIFY_ERROR);
             }
         }
 
         if (!empty($trainlogs) && debugging()) {
-            $output .= $OUTPUT->heading(get_string('extrainfo', 'tool_analytics'), 4);
+            $output .= $this->output->heading(get_string('extrainfo', 'tool_analytics'), 4);
             foreach ($trainlogs as $log) {
-                $output .= $OUTPUT->notification($log, \core\output\notification::NOTIFY_WARNING);
+                $output .= $this->output->notification($log, \core\output\notification::NOTIFY_WARNING);
             }
         }
 
         if ($predictresults || (!empty($predictlogs) && debugging())) {
-            $output .= $OUTPUT->heading(get_string('predictionresults', 'tool_analytics'), 3, 'main mt-3');
+            $output .= $this->output->heading(
+                    get_string('predictionresults', 'tool_analytics'), 3, 'main mt-3');
         }
 
         if ($predictresults) {
             if ($predictresults->status == 0) {
-                $output .= $OUTPUT->notification(get_string('predictionprocessfinished', 'tool_analytics'),
+                $output .= $this->output->notification(
+                        get_string('predictionprocessfinished', 'tool_analytics'),
                     \core\output\notification::NOTIFY_SUCCESS);
             } else if ($predictresults->status === \core_analytics\model::NO_DATASET ||
                     $predictresults->status === \core_analytics\model::NOT_ENOUGH_DATA) {
-                $output .= $OUTPUT->notification(get_string('nodatatopredict', 'tool_analytics'),
+                $output .= $this->output->notification(
+                        get_string('nodatatopredict', 'tool_analytics'),
                     \core\output\notification::NOTIFY_WARNING);
             } else {
-                $output .= $OUTPUT->notification(get_string('generalerror', 'tool_analytics', $predictresults->status),
+                $output .= $this->output->notification(
+                        get_string('generalerror', 'tool_analytics', $predictresults->status),
                     \core\output\notification::NOTIFY_ERROR);
             }
         }
 
         if (!empty($predictlogs) && debugging()) {
-            $output .= $OUTPUT->heading(get_string('extrainfo', 'tool_analytics'), 4);
+            $output .= $this->output->heading(get_string('extrainfo', 'tool_analytics'), 4);
             foreach ($predictlogs as $log) {
-                $output .= $OUTPUT->notification($log, \core\output\notification::NOTIFY_WARNING);
+                $output .= $this->output->notification($log, \core\output\notification::NOTIFY_WARNING);
             }
         }
 
         if (!CLI_SCRIPT) {
-            $output .= $OUTPUT->single_button(new \moodle_url('/admin/tool/analytics/index.php'), get_string('continue'), 'get');
+            $output .= $this->output->single_button(new \moodle_url('/admin/tool/analytics/index.php'),
+                    get_string('continue'), 'get');
         }
 
         return $output;
@@ -236,17 +240,18 @@ class renderer extends plugin_renderer_base {
      * @return string HTML
      */
     public function render_analytics_disabled() {
-        global $OUTPUT, $PAGE, $FULLME;
+        global $FULLME;
 
-        $PAGE->set_url($FULLME);
-        $PAGE->set_title(get_string('pluginname', 'tool_analytics'));
-        $PAGE->set_heading(get_string('pluginname', 'tool_analytics'));
+        $this->page->set_url($FULLME);
+        $this->page->set_title(get_string('pluginname', 'tool_analytics'));
+        $this->page->set_heading(get_string('pluginname', 'tool_analytics'));
 
-        $output = $OUTPUT->header();
-        $output .= $OUTPUT->notification(get_string('analyticsdisabled', 'analytics'), \core\output\notification::NOTIFY_INFO);
+        $output = $this->output->header();
+        $output .= $this->output->notification(get_string('analyticsdisabled', 'analytics'),
+                \core\output\notification::NOTIFY_INFO);
         $output .= \html_writer::tag('a', get_string('continue'), ['class' => 'btn btn-primary',
             'href' => (new \moodle_url('/'))->out()]);
-        $output .= $OUTPUT->footer();
+        $output .= $this->output->footer();
 
         return $output;
     }
index 145b59d..1a76a3b 100644 (file)
@@ -34,46 +34,59 @@ require_once($CFG->libdir.'/formslib.php');
  */
 class tool_task_edit_scheduled_task_form extends moodleform {
     public function definition() {
+        global $PAGE;
+
         $mform = $this->_form;
         /** @var \core\task\scheduled_task $task */
         $task = $this->_customdata;
+        $defaulttask = \core\task\manager::get_default_scheduled_task(get_class($task), false);
+        $renderer = $PAGE->get_renderer('tool_task');
 
-        $plugininfo = core_plugin_manager::instance()->get_plugin_info($task->get_component());
-        $plugindisabled = $plugininfo && $plugininfo->is_enabled() === false && !$task->get_run_if_component_disabled();
-
-        $lastrun = $task->get_last_run_time() ? userdate($task->get_last_run_time()) : get_string('never');
-        $nextrun = $task->get_next_run_time();
-        if ($plugindisabled) {
-            $nextrun = get_string('plugindisabled', 'tool_task');
-        } else if ($task->get_disabled()) {
-            $nextrun = get_string('taskdisabled', 'tool_task');
-        } else if ($nextrun > time()) {
-            $nextrun = userdate($nextrun);
-        } else {
-            $nextrun = get_string('asap', 'tool_task');
-        }
-        $mform->addElement('static', 'lastrun', get_string('lastruntime', 'tool_task'), $lastrun);
-        $mform->addElement('static', 'nextrun', get_string('nextruntime', 'tool_task'), $nextrun);
+        $mform->addElement('static', 'lastrun', get_string('lastruntime', 'tool_task'),
+                $renderer->last_run_time($task));
+
+        $mform->addElement('static', 'nextrun', get_string('nextruntime', 'tool_task'),
+                $renderer->next_run_time($task));
 
-        $mform->addElement('text', 'minute', get_string('taskscheduleminute', 'tool_task'));
+        $mform->addGroup([
+                $mform->createElement('text', 'minute'),
+                $mform->createElement('static', 'minutedefault', '',
+                        get_string('defaultx', 'tool_task', $defaulttask->get_minute())),
+            ], 'minutegroup', get_string('taskscheduleminute', 'tool_task'), null, false);
         $mform->setType('minute', PARAM_RAW);
-        $mform->addHelpButton('minute', 'taskscheduleminute', 'tool_task');
+        $mform->addHelpButton('minutegroup', 'taskscheduleminute', 'tool_task');
 
-        $mform->addElement('text', 'hour', get_string('taskschedulehour', 'tool_task'));
+        $mform->addGroup([
+                $mform->createElement('text', 'hour'),
+                $mform->createElement('static', 'hourdefault', '',
+                        get_string('defaultx', 'tool_task', $defaulttask->get_hour())),
+        ], 'hourgroup', get_string('taskschedulehour', 'tool_task'), null, false);
         $mform->setType('hour', PARAM_RAW);
-        $mform->addHelpButton('hour', 'taskschedulehour', 'tool_task');
+        $mform->addHelpButton('hourgroup', 'taskschedulehour', 'tool_task');
 
-        $mform->addElement('text', 'day', get_string('taskscheduleday', 'tool_task'));
+        $mform->addGroup([
+                $mform->createElement('text', 'day'),
+                $mform->createElement('static', 'daydefault', '',
+                        get_string('defaultx', 'tool_task', $defaulttask->get_day())),
+        ], 'daygroup', get_string('taskscheduleday', 'tool_task'), null, false);
         $mform->setType('day', PARAM_RAW);
-        $mform->addHelpButton('day', 'taskscheduleday', 'tool_task');
+        $mform->addHelpButton('daygroup', 'taskscheduleday', 'tool_task');
 
-        $mform->addElement('text', 'month', get_string('taskschedulemonth', 'tool_task'));
+        $mform->addGroup([
+                $mform->createElement('text', 'month'),
+                $mform->createElement('static', 'monthdefault', '',
+                        get_string('defaultx', 'tool_task', $defaulttask->get_month())),
+        ], 'monthgroup', get_string('taskschedulemonth', 'tool_task'), null, false);
         $mform->setType('month', PARAM_RAW);
-        $mform->addHelpButton('month', 'taskschedulemonth', 'tool_task');
+        $mform->addHelpButton('monthgroup', 'taskschedulemonth', 'tool_task');
 
-        $mform->addElement('text', 'dayofweek', get_string('taskscheduledayofweek', 'tool_task'));
+        $mform->addGroup([
+                $mform->createElement('text', 'dayofweek'),
+                $mform->createElement('static', 'dayofweekdefault', '',
+                        get_string('defaultx', 'tool_task', $defaulttask->get_day_of_week())),
+        ], 'dayofweekgroup', get_string('taskscheduledayofweek', 'tool_task'), null, false);
         $mform->setType('dayofweek', PARAM_RAW);
-        $mform->addHelpButton('dayofweek', 'taskscheduledayofweek', 'tool_task');
+        $mform->addHelpButton('dayofweekgroup', 'taskscheduledayofweek', 'tool_task');
 
         $mform->addElement('advcheckbox', 'disabled', get_string('disabled', 'tool_task'));
         $mform->addHelpButton('disabled', 'disabled', 'tool_task');
@@ -111,7 +124,7 @@ class tool_task_edit_scheduled_task_form extends moodleform {
         $fields = array('minute', 'hour', 'day', 'month', 'dayofweek');
         foreach ($fields as $field) {
             if (!self::validate_fields($field, $data[$field])) {
-                $error[$field] = get_string('invaliddata', 'core_error');
+                $error[$field . 'group'] = get_string('invaliddata', 'core_error');
             }
         }
         return $error;
index 8f41b45..8f357b8 100644 (file)
@@ -39,13 +39,16 @@ if (!$task) {
     print_error('cannotfindinfo', 'error', $taskname);
 }
 
+$returnurl = new moodle_url('/admin/tool/task/scheduledtasks.php',
+        ['lastchanged' => get_class($task)]);
+
 // If actually doing the clear, then carry out the task and redirect to the scheduled task page.
 if (optional_param('confirm', 0, PARAM_INT)) {
     require_sesskey();
 
     \core\task\manager::clear_fail_delay($task);
 
-    redirect(new moodle_url('/admin/tool/task/scheduledtasks.php'));
+    redirect($returnurl);
 }
 
 // Start output.
@@ -60,9 +63,8 @@ echo $OUTPUT->header();
 // they confirm.
 echo $OUTPUT->confirm(get_string('clearfaildelay_confirm', 'tool_task', $task->get_name()),
         new single_button(new moodle_url('/admin/tool/task/clear_fail_delay.php',
-                array('task' => $taskname, 'confirm' => 1, 'sesskey' => sesskey())),
+                ['task' => $taskname, 'confirm' => 1, 'sesskey' => sesskey()]),
                 get_string('clear')),
-        new single_button(new moodle_url('/admin/tool/task/scheduledtasks.php'),
-                get_string('cancel'), false));
+        new single_button($returnurl, get_string('cancel'), false));
 
 echo $OUTPUT->footer();
index 76ab5da..b0a5535 100644 (file)
@@ -30,12 +30,14 @@ $string['clearfaildelay_confirm'] = 'Are you sure you want to clear the fail del
 $string['component'] = 'Component';
 $string['corecomponent'] = 'Core';
 $string['default'] = 'Default';
+$string['defaultx'] = 'Default: {$a}';
 $string['disabled'] = 'Disabled';
 $string['disabled_help'] = 'Disabled scheduled tasks are not executed from cron, however they can still be executed manually via the CLI tool.';
 $string['edittaskschedule'] = 'Edit task schedule: {$a}';
 $string['enablerunnow'] = 'Allow \'Run now\' for scheduled tasks';
 $string['enablerunnow_desc'] = 'Allows administrators to run a single scheduled task immediately, rather than waiting for it to run as scheduled. The feature requires \'Path to PHP CLI\' (pathtophp) to be set in System paths. The task runs on the web server, so you may wish to disable this feature to avoid potential performance issues.';
 $string['faildelay'] = 'Fail delay';
+$string['fromcomponent'] = 'From component: {$a}';
 $string['lastruntime'] = 'Last run';
 $string['nextruntime'] = 'Next run';
 $string['plugindisabled'] = 'Plugin disabled';
index 3afd20c..0a01d0a 100644 (file)
@@ -25,6 +25,9 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+use core\task\scheduled_task;
+
+
 /**
  * Implements the plugin renderer
  *
@@ -36,9 +39,10 @@ class tool_task_renderer extends plugin_renderer_base {
      * This function will render one beautiful table with all the scheduled tasks.
      *
      * @param \core\task\scheduled_task[] $tasks - list of all scheduled tasks.
+     * @param string $lastchanged (optional) the last task edited. Gets highlighted in teh table.
      * @return string HTML to output.
      */
-    public function scheduled_tasks_table($tasks) {
+    public function scheduled_tasks_table($tasks, $lastchanged = '') {
         global $CFG;
 
         $showloglink = \core\task\logmanager::has_log_report();
@@ -68,118 +72,205 @@ class tool_task_renderer extends plugin_renderer_base {
             $table->colclasses['3'] = 'hidden';
         }
 
-        $data = array();
+        $data = [];
         $yes = get_string('yes');
         $no = get_string('no');
-        $never = get_string('never');
-        $asap = get_string('asap', 'tool_task');
-        $disabledstr = get_string('taskdisabled', 'tool_task');
-        $plugindisabledstr = get_string('plugindisabled', 'tool_task');
-        $runnabletasks = tool_task\run_from_cli::is_runnable();
+        $canruntasks = tool_task\run_from_cli::is_runnable();
         foreach ($tasks as $task) {
+            $classname = get_class($task);
+            $defaulttask = \core\task\manager::get_default_scheduled_task($classname, false);
+
             $customised = $task->is_customised() ? $no : $yes;
             if (empty($CFG->preventscheduledtaskchanges)) {
-                $configureurl = new moodle_url('/admin/tool/task/scheduledtasks.php', array('action'=>'edit', 'task' => get_class($task)));
-                $editlink = $this->action_icon($configureurl, new pix_icon('t/edit', get_string('edittaskschedule', 'tool_task', $task->get_name())));
+                $configureurl = new moodle_url('/admin/tool/task/scheduledtasks.php',
+                        ['action' => 'edit', 'task' => $classname]);
+                $editlink = $this->output->action_icon($configureurl, new pix_icon('t/edit',
+                        get_string('edittaskschedule', 'tool_task', $task->get_name())));
             } else {
-                $editlink = $this->render(new pix_icon('t/locked', get_string('scheduledtaskchangesdisabled', 'tool_task')));
+                $editlink = $this->render(new pix_icon('t/locked',
+                        get_string('scheduledtaskchangesdisabled', 'tool_task')));
             }
 
             $loglink = '';
             if ($showloglink) {
-                $loglink = $this->action_icon(
-                    \core\task\logmanager::get_url_for_task_class(get_class($task)),
+                $loglink = $this->output->action_icon(
+                    \core\task\logmanager::get_url_for_task_class($classname),
                     new pix_icon('e/file-text', get_string('viewlogs', 'tool_task', $task->get_name())
                 ));
             }
 
-            $namecell = new html_table_cell($task->get_name() . "\n" . html_writer::tag('span', '\\'.get_class($task),
-                array('class' => 'task-class text-ltr')));
+            $namecell = new html_table_cell($task->get_name() . "\n" .
+                    html_writer::span('\\' . $classname, 'task-class text-ltr'));
             $namecell->header = true;
 
-            $component = $task->get_component();
-            $plugininfo = null;
-            list($type, $plugin) = core_component::normalize_component($component);
-            if ($type === 'core') {
-                $componentcell = new html_table_cell(get_string('corecomponent', 'tool_task'));
-            } else {
-                if ($plugininfo = core_plugin_manager::instance()->get_plugin_info($component)) {
-                    $plugininfo->init_display_name();
-                    $componentcell = new html_table_cell($plugininfo->displayname);
-                } else {
-                    $componentcell = new html_table_cell($component);
-                }
-            }
-
-            $lastrun = $task->get_last_run_time() ? userdate($task->get_last_run_time()) : $never;
-            $nextrun = $task->get_next_run_time();
-            $disabled = false;
-            if ($plugininfo && $plugininfo->is_enabled() === false && !$task->get_run_if_component_disabled()) {
-                $disabled = true;
-                $nextrun = $plugindisabledstr;
-            } else if ($task->get_disabled()) {
-                $disabled = true;
-                $nextrun = $disabledstr;
-            } else if ($nextrun > time()) {
-                $nextrun = userdate($nextrun);
-            } else {
-                $nextrun = $asap;
-            }
+            $plugininfo = core_plugin_manager::instance()->get_plugin_info($task->get_component());
+            $plugindisabled = $plugininfo && $plugininfo->is_enabled() === false &&
+                    !$task->get_run_if_component_disabled();
+            $disabled = $plugindisabled || $task->get_disabled();
 
             $runnow = '';
-            if ( ! $disabled && get_config('tool_task', 'enablerunnow') && $runnabletasks ) {
+            if (!$disabled && get_config('tool_task', 'enablerunnow') && $canruntasks ) {
                 $runnow = html_writer::div(html_writer::link(
                         new moodle_url('/admin/tool/task/schedule_task.php',
-                            array('task' => get_class($task))),
+                            ['task' => $classname]),
                         get_string('runnow', 'tool_task')), 'task-runnow');
             }
 
-            $clearfail = '';
+            $faildelaycell = new html_table_cell($task->get_fail_delay());
             if ($task->get_fail_delay()) {
-                $clearfail = html_writer::div(html_writer::link(
+                $faildelaycell->text .= html_writer::div(html_writer::link(
                         new moodle_url('/admin/tool/task/clear_fail_delay.php',
-                                array('task' => get_class($task), 'sesskey' => sesskey())),
+                                ['task' => $classname, 'sesskey' => sesskey()]),
                         get_string('clear')), 'task-clearfaildelay');
+                $faildelaycell->attributes['class'] = 'table-danger';
             }
 
-            $row = new html_table_row(array(
+            $row = new html_table_row([
                         $namecell,
-                        $componentcell,
+                        new html_table_cell($this->component_name($task->get_component())),
                         new html_table_cell($editlink),
                         new html_table_cell($loglink),
-                        new html_table_cell($lastrun . $runnow),
-                        new html_table_cell($nextrun),
-                        new html_table_cell($task->get_minute()),
-                        new html_table_cell($task->get_hour()),
-                        new html_table_cell($task->get_day()),
-                        new html_table_cell($task->get_day_of_week()),
-                        new html_table_cell($task->get_month()),
-                        new html_table_cell($task->get_fail_delay() . $clearfail),
-                        new html_table_cell($customised)));
-
-            // Cron-style values must always be LTR.
-            $row->cells[6]->attributes['class'] = 'text-ltr';
-            $row->cells[7]->attributes['class'] = 'text-ltr';
-            $row->cells[8]->attributes['class'] = 'text-ltr';
-            $row->cells[9]->attributes['class'] = 'text-ltr';
-            $row->cells[10]->attributes['class'] = 'text-ltr';
+                        new html_table_cell($this->last_run_time($task) . $runnow),
+                        new html_table_cell($this->next_run_time($task)),
+                        $this->time_cell($task->get_minute(), $defaulttask->get_minute()),
+                        $this->time_cell($task->get_hour(), $defaulttask->get_hour()),
+                        $this->time_cell($task->get_day(), $defaulttask->get_day()),
+                        $this->time_cell($task->get_day_of_week(), $defaulttask->get_day_of_week()),
+                        $this->time_cell($task->get_month(), $defaulttask->get_month()),
+                        $faildelaycell,
+                        new html_table_cell($customised)]);
 
+            $classes = [];
             if ($disabled) {
-                $row->attributes['class'] = 'disabled';
+                $classes[] = 'disabled';
+            }
+            if (get_class($task) == $lastchanged) {
+                $classes[] = 'table-primary';
             }
+            $row->attributes['class'] = implode(' ', $classes);
             $data[] = $row;
         }
         $table->data = $data;
+        if ($lastchanged) {
+            // IE does not support this, and the ancient version of Firefox we use for Behat
+            // has the method, but then errors on 'centre'. So, just try to scroll, and if it fails, don't care.
+            $this->page->requires->js_init_code(
+                    'try{document.querySelector("tr.table-primary").scrollIntoView({block: "center"});}catch(e){}');
+        }
         return html_writer::table($table);
     }
 
+    /**
+     * Nicely display the name of a component, with its disabled status and internal name.
+     *
+     * @param string $component component name, e.g. 'core' or 'mod_forum'.
+     * @return string HTML.
+     */
+    public function component_name(string $component): string {
+        list($type) = core_component::normalize_component($component);
+        if ($type === 'core') {
+            return get_string('corecomponent', 'tool_task');
+        }
+
+        $plugininfo = core_plugin_manager::instance()->get_plugin_info($component);
+        if (!$plugininfo) {
+            return $component;
+        }
+
+        $plugininfo->init_display_name();
+
+        $componentname = $plugininfo->displayname;
+        if (!$plugininfo->is_enabled()) {
+            $componentname .= ' ' . html_writer::span(
+                            get_string('disabled', 'tool_task'), 'badge badge-secondary');
+        }
+        $componentname .= "\n" . html_writer::span($plugininfo->component, 'task-class text-ltr');
+
+        return $componentname;
+    }
+
+    /**
+     * Standard display of a tasks last run time.
+     *
+     * @param scheduled_task $task
+     * @return string HTML.
+     */
+    public function last_run_time(scheduled_task $task): string {
+        if ($task->get_last_run_time()) {
+            return userdate($task->get_last_run_time());
+        } else {
+            return get_string('never');
+        }
+    }
+
+    /**
+     * Standard display of a tasks next run time.
+     *
+     * @param scheduled_task $task
+     * @return string HTML.
+     */
+    public function next_run_time(scheduled_task $task): string {
+        $plugininfo = core_plugin_manager::instance()->get_plugin_info($task->get_component());
+
+        $nextrun = $task->get_next_run_time();
+        if ($plugininfo && $plugininfo->is_enabled() === false && !$task->get_run_if_component_disabled()) {
+            $nextrun = get_string('plugindisabled', 'tool_task');
+        } else if ($task->get_disabled()) {
+            $nextrun = get_string('taskdisabled', 'tool_task');
+        } else if ($nextrun > time()) {
+            $nextrun = userdate($nextrun);
+        } else {
+            $nextrun = get_string('asap', 'tool_task');
+        }
+
+        return $nextrun;
+    }
+
+    /**
+     * Get a table cell to show one time, comparing it to the default.
+     *
+     * @param string $current the current setting.
+     * @param string $default the default setting from the db/tasks.php file.
+     * @return html_table_cell for use in the table.
+     */
+    protected function time_cell(string $current, string $default): html_table_cell {
+        $cell = new html_table_cell($current);
+        // Cron-style values must always be LTR.
+        $cell->attributes['class'] = 'text-ltr';
+
+        // If the current value is default, that is all we want to do.
+        if ($default === '*') {
+            if ($current === '*') {
+                return $cell;
+            }
+        } else if ($default === 'R' ) {
+            if (is_numeric($current)) {
+                return $cell;
+            }
+        } else {
+            if ($default === $current) {
+                return $cell;
+            }
+        }
+
+        // Otherwise, highlight and show the default.
+        $cell->attributes['class'] .= ' table-warning';
+        $cell->text .= ' ' . html_writer::span(
+                get_string('defaultx', 'tool_task', $default), 'task-class');
+        return $cell;
+    }
+
     /**
      * Renders a link back to the scheduled tasks page (used from the 'run now' screen).
      *
+     * @param string $taskclassname if specified, the list of tasks will scroll to show this task.
      * @return string HTML code
      */
-    public function link_back() {
-        return $this->render_from_template('tool_task/link_back',
-                array('url' => new moodle_url('/admin/tool/task/scheduledtasks.php')));
+    public function link_back($taskclassname = '') {
+        $url = new moodle_url('/admin/tool/task/scheduledtasks.php');
+        if ($taskclassname) {
+            $url->param('lastchanged', $taskclassname);
+        }
+        return $this->render_from_template('tool_task/link_back', ['url' => $url]);
     }
 }
index fd7bc30..b404a82 100644 (file)
@@ -71,9 +71,10 @@ echo $OUTPUT->heading($task->get_name());
 if (!optional_param('confirm', 0, PARAM_INT)) {
     echo $OUTPUT->confirm(get_string('runnow_confirm', 'tool_task', $task->get_name()),
             new single_button(new moodle_url('/admin/tool/task/schedule_task.php',
-            array('task' => $taskname, 'confirm' => 1, 'sesskey' => sesskey())),
+                    ['task' => $taskname, 'confirm' => 1, 'sesskey' => sesskey()]),
             get_string('runnow', 'tool_task')),
-            new single_button(new moodle_url('/admin/tool/task/scheduledtasks.php'),
+            new single_button(new moodle_url('/admin/tool/task/scheduledtasks.php',
+                    ['lastchanged' => get_class($task)]),
             get_string('cancel'), false));
     echo $OUTPUT->footer();
     exit;
@@ -97,6 +98,6 @@ $output = $PAGE->get_renderer('tool_task');
 echo $OUTPUT->single_button(new moodle_url('/admin/tool/task/schedule_task.php',
         array('task' => $taskname, 'confirm' => 1, 'sesskey' => sesskey())),
         get_string('runagain', 'tool_task'));
-echo $output->link_back();
+echo $output->link_back(get_class($task));
 
 echo $OUTPUT->footer();
index 90d8b8d..d256f3d 100644 (file)
@@ -26,19 +26,12 @@ require_once(__DIR__ . '/../../../config.php');
 require_once($CFG->libdir.'/adminlib.php');
 require_once($CFG->libdir.'/tablelib.php');
 
-$PAGE->set_url('/admin/tool/task/scheduledtasks.php');
-$PAGE->set_context(context_system::instance());
-$PAGE->set_pagelayout('admin');
-$strheading = get_string('scheduledtasks', 'tool_task');
-$PAGE->set_title($strheading);
-$PAGE->set_heading($strheading);
-
-require_admin();
-
-$renderer = $PAGE->get_renderer('tool_task');
+admin_externalpage_setup('scheduledtasks');
 
 $action = optional_param('action', '', PARAM_ALPHAEXT);
 $taskname = optional_param('task', '', PARAM_RAW);
+$lastchanged = optional_param('lastchanged', '', PARAM_RAW);
+
 $task = null;
 $mform = null;
 
@@ -55,15 +48,16 @@ if ($action == 'edit') {
 
 if ($task) {
     $mform = new tool_task_edit_scheduled_task_form(null, $task);
+    $nexturl = new moodle_url($PAGE->url, ['lastchanged' => $taskname]);
 }
 
+$renderer = $PAGE->get_renderer('tool_task');
+
 if ($mform && ($mform->is_cancelled() || !empty($CFG->preventscheduledtaskchanges))) {
-    redirect(new moodle_url('/admin/tool/task/scheduledtasks.php'));
+    redirect($nexturl);
 } else if ($action == 'edit' && empty($CFG->preventscheduledtaskchanges)) {
 
     if ($data = $mform->get_data()) {
-
-
         if ($data->resettodefaults) {
             $defaulttask = \core\task\manager::get_default_scheduled_task($taskname);
             $task->set_minute($defaulttask->get_minute());
@@ -85,13 +79,16 @@ if ($mform && ($mform->is_cancelled() || !empty($CFG->preventscheduledtaskchange
 
         try {
             \core\task\manager::configure_scheduled_task($task);
-            redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS);
+            redirect($nexturl, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS);
         } catch (Exception $e) {
-            redirect($PAGE->url, $e->getMessage(), null, \core\output\notification::NOTIFY_ERROR);
+            redirect($nexturl, $e->getMessage(), null, \core\output\notification::NOTIFY_ERROR);
         }
     } else {
         echo $OUTPUT->header();
         echo $OUTPUT->heading(get_string('edittaskschedule', 'tool_task', $task->get_name()));
+        echo html_writer::div('\\' . get_class($task), 'task-class text-ltr');
+        echo html_writer::div(get_string('fromcomponent', 'tool_task',
+                $renderer->component_name($task->get_component())));
         $mform->display();
         echo $OUTPUT->footer();
     }
@@ -99,6 +96,6 @@ if ($mform && ($mform->is_cancelled() || !empty($CFG->preventscheduledtaskchange
 } else {
     echo $OUTPUT->header();
     $tasks = core\task\manager::get_all_scheduled_tasks();
-    echo $renderer->scheduled_tasks_table($tasks);
+    echo $renderer->scheduled_tasks_table($tasks, $lastchanged);
     echo $OUTPUT->footer();
 }
index aaa36d9..643750b 100644 (file)
@@ -9,6 +9,11 @@ Feature: Clear scheduled task fail delay
     And I log in as "admin"
     And I navigate to "Server > Tasks > Scheduled tasks" in site administration
 
+  Scenario: Any fail delay is highlighted
+    Then I should see "60" in the "Send new user passwords" "table_row"
+    And I should see "Clear" in the "Send new user passwords" "table_row"
+    And I should see "60" in the "td.table-danger" "css_element"
+
   Scenario: Clear fail delay
     When I click on "Clear" "text" in the "Send new user passwords" "table_row"
     And I should see "Are you sure you want to clear the fail delay"
@@ -16,6 +21,8 @@ Feature: Clear scheduled task fail delay
 
     Then I should not see "60" in the "Send new user passwords" "table_row"
     And I should not see "Clear" in the "Send new user passwords" "table_row"
+    And I should see "Send new user passwords" in the "tr.table-primary" "css_element"
+
 
   Scenario: Cancel clearing the fail delay
     When I click on "Clear" "text" in the "Send new user passwords" "table_row"
@@ -23,3 +30,4 @@ Feature: Clear scheduled task fail delay
 
     Then I should see "60" in the "Send new user passwords" "table_row"
     And I should see "Clear" in the "Send new user passwords" "table_row"
+    And I should see "Send new user passwords" in the "tr.table-primary" "css_element"
index 4da19e5..160451d 100644 (file)
@@ -16,6 +16,7 @@ Feature: Manage scheduled tasks
     And I press "Save changes"
     Then I should see "Changes saved"
     And I should see "Task disabled" in the "Log table cleanup" "table_row"
+    And I should see "Log table cleanup" in the "tr.table-primary" "css_element"
 
   Scenario: Enable scheduled task
     When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row"
@@ -25,10 +26,20 @@ Feature: Manage scheduled tasks
     And I press "Save changes"
     Then I should see "Changes saved"
     And I should not see "Task disabled" in the "Log table cleanup" "table_row"
+    And I should see "Log table cleanup" in the "tr.table-primary" "css_element"
 
   Scenario: Edit scheduled task
     When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row"
     Then I should see "Edit task schedule: Log table cleanup"
+    And I should see "\logstore_standard\task\cleanup_task"
+    And I should see "From component: Standard log"
+    And I should see "logstore_standard"
+    And I should see "Default: R" in the "Minute" "fieldset"
+    And I should see "Default: *" in the "Day" "fieldset"
+    And I set the following fields to these values:
+      | minute               | frog |
+    And I press "Save changes"
+    And I should see "Data submitted is invalid"
     And I set the following fields to these values:
       | minute               | */5 |
       | hour                 | 1   |
@@ -36,10 +47,12 @@ Feature: Manage scheduled tasks
       | month                | 3   |
       | dayofweek            | 4   |
     And I press "Save changes"
-    Then I should see "Changes saved"
+    And I should see "Changes saved"
     And the following should exist in the "admintable" table:
-      | Component    | Minute | Hour | Day | Day of week | Month |
-      | Standard log | */5    | 1    | 2   | 4           | 3     |
+      | Component                      | Minute         | Hour         | Day          | Day of week  | Month        |
+      | Standard log logstore_standard | */5 Default: R | 1 Default: 4 | 2 Default: * | 4 Default: * | 3 Default: * |
+    And I should see "Log table cleanup" in the "tr.table-primary" "css_element"
+    And I should see "*/5 Default: R" in the "td.table-warning" "css_element"
 
   Scenario: Reset scheduled task to default
     When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row"
@@ -50,4 +63,5 @@ Feature: Manage scheduled tasks
     Then I should see "Changes saved"
     And the following should not exist in the "admintable" table:
       | Name               | Component    | Minute | Hour | Day | Day of week | Month |
-      | Log table cleanup  | Standard log | */5    | 1    | 2   | 4           | 3     |
\ No newline at end of file
+      | Log table cleanup  | Standard log | */5    | 1    | 2   | 4           | 3     |
+    And I should see "Log table cleanup" in the "tr.table-primary" "css_element"
diff --git a/auth/none/classes/check/noauth.php b/auth/none/classes/check/noauth.php
new file mode 100644 (file)
index 0000000..7e57907
--- /dev/null
@@ -0,0 +1,69 @@
+<?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/>.
+
+/**
+ * Verifies unsupported noauth setting
+ *
+ * @package    auth_none
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace auth_none\check;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\result;
+
+/**
+ * Verifies unsupported noauth setting
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class noauth extends \core\check\check {
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/settings.php?section=manageauths'),
+            get_string('authsettings', 'admin'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        if (is_enabled_auth('none')) {
+            $status = result::ERROR;
+            $summary = get_string('checknoautherror', 'auth_none');
+        } else {
+            $status = result::OK;
+            $summary = get_string('checknoauthok', 'auth_none');
+        }
+        $details = get_string('checknoauthdetails', 'auth_none');
+
+        return new result($status, $summary, $details);
+    }
+}
+
index a4ba89d..4ae1975 100644 (file)
@@ -25,3 +25,7 @@
 $string['auth_nonedescription'] = 'Users can sign in and create valid accounts immediately, with no authentication against an external server and no confirmation via email.  Be careful using this option - think of the security and administration problems this could cause.';
 $string['pluginname'] = 'No authentication';
 $string['privacy:metadata'] = 'The No authentication plugin does not store any personal data.';
+$string['checknoauthdetails'] = '<p>The <em>No authentication</em> plugin is not intended for production sites. Please disable it unless this is a development test site.</p>';
+$string['checknoautherror'] = 'The No authentication plugin cannot be used on production sites.';
+$string['checknoauth'] = 'No authentication';
+$string['checknoauthok'] = 'The no authentication plugin is disabled.';
diff --git a/auth/none/lib.php b/auth/none/lib.php
new file mode 100644 (file)
index 0000000..7c6b551
--- /dev/null
@@ -0,0 +1,36 @@
+<?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/>.
+
+/**
+ * Anybody can login with any password.
+ *
+ * @package    auth_none
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Add security check to make sure this isn't on in production.
+ *
+ * @return array check
+ */
+function auth_none_security_checks() {
+    return [new auth_none\check\noauth()];
+}
+
index b09772e..4dea016 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2019111800;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2019111801;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2019111200;        // Requires this Moodle version
 $plugin->component = 'auth_none';       // Full name of the plugin (used for diagnostics)
index c531260..d9bdbe1 100644 (file)
@@ -277,7 +277,7 @@ class core_backup_renderer extends plugin_renderer_base {
      */
     public function course_selector(moodle_url $nextstageurl, $wholecourse = true, restore_category_search $categories = null,
                                     restore_course_search $courses = null, $currentcourse = null) {
-        global $CFG, $PAGE;
+        global $CFG;
         require_once($CFG->dirroot.'/course/lib.php');
 
         // These variables are used to check if the form using this function was submitted.
index 8920393..e1c471f 100644 (file)
@@ -49,7 +49,7 @@ class core_badges_assertion {
     private $_url;
 
     /** @var int $obversion to control version JSON-LD. */
-    private $_obversion = OPEN_BADGES_V1;
+    private $_obversion = OPEN_BADGES_V2;
 
     /**
      * Constructs with issued badge unique hash.
@@ -57,7 +57,7 @@ class core_badges_assertion {
      * @param string $hash Badge unique hash from badge_issued table.
      * @param int $obversion to control version JSON-LD.
      */
-    public function __construct($hash, $obversion = OPEN_BADGES_V1) {
+    public function __construct($hash, $obversion = OPEN_BADGES_V2) {
         global $DB;
 
         $this->_data = $DB->get_record_sql('
@@ -198,11 +198,8 @@ class core_badges_assertion {
             $class['image'] = 'data:image/png;base64,' . $imagedata;
             $class['criteria'] = $this->_url->out(false); // Currently issued badge URL.
             if ($issued) {
-                if ($this->_obversion == OPEN_BADGES_V2) {
-                    $issuerurl = new moodle_url('/badges/issuer_json.php', array('id' => $this->get_badge_id()));
-                } else {
-                    $issuerurl = new moodle_url('/badges/assertion.php', array('b' => $this->_data->uniquehash, 'action' => 0));
-                }
+                $params = ['id' => $this->get_badge_id(), 'obversion' => $this->_obversion];
+                $issuerurl = new moodle_url('/badges/issuer_json.php', $params);
                 $class['issuer'] = $issuerurl->out(false);
             }
             $this->embed_data_badge_version2($class, OPEN_BADGES_V2_TYPE_BADGE);
@@ -223,7 +220,7 @@ class core_badges_assertion {
         $issuer = array();
         if ($this->_data) {
             // Required.
-            if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) {
+            if ($this->_obversion == OPEN_BADGES_V1) {
                 $issuer['name'] = $this->_data->issuername;
                 $issuer['url'] = $this->_data->issuerurl;
                 // Optional.
index a3dfdba..9bd9d19 100644 (file)
@@ -925,17 +925,28 @@ class badge {
     /**
      * Define issuer information by format Open Badges specification version 2.
      *
+     * @param int $obversion OB version to use.
      * @return array Issuer informations of the badge.
      */
-    public function get_badge_issuer() {
-        $issuer = array();
-        $issuer['name'] = $this->issuername;
-        $issuer['url'] = $this->issuerurl;
-        $issuer['email'] = $this->issuercontact;
-        $issuer['@context'] = OPEN_BADGES_V2_CONTEXT;
-        $issueridurl = new moodle_url('/badges/issuer_json.php', array('id' => $this->id));
-        $issuer['id'] = $issueridurl->out(false);
-        $issuer['type'] = OPEN_BADGES_V2_TYPE_ISSUER;
+    public function get_badge_issuer(?int $obversion = null) {
+        global $DB;
+
+        $issuer = [];
+        if ($obversion == OPEN_BADGES_V1) {
+            $data = $DB->get_record('badge', ['id' => $this->id]);
+            $issuer['name'] = $data->issuername;
+            $issuer['url'] = $data->issuerurl;
+            $issuer['email'] = $data->issuercontact;
+        } else {
+            $issuer['name'] = $this->issuername;
+            $issuer['url'] = $this->issuerurl;
+            $issuer['email'] = $this->issuercontact;
+            $issuer['@context'] = OPEN_BADGES_V2_CONTEXT;
+            $issueridurl = new moodle_url('/badges/issuer_json.php', array('id' => $this->id));
+            $issuer['id'] = $issueridurl->out(false);
+            $issuer['type'] = OPEN_BADGES_V2_TYPE_ISSUER;
+        }
+
         return $issuer;
     }
 }
index ebbf74f..87fe6bd 100644 (file)
@@ -30,6 +30,8 @@ require_once($CFG->libdir . '/badgeslib.php');
 
 
 $id = optional_param('id', null, PARAM_INT);
+// OB specification version. If it's not defined, the site will be used as default.
+$obversion = optional_param('obversion', badges_open_badges_backpack_api(), PARAM_INT);
 
 if (empty($id)) {
     // Get the default issuer for this site.
@@ -38,7 +40,7 @@ if (empty($id)) {
     // Get the issuer for this badge.
     $badge = new badge($id);
     if ($badge->status != BADGE_STATUS_INACTIVE) {
-        $json = $badge->get_badge_issuer();
+        $json = $badge->get_badge_issuer($obversion);
     } else {
         // The badge doen't exist or not accessible for the users.
         header("HTTP/1.0 410 Gone");
index a95f9cc..d3c2a6b 100644 (file)
@@ -599,7 +599,7 @@ class core_badges_renderer extends plugin_renderer_base {
      * @return string
      */
     protected function render_badge_user_collection(\core_badges\output\badge_user_collection $badges) {
-        global $CFG, $USER, $SITE, $OUTPUT;
+        global $CFG, $USER, $SITE;
         $backpack = $badges->backpack;
         $mybackpack = new moodle_url('/badges/mybackpack.php');
 
@@ -645,7 +645,7 @@ class core_badges_renderer extends plugin_renderer_base {
             $externalhtml .= $this->output->heading_with_help(get_string('externalbadges', 'badges'), 'externalbadges', 'badges');
             if (!is_null($backpack)) {
                 if ($backpack->backpackid != $CFG->badges_site_backpack) {
-                    $externalhtml .= $OUTPUT->notification(get_string('backpackneedsupdate', 'badges'), 'warning');
+                    $externalhtml .= $this->output->notification(get_string('backpackneedsupdate', 'badges'), 'warning');
 
                 }
                 if ($backpack->totalcollections == 0) {
index 1e379e8..d3d3541 100644 (file)
@@ -75,6 +75,10 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
 
         $this->badgeid = $DB->insert_record('badge', $fordb, true);
 
+        // Set the default Issuer (because OBv2 needs them).
+        set_config('badges_defaultissuername', $fordb->issuername);
+        set_config('badges_defaultissuercontact', $fordb->issuercontact);
+
         // Create a course with activity and auto completion tracking.
         $this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
         $this->user = $this->getDataGenerator()->create_user();
@@ -670,7 +674,7 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
 
         // Get assertion.
         $award = reset($awards);
-        $assertion = new core_badges_assertion($award->uniquehash);
+        $assertion = new core_badges_assertion($award->uniquehash, OPEN_BADGES_V1);
         $testassertion = $this->assertion;
 
         // Make sure JSON strings have the same structure.
index c2a4c00..81ecdb7 100644 (file)
@@ -16,8 +16,9 @@ Feature: Add badges to the system
     And I press "Save changes"
     And I follow "Badges"
     When I follow "Add a new badge"
-    Then the field "issuercontact" matches value "testuser@example.com"
-    And the field "issuername" matches value "Test Badge Site"
+    And I press "Issuer details"
+    Then I should see "testuser@example.com"
+    And I should see "Test Badge Site"
 
   @javascript
   Scenario: Accessing the badges
@@ -38,8 +39,6 @@ Feature: Add badges to the system
       | Description | Test badge description |
       | Image author | http://author.example.com |
       | Image caption | Test caption image |
-      | issuername | Test Badge Site |
-      | issuercontact | testuser@example.com |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     When I press "Create badge"
     Then I should see "Edit details"
@@ -62,8 +61,6 @@ Feature: Add badges to the system
       | Description | Test badge related description |
       | Image author | http://author.example.com |
       | Image caption | Test caption image |
-      | issuername | Test Badge Site |
-      | issuercontact | testuser@example.com |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I wait until the page is ready
@@ -77,8 +74,6 @@ Feature: Add badges to the system
       | Description | Test badge description |
       | Image author | http://author.example.com |
       | Image caption | Test caption image |
-      | issuername | Test Badge Site |
-      | issuercontact | testuser@example.com |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I follow "Related badges (0)"
@@ -101,8 +96,6 @@ Feature: Add badges to the system
       | Description | Test badge description |
       | Image author | http://author.example.com |
       | Image caption | Test caption image |
-      | issuername | Test Badge Site |
-      | issuercontact | testuser@example.com |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     When I press "Create badge"
     Then I should see "Edit details"
@@ -127,8 +120,6 @@ Feature: Add badges to the system
       | Description | Test badge description |
       | Image author | http://author.example.com |
       | Image caption | Test caption image |
-      | issuername | Test Badge Site |
-      | issuercontact | testuser@example.com |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     When I press "Create badge"
     Then I should see "Test Badge"
@@ -161,8 +152,6 @@ Feature: Add badges to the system
       | Description | Test badge description |
       | Image author | http://author.example.com |
       | Image caption | Test caption image |
-      | issuername | Test Badge Site |
-      | issuercontact | testuser@example.com |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     Then I should see "Edit details"
index 079b2c6..313a9a5 100644 (file)
@@ -25,7 +25,6 @@ Feature: Award badges
     And I set the following fields to these values:
       | Name | Course Badge 1 |
       | Description | Course badge 1 description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Manual issue by role"
@@ -43,7 +42,6 @@ Feature: Award badges
     And I set the following fields to these values:
       | Name | Course Badge 2 |
       | Description | Course badge 2 description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     # Set "course badge 1" as criteria
@@ -102,8 +100,6 @@ Feature: Award badges
     And I set the following fields to these values:
       | Name | Profile Badge |
       | Description | Test badge description |
-      | issuername | Test Badge Site |
-      | issuercontact | testuser@example.com |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Profile completion"
@@ -140,7 +136,6 @@ Feature: Award badges
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Manual issue by role"
@@ -183,7 +178,6 @@ Feature: Award badges
     And I set the following fields to these values:
       | Name | Course Badge |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Manual issue by role"
@@ -235,7 +229,6 @@ Feature: Award badges
     And I set the following fields to these values:
       | Name | Course Badge |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Activity completion"
@@ -290,7 +283,6 @@ Feature: Award badges
     And I set the following fields to these values:
       | Name | Course Badge |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Course completion"
@@ -340,7 +332,6 @@ Feature: Award badges
     And I set the following fields to these values:
       | Name | Course Badge 1 |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Manual issue by role"
@@ -366,7 +357,6 @@ Feature: Award badges
     And I set the following fields to these values:
       | Name | Course Badge 2 |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Manual issue by role"
@@ -423,7 +413,6 @@ Feature: Award badges
     And I set the following fields to these values:
       | Name | Course Badge |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Manual issue by role"
index 90683b3..67c5b6e 100644 (file)
@@ -41,7 +41,6 @@ Feature: Award badges with separate groups
     And I set the following fields to these values:
       | Name | Course Badge |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Manual issue by role"
index b71fcea..55f9254 100644 (file)
@@ -40,7 +40,6 @@ Feature: Award badges based on activity completion
     And I set the following fields to these values:
       | Name | Course Badge |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Activity completion"
index e497144..e0578de 100644 (file)
@@ -23,7 +23,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
@@ -59,7 +58,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
@@ -100,7 +98,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
@@ -137,7 +134,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
@@ -188,7 +184,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
@@ -245,7 +240,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
@@ -302,7 +296,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
@@ -360,7 +353,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge 1 |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
@@ -373,7 +365,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge 2 |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
@@ -415,7 +406,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge 1 |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
@@ -430,7 +420,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge 2 |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
index 88bc30a..f84cf82 100644 (file)
@@ -44,7 +44,6 @@ Feature: Award badges based on competency completion
     And I set the following fields to these values:
       | Name | Course Badge |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     # Set the competency as a criteria for the badge
@@ -89,7 +88,6 @@ Feature: Award badges based on competency completion
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     # Set the competency as a criteria for the badge
@@ -142,7 +140,6 @@ Feature: Award badges based on competency completion
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     # Set the competency as a criteria for the badge
index 14cc5b5..93be0ed 100644 (file)
@@ -14,7 +14,6 @@ Feature: Award badges based on user profile field
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Profile completion"
index e89489b..036ff5d 100644 (file)
@@ -26,7 +26,6 @@ Feature: Test role visibility for the badge administration page
     And I set the following fields to these values:
       | Name | Course Badge |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Manual issue by role"
@@ -42,7 +41,6 @@ Feature: Test role visibility for the badge administration page
     And I set the following fields to these values:
       | Name | Course Badge |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the following fields to these values:
index ef6f892..faca603 100644 (file)
@@ -1,5 +1,10 @@
 This files describes API changes in /badges/*,
 information provided here is intended especially for developers.
+
+=== 3.9 ===
+* BADGE_BACKPACKAPIURL and BADGE_BACKPACKWEBURL are deprecated and should not be used.
+* OBv2 has been set to the default value when the obversion is not defined.
+
 === 3.7 ===
 * BADGE_BACKPACKURL is deprecated and should not be used.
 * Incorrect term "badge competencies" has been refactored to "alignments" everywhere.
index d1acd8b..7ce5b00 100644 (file)
@@ -33,23 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function badges_install_default_backpacks() {
     global $DB;
 
-    $record = new stdClass();
-    $record->backpackweburl = 'https://backpack.openbadges.org';
-    $record->backpackapiurl = 'https://backpack.openbadges.org';
-    $record->apiversion = 1;
-    $record->sortorder = 0;
-    $record->password = '';
-
-    if (!($bp = $DB->get_record('badge_external_backpack', array('backpackapiurl' => $record->backpackapiurl)))) {
-        $bpid = $DB->insert_record('badge_external_backpack', $record);
-    } else {
-        $bpid = $bp->id;
-    }
-    set_config('badges_site_backpack', $bpid);
-
-    // All existing backpacks default to V1.
-    $DB->set_field('badge_backpack', 'externalbackpackid', $bpid);
-
     $record = new stdClass();
     $record->backpackapiurl = 'https://api.badgr.io/v2';
     $record->backpackweburl = 'https://badgr.io';
@@ -57,9 +40,16 @@ function badges_install_default_backpacks() {
     $record->sortorder = 1;
     $record->password = '';
 
-    if (!$DB->record_exists('badge_external_backpack', array('backpackapiurl' => $record->backpackapiurl))) {
-        $DB->insert_record('badge_external_backpack', $record);
+    $bp = $DB->get_record('badge_external_backpack', ['backpackapiurl' => $record->backpackapiurl]);
+    if ($bp) {
+        $bpid = $bp->id;
+    } else {
+        $bpid = $DB->insert_record('badge_external_backpack', $record);
     }
 
+    set_config('badges_site_backpack', $bpid);
+
+    // Set external backpack to v2.
+    $DB->set_field('badge_backpack', 'externalbackpackid', $bpid);
 }
 
index 5d6f205..6ecdbcd 100644 (file)
@@ -24,7 +24,7 @@ Feature: Add a bookmarks to an admin pages
     And I navigate to "Notifications" in site administration
     And I click on "Scheduled tasks" "link" in the "Admin bookmarks" "block"
     # Verify that we are on the right page.
-    Then I should see "Scheduled tasks" in the "h1" "css_element"
+    Then I should see "Day of week" in the "admintable" "table"
 
   Scenario: Admin page can be removed from bookmarks
     Given I log in as "admin"
index 8fb51b6..96eefc6 100644 (file)
@@ -67,7 +67,7 @@ class block_badges extends block_base {
     }
 
     public function get_content() {
-        global $USER, $PAGE, $CFG;
+        global $USER, $CFG;
 
         if ($this->content !== null) {
             return $this->content;
@@ -105,4 +105,4 @@ class block_badges extends block_base {
 
         return $this->content;
     }
-}
\ No newline at end of file
+}
index cc739af..32d1626 100644 (file)
@@ -21,7 +21,6 @@ Feature: Enable Block Badges in a course
     And I set the following fields to these values:
       | id_name | Badge 1 |
       | id_description | Badge 1 |
-      | id_issuername | Teacher 1 |
     And I upload "blocks/badges/tests/fixtures/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I select "Manual issue by role" from the "Add badge criteria" singleselect
@@ -39,7 +38,6 @@ Feature: Enable Block Badges in a course
     And I set the following fields to these values:
       | id_name | Badge 2 |
       | id_description | Badge 2 |
-      | id_issuername | Teacher 1 |
     And I upload "blocks/badges/tests/fixtures/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I select "Manual issue by role" from the "Add badge criteria" singleselect
index 45d10c7..fe21e46 100644 (file)
@@ -21,7 +21,6 @@ Feature: Enable Block Badges on the dashboard and view awarded badges
     And I set the following fields to these values:
       | id_name | Badge 1 |
       | id_description | Badge 1 |
-      | id_issuername | Teacher 1 |
     And I upload "blocks/badges/tests/fixtures/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I select "Manual issue by role" from the "Add badge criteria" singleselect
index 89a99de..388f4a3 100644 (file)
@@ -26,7 +26,6 @@ Feature: Enable Block Badges on the frontpage and view awarded badges
     And I set the following fields to these values:
       | id_name | Badge 1 |
       | id_description | Badge 1 |
-      | id_issuername | Teacher 1 |
     And I upload "blocks/badges/tests/fixtures/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I select "Manual issue by role" from the "Add badge criteria" singleselect
index b36facc..a54d5cc 100644 (file)
@@ -46,7 +46,7 @@ class block_comments extends block_base {
     }
 
     function get_content() {
-        global $CFG, $PAGE;
+        global $CFG;
         if ($this->content !== NULL) {
             return $this->content;
         }
@@ -64,10 +64,10 @@ class block_comments extends block_base {
         if (empty($this->instance)) {
             return $this->content;
         }
-        list($context, $course, $cm) = get_context_info_array($PAGE->context->id);
+        list($context, $course, $cm) = get_context_info_array($this->page->context->id);
 
         $args = new stdClass;
-        $args->context   = $PAGE->context;
+        $args->context   = $this->page->context;
         $args->course    = $course;
         $args->area      = 'page_comments';
         $args->itemid    = 0;
index fba8113..dcebb01 100644 (file)
@@ -41,7 +41,6 @@ class block_private_files extends block_base {
     }
 
     function get_content() {
-        global $CFG, $USER, $PAGE, $OUTPUT;
 
         if ($this->content !== NULL) {
             return $this->content;
@@ -62,7 +61,7 @@ class block_private_files extends block_base {
             $this->content->text = $renderer->private_files_tree();
             if (has_capability('moodle/user:manageownfiles', $this->context)) {
                 $this->content->footer = html_writer::link(
-                    new moodle_url('/user/files.php', array('returnurl' => $PAGE->url->out())),
+                    new moodle_url('/user/files.php', array('returnurl' => $this->page->url->out())),
                     get_string('privatefilesmanage') . '...');
             }
 
index 06052c7..3f3e292 100644 (file)
@@ -59,7 +59,6 @@
      * @return block_rss_client\output\footer|null The renderable footer or null if none should be displayed.
      */
     protected function get_footer($feedrecords) {
-        global $PAGE;
         $footer = null;
 
         if ($this->config->block_rss_client_show_channel_link) {
@@ -80,7 +79,8 @@
                 if ($footer === null) {
                     $footer = new block_rss_client\output\footer();
                 }
-                $manageurl = new moodle_url('/blocks/rss_client/managefeeds.php', ['courseid' => $PAGE->course->id]);
+                $manageurl = new moodle_url('/blocks/rss_client/managefeeds.php',
+                        ['courseid' => $this->page->course->id]);
                 $footer->set_failed($manageurl);
             }
         }
index 96f79f9..e6d9c65 100644 (file)
@@ -90,8 +90,7 @@ class block_settings extends block_base {
     }
 
     function get_required_javascript() {
-        global $PAGE;
-        $adminnode = $PAGE->settingsnav->find('siteadministration', navigation_node::TYPE_SITE_ADMIN);
+        $adminnode = $this->page->settingsnav->find('siteadministration', navigation_node::TYPE_SITE_ADMIN);
         parent::get_required_javascript();
         $arguments = array(
             'instanceid' => $this->instance->id,
index 56f6374..ea7a72e 100644 (file)
Binary files a/course/amd/build/activitychooser.min.js and b/course/amd/build/activitychooser.min.js differ
index 9201790..f410d70 100644 (file)
Binary files a/course/amd/build/activitychooser.min.js.map and b/course/amd/build/activitychooser.min.js.map differ
index 65c611f..92a8164 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js and b/course/amd/build/local/activitychooser/dialogue.min.js differ
index 8ea4836..6a85405 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js.map and b/course/amd/build/local/activitychooser/dialogue.min.js.map differ
index 37df273..bc9cee0 100644 (file)
Binary files a/course/amd/build/local/activitychooser/selectors.min.js and b/course/amd/build/local/activitychooser/selectors.min.js differ
index 75707c6..f6b3784 100644 (file)
Binary files a/course/amd/build/local/activitychooser/selectors.min.js.map and b/course/amd/build/local/activitychooser/selectors.min.js.map differ
index cb58e9f..0207281 100644 (file)
@@ -176,20 +176,28 @@ const buildModal = data => {
  * @param {HTMLElement} modalBody Our current modals' body
  */
 const nullFavouriteDomManager = (favouriteTabNav, modalBody) => {
+    favouriteTabNav.tabIndex = -1;
     favouriteTabNav.classList.add('d-none');
     // Need to set active to an available tab.
     if (favouriteTabNav.classList.contains('active')) {
         favouriteTabNav.classList.remove('active');
+        favouriteTabNav.setAttribute('aria-selected', 'false');
         const favouriteTab = modalBody.querySelector(selectors.regions.favouriteTab);
         favouriteTab.classList.remove('active');
         const recommendedTabNav = modalBody.querySelector(selectors.regions.recommendedTabNav);
         const defaultTabNav = modalBody.querySelector(selectors.regions.defaultTabNav);
         if (recommendedTabNav.classList.contains('d-none') === false) {
             recommendedTabNav.classList.add('active');
+            recommendedTabNav.setAttribute('aria-selected', 'true');
+            recommendedTabNav.tabIndex = 0;
+            recommendedTabNav.focus();
             const recommendedTab = modalBody.querySelector(selectors.regions.recommendedTab);
             recommendedTab.classList.add('active');
         } else {
             defaultTabNav.classList.add('active');
+            defaultTabNav.setAttribute('aria-selected', 'true');
+            defaultTabNav.tabIndex = 0;
+            defaultTabNav.focus();
             const defaultTab = modalBody.querySelector(selectors.regions.defaultTab);
             defaultTab.classList.add('active');
         }
index 9f803c3..6a476cf 100644 (file)
@@ -108,7 +108,7 @@ const manageFavouriteState = async(modalBody, caller, partialFavourite) => {
  * @param {Function} partialFavourite Partially applied function we need to manage favourite status
  */
 const registerListenerEvents = (modal, mappedModules, partialFavourite) => {
-    const bodyClickListener = e => {
+    const bodyClickListener = async(e) => {
         if (e.target.closest(selectors.actions.optionActions.showSummary)) {
             const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));
 
@@ -120,7 +120,14 @@ const registerListenerEvents = (modal, mappedModules, partialFavourite) => {
 
         if (e.target.closest(selectors.actions.optionActions.manageFavourite)) {
             const caller = e.target.closest(selectors.actions.optionActions.manageFavourite);
-            manageFavouriteState(modal.getBody()[0], caller, partialFavourite);
+            await manageFavouriteState(modal.getBody()[0], caller, partialFavourite);
+            const activeSectionId = modal.getBody()[0].querySelector(selectors.elements.activetab).getAttribute("href");
+            const sectionChooserOptions = modal.getBody()[0]
+                .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));
+            const firstChooserOption = sectionChooserOptions
+                .querySelector(selectors.regions.chooserOption.container);
+            toggleFocusableChooserOption(firstChooserOption, true);
+            initChooserOptionsKeyboardNavigation(modal.getBody()[0], mappedModules, sectionChooserOptions);
         }
 
         // From the help screen go back to the module overview.
@@ -208,49 +215,51 @@ const initTabsKeyboardNavigation = (body) => {
     const defaultTabNav = body.querySelector(selectors.regions.defaultTabNav);
     const tabNavArray = [favTabNav, recommendedTabNav, defaultTabNav];
     tabNavArray.forEach((element) => {
-        return element.addEventListener('keyup', (e) => {
-            const firstLink = e.target.parentElement.parentElement.firstElementChild.firstElementChild;
-            const lastLink = e.target.parentElement.parentElement.lastElementChild.firstElementChild;
+        return element.addEventListener('keydown', (e) => {
+            // The first visible navigation tab link.
+            const firstLink = e.target.parentElement.querySelector(selectors.elements.visibletabs);
+            // The last navigation tab link. It would always be the default activities tab link.
+            const lastLink = e.target.parentElement.lastElementChild;
 
             if (e.keyCode === arrowRight) {
-                const nextLink = e.target.parentElement.nextElementSibling;
+                const nextLink = e.target.nextElementSibling;
                 if (nextLink === null) {
-                    e.srcElement.tabIndex = -1;
+                    e.target.tabIndex = -1;
                     firstLink.tabIndex = 0;
                     firstLink.focus();
-                } else if (nextLink.firstElementChild.classList.contains('d-none')) {
-                    e.srcElement.tabIndex = -1;
+                } else if (nextLink.classList.contains('d-none')) {
+                    e.target.tabIndex = -1;
                     lastLink.tabIndex = 0;
                     lastLink.focus();
                 } else {
-                    e.srcElement.tabIndex = -1;
-                    nextLink.firstElementChild.tabIndex = 0;
-                    nextLink.firstElementChild.focus();
+                    e.target.tabIndex = -1;
+                    nextLink.tabIndex = 0;
+                    nextLink.focus();
                 }
             }
             if (e.keyCode === arrowLeft) {
-                const previousLink = e.target.parentElement.previousElementSibling;
+                const previousLink = e.target.previousElementSibling;
                 if (previousLink === null) {
-                    e.srcElement.tabIndex = -1;
+                    e.target.tabIndex = -1;
                     lastLink.tabIndex = 0;
                     lastLink.focus();
-                } else if (previousLink.firstElementChild.classList.contains('d-none')) {
-                    e.srcElement.tabIndex = -1;
+                } else if (previousLink.classList.contains('d-none')) {
+                    e.target.tabIndex = -1;
                     firstLink.tabIndex = 0;
                     firstLink.focus();
                 } else {
-                    e.srcElement.tabIndex = -1;
-                    previousLink.firstElementChild.tabIndex = 0;
-                    previousLink.firstElementChild.focus();
+                    e.target.tabIndex = -1;
+                    previousLink.tabIndex = 0;
+                    previousLink.focus();
                 }
             }
             if (e.keyCode === home) {
-                e.srcElement.tabIndex = -1;
+                e.target.tabIndex = -1;
                 firstLink.tabIndex = 0;
                 firstLink.focus();
             }
             if (e.keyCode === end) {
-                e.srcElement.tabIndex = -1;
+                e.target.tabIndex = -1;
                 lastLink.tabIndex = 0;
                 lastLink.focus();
             }
@@ -274,7 +283,7 @@ const initChooserOptionsKeyboardNavigation = (body, mappedModules, chooserOption
     const chooserOptions = chooserOptionsContainer.querySelectorAll(selectors.regions.chooserOption.container);
 
     Array.from(chooserOptions).forEach((element) => {
-        return element.addEventListener('keyup', (e) => {
+        return element.addEventListener('keydown', (e) => {
 
             // Check for enter/ space triggers for showing the help.
             if (e.keyCode === enter || e.keyCode === space) {
index a072a1e..b8361e6 100644 (file)
@@ -82,6 +82,7 @@ export default {
         sitetopic: 'div.sitetopic',
         tab: 'a[data-toggle="tab"]',
         activetab: 'a[data-toggle="tab"][aria-selected="true"]',
+        visibletabs: 'a[data-toggle="tab"]:not(.d-none)',
         searchicon: '.searchbar-append .search-icon',
         clearsearch: '.searchbar-append .clear'
     },
index 162a68d..bece6a1 100644 (file)
@@ -374,9 +374,7 @@ class core_course_management_renderer extends plugin_renderer_base {
     }
 
     public function render_action_menu($menu) {
-        global $OUTPUT;
-
-        return $OUTPUT->render($menu);
+        return $this->output->render($menu);
     }
 
     /**
index 50a5fe1..7f4beaa 100644 (file)
@@ -184,8 +184,6 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
      * @return string HTML to output.
      */
     protected function section_header($section, $course, $onsectionpage, $sectionreturn=null) {
-        global $PAGE;
-
         $o = '';
         $currenttext = '';
         $sectionstyle = '';
@@ -270,9 +268,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
      * @return array of edit control items
      */
     protected function section_edit_control_items($course, $section, $onsectionpage = false) {
-        global $PAGE;
-
-        if (!$PAGE->user_is_editing()) {
+        if (!$this->page->user_is_editing()) {
             return array();
         }
 
@@ -743,8 +739,6 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
      * @param int $displaysection The section number in the course which is being displayed
      */
     public function print_single_section_page($course, $sections, $mods, $modnames, $modnamesused, $displaysection) {
-        global $PAGE;
-
         $modinfo = get_fast_modinfo($course);
         $course = course_get_format($course)->get_course();
 
@@ -759,7 +753,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         // Copy activity clipboard..
         echo $this->course_activity_clipboard($course, $displaysection);
         $thissection = $modinfo->get_section_info(0);
-        if ($thissection->summary or !empty($modinfo->sections[0]) or $PAGE->user_is_editing()) {
+        if ($thissection->summary or !empty($modinfo->sections[0]) or $this->page->user_is_editing()) {
             echo $this->start_section_list();
             echo $this->section_header($thissection, $course, true, $displaysection);
             echo $this->courserenderer->course_section_cm_list($course, $thissection, $displaysection);
@@ -828,8 +822,6 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
      * @param array $modnamesused (argument not used)
      */
     public function print_multiple_section_page($course, $sections, $mods, $modnames, $modnamesused) {
-        global $PAGE;
-
         $modinfo = get_fast_modinfo($course);
         $course = course_get_format($course)->get_course();
 
@@ -849,7 +841,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         foreach ($modinfo->get_section_info_all() as $section => $thissection) {
             if ($section == 0) {
                 // 0-section is displayed a little different then the others
-                if ($thissection->summary or !empty($modinfo->sections[0]) or $PAGE->user_is_editing()) {
+                if ($thissection->summary or !empty($modinfo->sections[0]) or $this->page->user_is_editing()) {
                     echo $this->section_header($thissection, $course, false, 0);
                     echo $this->courserenderer->course_section_cm_list($course, $thissection, 0);
                     echo $this->courserenderer->course_section_add_cm_control($course, 0, 0);
@@ -871,7 +863,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                 continue;
             }
 
-            if (!$PAGE->user_is_editing() && $course->coursedisplay == COURSE_DISPLAY_MULTIPAGE) {
+            if (!$this->page->user_is_editing() && $course->coursedisplay == COURSE_DISPLAY_MULTIPAGE) {
                 // Display section summary only.
                 echo $this->section_summary($thissection, $course, null);
             } else {
@@ -884,7 +876,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
             }
         }
 
-        if ($PAGE->user_is_editing() and has_capability('moodle/course:update', $context)) {
+        if ($this->page->user_is_editing() and has_capability('moodle/course:update', $context)) {
             // Print stealth sections if present.
             foreach ($modinfo->get_section_info_all() as $section => $thissection) {
                 if ($section <= $numsections or empty($modinfo->sections[$section])) {
index c7e6941..8d56504 100644 (file)
@@ -40,14 +40,17 @@ M.course.format.swap_sections = function(Y, node1, node2) {
     };
 
     var sectionlist = Y.Node.all('.'+CSS.COURSECONTENT+' '+M.course.format.get_section_selector(Y));
-    // Swap menus.
-    sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS));
+    // Swap the non-ajax menus, noting these are not always present (depends on theme and user prefs).
+    if (sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS)) {
+        sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS));
+    }
 }
 
 /**
  * Process sections after ajax response
  *
  * @param {YUI} Y YUI3 instance
+ * @param {NodeList} sectionlist of sections
  * @param {array} response ajax response
  * @param {string} sectionfrom first affected section
  * @param {string} sectionto last affected section
@@ -76,13 +79,14 @@ M.course.format.process_sections = function(Y, sectionlist, response, sectionfro
             // Update section title.
             var content = Y.Node.create('<span>' + response.sectiontitles[i] + '</span>');
             sectionlist.item(i).all('.'+CSS.SECTIONNAME).setHTML(content);
-            // Update move icon.
-            ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE);
-            str = ele.getAttribute('alt');
+            // Update the drag handle.
+            ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE).ancestor('.section-handle');
+            str = ele.getAttribute('title');
             stridx = str.lastIndexOf(' ');
             newstr = str.substr(0, stridx +1) + i;
-            ele.setAttribute('alt', newstr);
-            ele.setAttribute('title', newstr); // For FireFox as 'alt' is not refreshed.
+            ele.setAttribute('title', newstr);
+            // Update the aria-label for the section.
+            sectionlist.item(i).setAttribute('aria-label', content.get('innerText').trim());
         }
     }
 }
index 67551c5..5d11be9 100644 (file)
@@ -104,9 +104,7 @@ class format_topics_renderer extends format_section_renderer_base {
      * @return array of edit control items
      */
     protected function section_edit_control_items($course, $section, $onsectionpage = false) {
-        global $PAGE;
-
-        if (!$PAGE->user_is_editing()) {
+        if (!$this->page->user_is_editing()) {
             return array();
         }
 
index bac0e59..382287d 100644 (file)
@@ -11,6 +11,9 @@ Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
     - format_weeks_upgrade_remove_numsections()
     - format_weeks_upgrade_hide_extra_sections()
     - format_weeks_upgrade_add_empty_sections()
+* The non-ajax controls to add resources and activities are now rendered only when needed, such as when the user
+  preference is set, or when the theme sets $THEME->enablecourseajaxtheme to false. Formats which directly access
+  the '.section_add_menus' element or its children should be updated accordingly.
 
 === 3.8 ===
 
index 1fb0c8e..0ad69b8 100644 (file)
@@ -40,14 +40,17 @@ M.course.format.swap_sections = function(Y, node1, node2) {
     };
 
     var sectionlist = Y.Node.all('.'+CSS.COURSECONTENT+' '+M.course.format.get_section_selector(Y));
-    // Swap menus.
-    sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS));
+    // Swap the non-ajax menus, noting these are not always present (depends on theme and user prefs).
+    if (sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS)) {
+        sectionlist.item(node1).one('.' + CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.' + CSS.SECTIONADDMENUS));
+    }
 }
 
 /**
  * Process sections after ajax response
  *
  * @param {YUI} Y YUI3 instance
+ * @param {NodeList} sectionlist of sections
  * @param {array} response ajax response
  * @param {string} sectionfrom first affected section
  * @param {string} sectionto last affected section
@@ -77,13 +80,14 @@ M.course.format.process_sections = function(Y, sectionlist, response, sectionfro
             var content = Y.Node.create('<span>' + response.sectiontitles[i] + '</span>');
             sectionlist.item(i).all('.'+CSS.SECTIONNAME).setHTML(content);
 
-            // Update move icon.
-            ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE);
-            str = ele.getAttribute('alt');
+            // Update the drag handle.
+            ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE).ancestor('.section-handle');
+            str = ele.getAttribute('title');
             stridx = str.lastIndexOf(' ');
             newstr = str.substr(0, stridx +1) + i;
-            ele.setAttribute('alt', newstr);
-            ele.setAttribute('title', newstr); // For FireFox as 'alt' is not refreshed.
+            ele.setAttribute('title', newstr);
+            // Update the aria-label for the section.
+            sectionlist.item(i).setAttribute('aria-label', content.get('innerText').trim());
 
             // Remove the current class as section has been moved.
             sectionlist.item(i).removeClass('current');
index 8c0965c..43efcd4 100644 (file)
@@ -33,7 +33,7 @@
     <tbody>
         {{#categorydata}}
         <tr class="d-flex">
-            <td class="font-weight-bold col-7 c0"><span>{{{icon}}}</span>{{name}}</td>
+            <td class="col-7 c0"><span>{{{icon}}}</span>{{name}}</td>
             {{#id}}
             <td class="col-5 c1 colselect">
             <input class="activity-recommend-checkbox" type="checkbox" aria-label="{{#str}}recommendcheckbox, course, {{name}}{{/str}}" data-area="{{componentname}}" data-id="{{id}}" {{#recommended}}checked="checked"{{/recommended}}  />
index 7bd4b3d..45d9a25 100644 (file)
                     {{>core_course/local/activitychooser/search}}
                 </div>
                 <div data-region="chooser-container">
-                    <ul class="nav nav-tabs mb-3" id="activities-{{uniqid}}" role="tablist">
-                        <li class="nav-item">
-                            <a class="nav-link {{#favouritesFirst}}active{{/favouritesFirst}} {{^favourites}}d-none{{/favourites}}"
-                               id="starred-tab-{{uniqid}}"
-                               data-toggle="tab"
-                               data-region="favourite-tab-nav"
-                               href="#starred-{{uniqid}}"
-                               role="tab"
-                               aria-label="{{#str}} aria:favouritestab, core_course {{/str}}"
-                               aria-controls="starred-{{uniqid}}"
-                               aria-selected="{{#favouritesFirst}}true{{/favouritesFirst}}{{^favouritesFirst}}false{{/favouritesFirst}}"
-                               tabindex="{{#favouritesFirst}}0{{/favouritesFirst}}{{^favouritesFirst}}-1{{/favouritesFirst}}"
-                            >
-                                {{#str}} favourites, core {{/str}}
-                            </a>
-                        </li>
-                        <li class="nav-item">
-                            <a class="nav-link {{#recommendedFirst}}active{{/recommendedFirst}} {{^recommended}}d-none{{/recommended}}"
-                               id="recommended-tab-{{uniqid}}"
-                               data-region="recommended-tab-nav"
-                               data-toggle="tab"
-                               href="#recommended-{{uniqid}}"
-                               role="tab"
-                               aria-label="{{#str}} aria:recommendedtab, core_course {{/str}}"
-                               aria-controls="recommended-{{uniqid}}"
-                               aria-selected="{{#recommendedFirst}}true{{/recommendedFirst}}{{^recommendedFirst}}false{{/recommendedFirst}}"
-                               tabindex="{{#recommendedFirst}}0{{/recommendedFirst}}{{^recommendedFirst}}-1{{/recommendedFirst}}">
-                                {{#str}} recommended, core {{/str}}
-                            </a>
-                        </li>
-                        <li class="nav-item">
-                            <a class="nav-link {{#fallback}}active{{/fallback}}"
-                               id="all-tab-{{uniqid}}"
-                               data-toggle="tab"
-                               data-region="default-tab-nav"
-                               href="#all-{{uniqid}}"
-                               role="tab"
-                               aria-label="{{#str}} aria:defaulttab, core_course {{/str}}"
-                               aria-controls="all-{{uniqid}}"
-                               aria-selected="{{#fallback}}true{{/fallback}}{{^fallback}}false{{/fallback}}"
-                               tabindex="{{#fallback}}0{{/fallback}}{{^fallback}}-1{{/fallback}}"
-                            >
-                                {{#str}} activities, core {{/str}}
-                            </a>
-                        </li>
-                    </ul>
+                    <div class="nav nav-tabs mb-3" id="activities-{{uniqid}}" role="tablist">
+                        <a class="nav-item nav-link {{#favouritesFirst}}active{{/favouritesFirst}} {{^favourites}}d-none{{/favourites}}"
+                           id="starred-tab-{{uniqid}}"
+                           data-toggle="tab"
+                           data-region="favourite-tab-nav"
+                           href="#starred-{{uniqid}}"
+                           role="tab"
+                           aria-label="{{#str}} aria:favouritestab, core_course {{/str}}"
+                           aria-controls="starred-{{uniqid}}"
+                           aria-selected="{{#favouritesFirst}}true{{/favouritesFirst}}{{^favouritesFirst}}false{{/favouritesFirst}}"
+                           tabindex="{{#favouritesFirst}}0{{/favouritesFirst}}{{^favouritesFirst}}-1{{/favouritesFirst}}"
+                        >
+                            {{#str}} favourites, core {{/str}}
+                        </a>
+                        <a class="nav-item nav-link {{#recommendedFirst}}active{{/recommendedFirst}} {{^recommended}}d-none{{/recommended}}"
+                           id="recommended-tab-{{uniqid}}"
+                           data-region="recommended-tab-nav"
+                           data-toggle="tab"
+                           href="#recommended-{{uniqid}}"
+                           role="tab"
+                           aria-label="{{#str}} aria:recommendedtab, core_course {{/str}}"
+                           aria-controls="recommended-{{uniqid}}"
+                           aria-selected="{{#recommendedFirst}}true{{/recommendedFirst}}{{^recommendedFirst}}false{{/recommendedFirst}}"
+                           tabindex="{{#recommendedFirst}}0{{/recommendedFirst}}{{^recommendedFirst}}-1{{/recommendedFirst}}"
+                        >
+                            {{#str}} recommended, core {{/str}}
+                        </a>
+                        <a class="nav-item nav-link {{#fallback}}active{{/fallback}}"
+                           id="all-tab-{{uniqid}}"
+                           data-toggle="tab"
+                           data-region="default-tab-nav"
+                           href="#all-{{uniqid}}"
+                           role="tab"
+                           aria-label="{{#str}} aria:defaulttab, core_course {{/str}}"
+                           aria-controls="all-{{uniqid}}"
+                           aria-selected="{{#fallback}}true{{/fallback}}{{^fallback}}false{{/fallback}}"
+                           tabindex="{{#fallback}}0{{/fallback}}{{^fallback}}-1{{/fallback}}"
+                        >
+                            {{#str}} activities, core {{/str}}
+                        </a>
+                    </div>
                     <div class="tab-content" id="tabbed-activities-{{uniqid}}">
                         <div class="tab-pane {{#favouritesFirst}}active{{/favouritesFirst}}" id="starred-{{uniqid}}" data-region="favourites" role="tabpanel" aria-labelledby="starred-tab-{{uniqid}}">
                             <div class="optionscontainer d-flex flex-wrap p-1 mw-100 position-relative" role="menubar" data-region="chooser-options-container" data-render="favourites-area">
index 88a9706..dc8e8f1 100644 (file)
Binary files a/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-debug.js and b/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-debug.js differ
index 6ea5d1b..5782dd8 100644 (file)
Binary files a/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-min.js and b/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-min.js differ
index 6c016e6..5d985c2 100644 (file)
Binary files a/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop.js and b/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop.js differ
index fc32f51..5864ec1 100644 (file)
@@ -116,6 +116,11 @@ Y.extend(DRAGRESOURCE, M.core.dragdrop, {
     drag_start: function(e) {
         // Get our drag object
         var drag = e.target;
+        if (drag.get('dragNode') === drag.get('node')) {
+            // We do not want to modify the contents of the real node.
+            // They will be the same during a keyboard drag and drop.
+            return;
+        }
         drag.get('dragNode').setContent(drag.get('node').get('innerHTML'));
         drag.get('dragNode').all('img.iconsmall').setStyle('vertical-align', 'baseline');
     },
index efbb73e..ac80e63 100644 (file)
@@ -16,6 +16,8 @@ Y.extend(DRAGSECTION, M.core.dragdrop, {
         this.groups = [CSS.SECTIONDRAGGABLE];
         this.samenodeclass = M.course.format.get_sectionwrapperclass();
         this.parentnodeclass = M.course.format.get_containerclass();
+        // Detect the direction of travel.
+        this.detectkeyboarddirection = true;
 
         // Check if we are in single section mode
         if (Y.Node.one('.' + CSS.JUMPMENU)) {
@@ -115,6 +117,14 @@ Y.extend(DRAGSECTION, M.core.dragdrop, {
     drag_start: function(e) {
         // Get our drag object
         var drag = e.target;
+        // This is the node that the user started to drag.
+        var node = drag.get('node');
+        // This is the container node that will follow the mouse around,
+        // or during a keyboard drag and drop the original node.
+        var dragnode = drag.get('dragNode');
+        if (node === dragnode) {
+            return;
+        }
         // Creat a dummy structure of the outer elemnents for clean styles application
         var containernode = Y.Node.create('<' + M.course.format.get_containernode() +
                 '></' + M.course.format.get_containernode() + '>');
@@ -123,10 +133,10 @@ Y.extend(DRAGSECTION, M.core.dragdrop, {
                 '></' + M.course.format.get_sectionwrappernode() + '>');
         sectionnode.addClass(M.course.format.get_sectionwrapperclass());
         sectionnode.setStyle('margin', 0);
-        sectionnode.setContent(drag.get('node').get('innerHTML'));
+        sectionnode.setContent(node.get('innerHTML'));
         containernode.appendChild(sectionnode);
-        drag.get('dragNode').setContent(containernode);
-        drag.get('dragNode').addClass(CSS.COURSECONTENT);
+        dragnode.setContent(containernode);
+        dragnode.addClass(CSS.COURSECONTENT);
     },
 
     drag_dropmiss: function(e) {
index 1292a9e..ef85ebc 100644 (file)
@@ -60,7 +60,6 @@ class gradingform_guide_renderer extends plugin_renderer_base {
      */
     public function criterion_template($mode, $options, $elementname = '{NAME}', $criterion = null, $value = null,
                                        $validationerrors = null, $comments = null) {
-        global $PAGE;
 
         if ($criterion === null || !is_array($criterion) || !array_key_exists('id', $criterion)) {
             $criterion = array('id' => '{CRITERION-id}',
@@ -254,9 +253,9 @@ class gradingform_guide_renderer extends plugin_renderer_base {
                 }
 
                 // Include string for JS for the comment chooser title.
-                $PAGE->requires->string_for_js('insertcomment', 'gradingform_guide');
+                $this->page->requires->string_for_js('insertcomment', 'gradingform_guide');
                 // Include comment_chooser module.
-                $PAGE->requires->js_call_amd('gradingform_guide/comment_chooser', 'initialise',
+                $this->page->requires->js_call_amd('gradingform_guide/comment_chooser', 'initialise',
                     array($criterion['id'], $chooserbuttonid, $remarkid, $commentoptions));
             }
 
index 7062bdd..6149d5d 100644 (file)
@@ -51,8 +51,8 @@ class gradereport_user_renderer extends plugin_renderer_base {
      * @return string
      */
     public function view_user_selector($userid, $userview) {
-        global $PAGE, $USER;
-        $url = $PAGE->url;
+        global $USER;
+        $url = $this->page->url;
         if ($userid != $USER->id) {
             $url->param('userid', $userid);
         }
index 3c15d85..6b0c3a4 100644 (file)
@@ -146,7 +146,6 @@ class core extends \H5PCore {
     public static function get_scripts(): array {
         global $PAGE;
 
-        $factory = new factory();
         $jsrev = $PAGE->requires->get_jsrev();
         $urls = [];
         foreach (self::$scripts as $script) {
@@ -224,10 +223,6 @@ class core extends \H5PCore {
     public function fetch_content_type(array $library): ?int {
         $factory = new factory();
 
-        // Get a temp path to download the content type.
-        $temppath = make_request_directory();
-        $tempfile = "{$temppath}/" . $library['machineName'] . ".h5p";
-
         // Download the latest content type from the H5P official repository.
         $fs = get_file_storage();
         $file = $fs->create_file_from_url(
@@ -287,6 +282,8 @@ class core extends \H5PCore {
      *     - array contentTypes: an object for each H5P content type with its information
      */
     public function get_latest_content_types(): \stdClass {
+        global $CFG;
+
         $siteuuid = $this->get_site_uuid() ?? md5($CFG->wwwroot);
         $postdata = ['uuid' => $siteuuid];
 
index 10fa5d1..7aa080a 100644 (file)
@@ -999,6 +999,7 @@ $string['changedpassword'] = 'Changed password';
 $string['changepassword'] = 'Change password';
 $string['changessaved'] = 'Changes saved';
 $string['check'] = 'Check';
+$string['checks'] = 'Checks';
 $string['checkall'] = 'Check all';
 $string['checkingbackup'] = 'Checking backup';
 $string['checkingcourse'] = 'Checking course';
@@ -1987,6 +1988,12 @@ $string['statsuserreads'] = 'Views';
 $string['statsuserwrites'] = 'Posts';
 $string['statswrites'] = 'Posts';
 $string['status'] = 'Status';
+$string['statuscritical'] = 'Critical';
+$string['statusinfo'] = 'Info';
+$string['statusna'] = 'N/A';
+$string['statusok'] = 'OK';
+$string['statuserror'] = 'Error';
+$string['statuswarning'] = 'Warning';
 $string['stringsnotset'] = 'The following strings are not defined in {$a}';
 $string['studentnotallowed'] = 'Sorry, but you can not enter this course as \'{$a}\'';
 $string['students'] = 'Students';
index 6d58efa..4fc5e65 100644 (file)
@@ -96,8 +96,6 @@ define('BADGE_MESSAGE_MONTHLY', 4);
 /*
  * URL of backpack. Custom ones can be added.
  */
-define('BADGE_BACKPACKAPIURL', 'https://backpack.openbadges.org');
-define('BADGE_BACKPACKWEBURL', 'https://backpack.openbadges.org');
 define('BADGRIO_BACKPACKAPIURL', 'https://api.badgr.io/v2');
 define('BADGRIO_BACKPACKWEBURL', 'https://badgr.io');
 
@@ -106,6 +104,12 @@ define('BADGRIO_BACKPACKWEBURL', 'https://badgr.io');
  */
 define('BADGE_BACKPACKURL', 'https://backpack.openbadges.org');
 
+/*
+ * @deprecated since 3.9 (MDL-66357).
+ */
+define('BADGE_BACKPACKAPIURL', 'https://backpack.openbadges.org');
+define('BADGE_BACKPACKWEBURL', 'https://backpack.openbadges.org');
+
 /*
  * Open Badges specifications.
  */
diff --git a/lib/classes/check/access/defaultuserrole.php b/lib/classes/check/access/defaultuserrole.php
new file mode 100644 (file)
index 0000000..da15a57
--- /dev/null
@@ -0,0 +1,111 @@
+<?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/>.
+
+/**
+ * Verifies sanity of default user role.
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\access;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Verifies sanity of default user role.
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class defaultuserrole extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_defaultuserrole_name', 'report_security');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        global $CFG;
+        return new \action_link(
+            new \moodle_url('/admin/roles/define.php?action=view&roleid=' . $CFG->defaultuserroleid),
+            get_string('userpolicies', 'admin'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $DB, $CFG;
+        $details = '';
+
+        if (!$defaultrole = $DB->get_record('role', ['id' => $CFG->defaultuserroleid])) {
+            $status  = result::WARNING;
+            $summary = get_string('check_defaultuserrole_notset', 'report_security');
+            return new result($status, $summary, $details);
+        }
+
+        // Risky caps - usually very dangerous.
+        $sql = "SELECT COUNT(DISTINCT rc.contextid)
+                  FROM {role_capabilities} rc
+                  JOIN {capabilities} cap ON cap.name = rc.capability
+                 WHERE " . $DB->sql_bitand('cap.riskbitmask', (RISK_XSS | RISK_CONFIG | RISK_DATALOSS)) . " <> 0
+                   AND rc.permission = :capallow
+                   AND rc.roleid = :roleid";
+
+        $riskycount = $DB->count_records_sql($sql, [
+            'capallow' => CAP_ALLOW,
+            'roleid' => $defaultrole->id,
+        ]);
+
+        // It may have either none or 'user' archetype - nothing else, or else it would break during upgrades badly.
+        if ($defaultrole->archetype === '' or $defaultrole->archetype === 'user') {
+            $legacyok = true;
+        } else {
+            $legacyok = false;
+        }
+
+        if ($riskycount or !$legacyok) {
+            $status = result::CRITICAL;
+            $summary = get_string('check_defaultuserrole_error', 'report_security', role_get_name($defaultrole));
+
+        } else {
+            $status = result::OK;
+            $summary = get_string('check_defaultuserrole_ok', 'report_security');
+        }
+
+        $details = get_string('check_defaultuserrole_details', 'report_security');
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/access/frontpagerole.php b/lib/classes/check/access/frontpagerole.php
new file mode 100644 (file)
index 0000000..5fcd3d8
--- /dev/null
@@ -0,0 +1,111 @@
+<?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/>.
+
+/**
+ * Verifies sanity of frontpage role
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\access;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Verifies sanity of frontpage role
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class frontpagerole extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_frontpagerole_name', 'report_security');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/settings.php?section=frontpagesettings#admin-defaultfrontpageroleid'),
+            get_string('frontpagesettings', 'admin'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $DB, $CFG;
+
+        if (!$frontpagerole = $DB->get_record('role', array('id' => $CFG->defaultfrontpageroleid))) {
+            $status  = result::INFO;
+            $summary = get_string('check_frontpagerole_notset', 'report_security');
+            $details = get_string('check_frontpagerole_details', 'report_security');
+            return new result($status, $summary, $details);
+        }
+
+        // Risky caps - usually very dangerous.
+        $sql = "SELECT COUNT(DISTINCT rc.contextid)
+                  FROM {role_capabilities} rc
+                  JOIN {capabilities} cap ON cap.name = rc.capability
+                 WHERE " . $DB->sql_bitand('cap.riskbitmask', (RISK_XSS | RISK_CONFIG | RISK_DATALOSS)) . " <> 0
+                   AND rc.permission = :capallow
+                   AND rc.roleid = :roleid";
+
+        $riskycount = $DB->count_records_sql($sql, [
+            'capallow' => CAP_ALLOW,
+            'roleid' => $frontpagerole->id,
+        ]);
+
+        // There is no legacy role type for frontpage yet - anyway we can not allow teachers or admins there!
+        if ($frontpagerole->archetype === 'teacher' or $frontpagerole->archetype === 'editingteacher'
+          or $frontpagerole->archetype === 'coursecreator' or $frontpagerole->archetype === 'manager') {
+            $legacyok = false;
+        } else {
+            $legacyok = true;
+        }
+
+        if ($riskycount or !$legacyok) {
+            $status  = result::CRITICAL;
+            $summary = get_string('check_frontpagerole_error', 'report_security', role_get_name($frontpagerole));
+
+        } else {
+            $status  = result::OK;
+            $summary = get_string('check_frontpagerole_ok', 'report_security');
+        }
+
+        $details = get_string('check_frontpagerole_details', 'report_security');
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/access/guestrole.php b/lib/classes/check/access/guestrole.php
new file mode 100644 (file)
index 0000000..99d1359
--- /dev/null
@@ -0,0 +1,109 @@
+<?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/>.
+
+/**
+ * Verifies sanity of guest role
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\access;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Verifies sanity of guest role
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class guestrole extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_guestrole_name', 'report_security');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/settings.php?section=userpolicies'),
+            get_string('userpolicies', 'admin'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $DB, $CFG;
+
+        if (!$guestrole = $DB->get_record('role', ['id' => $CFG->guestroleid])) {
+            $status  = result::WARNING;
+            $summary = get_string('check_guestrole_notset', 'report_security');
+            return new result($status, $summary);
+        }
+
+        // Risky caps - usually very dangerous.
+        $sql = "SELECT COUNT(DISTINCT rc.contextid)
+                  FROM {role_capabilities} rc
+                  JOIN {capabilities} cap ON cap.name = rc.capability
+                 WHERE " . $DB->sql_bitand('cap.riskbitmask', (RISK_XSS | RISK_CONFIG | RISK_DATALOSS)) . " <> 0
+                   AND rc.permission = :capallow
+                   AND rc.roleid = :roleid";
+
+        $riskycount = $DB->count_records_sql($sql, [
+            'capallow' => CAP_ALLOW,
+            'roleid' => $guestrole->id,
+        ]);
+
+        // It may have either no or 'guest' archetype - nothing else, or else it would break during upgrades badly.
+        if ($guestrole->archetype === '' or $guestrole->archetype === 'guest') {
+            $legacyok = true;
+        } else {
+            $legacyok = false;
+        }
+
+        if ($riskycount or !$legacyok) {
+            $status  = result::CRITICAL;
+            $summary = get_string('check_guestrole_error', 'report_security', format_string($guestrole->name));
+
+        } else {
+            $status  = result::OK;
+            $summary = get_string('check_guestrole_ok', 'report_security');
+        }
+
+        $details = get_string('check_guestrole_details', 'report_security');
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/access/riskadmin.php b/lib/classes/check/access/riskadmin.php
new file mode 100644 (file)
index 0000000..7c0729c
--- /dev/null
@@ -0,0 +1,90 @@
+<?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/>.
+
+/**
+ * Lists all admins.
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\access;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Lists all admins.
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class riskadmin extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_riskadmin_name', 'report_security');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/roles/admins.php'),
+            get_string('siteadministrators', 'role'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $DB, $CFG;
+        $userfields = \user_picture::fields('u');
+        $sql = "SELECT $userfields
+                  FROM {user} u
+                 WHERE u.id IN ($CFG->siteadmins)";
+
+        $admins = $DB->get_records_sql($sql);
+        $admincount = count($admins);
+
+        foreach ($admins as $uid => $user) {
+            $url = "$CFG->wwwroot/user/view.php?id=$user->id";
+            $link = \html_writer::link($url, fullname($user, true) . ' (' . s($user->email) . ')');
+            $admins[$uid] = \html_writer::tag('li' , $link);
+        }
+        $admins = \html_writer::tag('ul', implode('', $admins));
+        $status  = result::INFO;
+        $summary = get_string('check_riskadmin_ok', 'report_security', $admincount);
+        $details = get_string('check_riskadmin_detailsok', 'report_security', $admins);
+
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/access/riskbackup.php b/lib/classes/check/access/riskbackup.php
new file mode 100644 (file)
index 0000000..630efc3
--- /dev/null
@@ -0,0 +1,69 @@
+<?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/>.
+
+/**
+ * Abstract class for common properties of scheduled_task and adhoc_task.
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\access;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Lists all roles that have the ability to backup user data, as well as users
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class riskbackup extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_riskbackup_name', 'report_security');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/roles/manage.php'),
+            get_string('manageroles', 'role'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        return new riskbackup_result();
+    }
+}
+
diff --git a/lib/classes/check/access/riskbackup_result.php b/lib/classes/check/access/riskbackup_result.php
new file mode 100644 (file)
index 0000000..a234072
--- /dev/null
@@ -0,0 +1,196 @@
+<?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/>.
+
+/**
+ * Lists all users with XSS risk
+ *
+ * It would be great to combine this with risk trusts in user table,
+ * unfortunately nobody implemented user trust UI yet :-(
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\access;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\result;
+
+/**
+ * Lists all users with XSS risk
+ *
+ * It would be great to combine this with risk trusts in user table,
+ * unfortunately nobody implemented user trust UI yet :-(
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class riskbackup_result extends \core\check\result {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+        global $DB;
+
+        $syscontext = \context_system::instance();
+
+        $params = array('capability' => 'moodle/backup:userinfo', 'permission' => CAP_ALLOW, 'contextid' => $syscontext->id);
+        $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, r.archetype
+                  FROM {role} r
+                  JOIN {role_capabilities} rc ON rc.roleid = r.id
+                 WHERE rc.capability = :capability
+                   AND rc.contextid  = :contextid
+                   AND rc.permission = :permission";
+        $this->systemroles = $DB->get_records_sql($sql, $params);
+
+        $params = array('capability' => 'moodle/backup:userinfo', 'permission' => CAP_ALLOW, 'contextid' => $syscontext->id);
+        $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, r.archetype, rc.contextid
+                  FROM {role} r
+                  JOIN {role_capabilities} rc ON rc.roleid = r.id
+                 WHERE rc.capability = :capability
+                   AND rc.contextid <> :contextid
+                   AND rc.permission = :permission";
+        $this->overriddenroles = $DB->get_records_sql($sql, $params);
+
+        // List of users that are able to backup personal info
+        // note:
+        // "sc" is context where is role assigned,
+        // "c" is context where is role overridden or system context if in role definition.
+        $params = [
+            'capability' => 'moodle/backup:userinfo',
+            'permission' => CAP_ALLOW,
+            'context1' => CONTEXT_COURSE,
+            'context2' => CONTEXT_COURSE,
+        ];
+
+        $this->sqluserinfo = "
+            FROM (SELECT DISTINCT rcx.contextid,
+                         rcx.roleid
+                    FROM {role_capabilities} rcx
+                   WHERE rcx.permission = :permission
+                     AND rcx.capability = :capability) rc
+            JOIN {context} c ON c.id = rc.contextid
+            JOIN {context} sc ON sc.contextlevel <= :context1
+            JOIN {role_assignments} ra ON ra.contextid = sc.id AND ra.roleid = rc.roleid
+            JOIN {user} u ON u.id = ra.userid AND u.deleted = 0
+           WHERE (sc.path = c.path OR
+                  sc.path LIKE " . $DB->sql_concat('c.path', "'/%'") . " OR
+                   c.path LIKE " . $DB->sql_concat('sc.path', "'/%'") . ")
+             AND c.contextlevel <= :context2";
+
+        $usercount = $DB->count_records_sql("SELECT COUNT('x') FROM (SELECT DISTINCT u.id $this->sqluserinfo) userinfo", $params);
+        $systemrolecount = empty($this->systemroles) ? 0 : count($this->systemroles);
+        $overriddenrolecount = empty($this->overriddenroles) ? 0 : count($this->overriddenroles);
+
+        if (max($usercount, $systemrolecount, $overriddenrolecount) > 0) {
+            $this->status = result::WARNING;
+        } else {
+            $this->status = result::OK;
+        }
+
+        $a = (object)array(
+            'rolecount' => $systemrolecount,
+            'overridecount' => $overriddenrolecount,
+            'usercount' => $usercount,
+        );
+        $this->summary = get_string('check_riskbackup_warning', 'report_security', $a);
+    }
+
+    /**
+     * Showing the full list of roles may be slow so defer it
+     *
+     * @return string
+     */
+    public function get_details(): string {
+
+        global $CFG, $DB;
+
+        $details = '';
+
+        // Make a list of roles.
+        if ($this->systemroles) {
+            $links = array();
+            foreach ($this->systemroles as $role) {
+                $role->name = role_get_name($role);
+                $role->url = (new \moodle_url('/admin/roles/manage.php', ['action' => 'edit', 'roleid' => $role->id]))->out();
+                $links[] = \html_writer::tag('li', get_string('check_riskbackup_editrole', 'report_security', $role));
+            }
+            $links = \html_writer::tag('ul', implode('', $links));
+            $details .= get_string('check_riskbackup_details_systemroles', 'report_security', $links);
+        }
+
+        // Make a list of overrides to roles.
+        $rolelinks2 = array();
+        if ($this->overriddenroles) {
+            $links = array();
+            foreach ($this->overriddenroles as $role) {
+                $role->name = $role->localname;
+                $context = context::instance_by_id($role->contextid);
+                $role->name = role_get_name($role, $context, ROLENAME_BOTH);
+                $role->contextname = $context->get_context_name();
+                $role->url = (new \moodle_url('/admin/roles/override.php',
+                    ['contextid' => $role->contextid, 'roleid' => $role->id]))->out();
+                $links[] = \html_writer::tag('li', get_string('check_riskbackup_editoverride', 'report_security', $role));
+            }
+            $links = \html_writer::tag('ul', implode('', $links));
+            $details .= get_string('check_riskbackup_details_overriddenroles', 'report_security', $links);
+        }
+
+        // Get a list of affected users as well.
+        $users = array();
+
+        list($sort, $sortparams) = users_order_by_sql('u');
+        $params = [
+            'capability' => 'moodle/backup:userinfo',
+            'permission' => CAP_ALLOW,
+            'context1' => CONTEXT_COURSE,
+            'context2' => CONTEXT_COURSE,
+        ];
+        $userfields = \user_picture::fields('u');
+        $rs = $DB->get_recordset_sql("
+            SELECT DISTINCT $userfields,
+                            ra.contextid,
+                            ra.roleid
+                            $this->sqluserinfo
+                   ORDER BY $sort", array_merge($params, $sortparams));
+
+        foreach ($rs as $user) {
+            $context = \context::instance_by_id($user->contextid);
+            $url = new \moodle_url('/admin/roles/assign.php', ['contextid' => $user->contextid, 'roleid' => $user->roleid]);
+            $a = (object)array(
+                'fullname' => fullname($user),
+                'url' => $url->out(),
+                'email' => s($user->email),
+                'contextname' => $context->get_context_name(),
+            );
+            $users[] = \html_writer::tag('li', get_string('check_riskbackup_unassign', 'report_security', $a));
+        }
+        $rs->close();
+        if (!empty($users)) {
+            $users = \html_writer::tag('ul', implode('', $users));
+            $details .= get_string('check_riskbackup_details_users', 'report_security', $users);
+        }
+
+        return $details;
+    }
+}
+
diff --git a/lib/classes/check/access/riskxss.php b/lib/classes/check/access/riskxss.php
new file mode 100644 (file)
index 0000000..3ce984a
--- /dev/null
@@ -0,0 +1,76 @@
+<?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/>.
+
+/**
+ * Lists all users with XSS risk
+ *
+ * It would be great to combine this with risk trusts in user table,
+ * unfortunately nobody implemented user trust UI yet :-(
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\access;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\result;
+
+/**
+ * Lists all users with XSS risk
+ *
+ * It would be great to combine this with risk trusts in user table,
+ * unfortunately nobody implemented user trust UI yet :-(
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class riskxss extends \core\check\check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_riskxss_name', 'report_security');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/roles/manage.php'),
+            get_string('manageroles', 'role'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        return new riskxss_result();
+    }
+}
+
diff --git a/lib/classes/check/access/riskxss_result.php b/lib/classes/check/access/riskxss_result.php
new file mode 100644 (file)
index 0000000..7097db6
--- /dev/null
@@ -0,0 +1,105 @@
+<?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/>.
+
+/**
+ * Lists all users with XSS risk
+ *
+ * It would be great to combine this with risk trusts in user table,
+ * unfortunately nobody implemented user trust UI yet :-(
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\access;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\result;
+
+/**
+ * Lists all users with XSS risk
+ *
+ * It would be great to combine this with risk trusts in user table,
+ * unfortunately nobody implemented user trust UI yet :-(
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class riskxss_result extends \core\check\result {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+
+        global $DB;
+        $this->params = array('capallow' => CAP_ALLOW);
+        $this->sqlfrom = "FROM (SELECT DISTINCT rcx.contextid, rcx.roleid
+                           FROM {role_capabilities} rcx
+                           JOIN {capabilities} cap ON (cap.name = rcx.capability AND
+                                " . $DB->sql_bitand('cap.riskbitmask', RISK_XSS) . " <> 0)
+                           WHERE rcx.permission = :capallow) rc,
+                     {context} c,
+                     {context} sc,
+            {role_assignments} ra,
+                        {user} u
+                         WHERE c.id = rc.contextid
+                           AND (sc.path = c.path OR
+                                sc.path LIKE " . $DB->sql_concat('c.path', "'/%'") . " OR
+                                c.path LIKE " . $DB->sql_concat('sc.path', "'/%'") . ")
+                           AND u.id = ra.userid AND u.deleted = 0
+                           AND ra.contextid = sc.id
+                           AND ra.roleid = rc.roleid";
+
+        $count = $DB->count_records_sql("SELECT COUNT(DISTINCT u.id) $this->sqlfrom", $this->params);
+
+        if ($count == 0) {
+            $this->status = result::OK;
+        } else {
+            $this->status = result::WARNING;
+        }
+
+        $this->summary = get_string('check_riskxss_warning', 'report_security', $count);
+
+    }
+
+    /**
+     * Showing the full list of user may be slow so defer it
+     *
+     * @return string
+     */
+    public function get_details(): string {
+
+        global $CFG, $DB;
+
+        $userfields = \user_picture::fields('u');
+        $users = $DB->get_records_sql("SELECT DISTINCT $userfields $this->sqlfrom", $this->params);
+        foreach ($users as $uid => $user) {
+            $url = "$CFG->wwwroot/user/view.php?id=$user->id";
+            $link = \html_writer::link($url, fullname($user, true) . ' (' . s($user->email) . ')');
+            $users[$uid] = \html_writer::tag('li' , $link);
+        }
+        $users = \html_writer::tag('ul', implode('', $users));
+
+        return get_string('check_riskxss_details', 'report_security', $users);
+    }
+}
+
diff --git a/lib/classes/check/check.php b/lib/classes/check/check.php
new file mode 100644 (file)
index 0000000..8042fe8
--- /dev/null
@@ -0,0 +1,118 @@
+<?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/>.
+
+/**
+ * Base class for checks
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core\check;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Base class for checks
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class check {
+
+    /**
+     * @var $string $component - The component / plugin this task belongs to.
+     *
+     * This is autopopulated by the check manager.
+     */
+    protected $component = 'core';
+
+    /**
+     * Get the frankenstyle component name
+     *
+     * @return string
+     */
+    public function get_component(): string {
+        return $this->component;
+    }
+
+    /**
+     * Get the frankenstyle component name
+     *
+     * @param string $component name
+     */
+    public function set_component(string $component) {
+        $this->component = $component;
+    }
+
+    /**
+     * Get the check's id
+     *
+     * This defaults to the base name of the class which is ok in the most
+     * cases but if you have a check which can have multiple instances then
+     * you should override this to be unique.
+     *
+     * @return string must be unique within a component
+     */
+    public function get_id(): string {
+        $class = get_class($this);
+        $id = explode("\\", $class);
+        return end($id);
+    }
+
+    /**
+     * Get the check reference
+     *
+     * @return string must be globally unique
+     */
+    public function get_ref(): string {
+        $ref = $this->get_component();
+        if (!empty($ref)) {
+            $ref .= '_';
+        }
+        $ref .= $this->get_id();
+        return $ref;
+    }
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        $id = $this->get_id();
+        return get_string("check{$id}", $this->get_component());
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return null;
+    }
+
+    /**
+     * Return the result
+     *
+     * @return result object
+     */
+    abstract public function get_result(): result;
+
+}
+
diff --git a/lib/classes/check/environment/configrw.php b/lib/classes/check/environment/configrw.php
new file mode 100644 (file)
index 0000000..32d4221
--- /dev/null
@@ -0,0 +1,70 @@
+<?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/>.
+
+/**
+ * Verifies config.php is not writable anymore after installation
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\environment;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Verifies config.php is not writable anymore after installation
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class configrw extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_configrw_name', 'report_security');
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $CFG;
+        $details = get_string('check_configrw_details', 'report_security');
+
+        if (is_writable($CFG->dirroot . '/config.php')) {
+            $status = result::WARNING;
+            $summary = get_string('check_configrw_warning', 'report_security');
+        } else {
+            $status = result::OK;
+            $summary = get_string('check_configrw_ok', 'report_security');
+        }
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/environment/displayerrors.php b/lib/classes/check/environment/displayerrors.php
new file mode 100644 (file)
index 0000000..e99c669
--- /dev/null
@@ -0,0 +1,75 @@
+<?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/>.
+
+/**
+ * Verifies displaying of errors
+ *
+ * Problem for lib files and 3rd party code because we can not disable debugging
+ * in these scripts (they do not include config.php)
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\environment;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\result;
+use core\check\check;
+
+/**
+ * Verifies displaying of errors
+ *
+ * Problem for lib files and 3rd party code because we can not disable debugging
+ * in these scripts (they do not include config.php)
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class displayerrors extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_displayerrors_name', 'report_security');
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        $details = get_string('check_displayerrors_details', 'report_security');
+
+        if (defined('WARN_DISPLAY_ERRORS_ENABLED')) {
+            $status = result::WARNING;
+            $summary = get_string('check_displayerrors_error', 'report_security');
+        } else {
+            $status = result::OK;
+            $summary = get_string('check_displayerrors_ok', 'report_security');
+        }
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/environment/nodemodules.php b/lib/classes/check/environment/nodemodules.php
new file mode 100644 (file)
index 0000000..34780d1
--- /dev/null
@@ -0,0 +1,69 @@
+<?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/>.
+
+/**
+ * Check the presence of the node_modules directory.
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\environment;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Check the presence of the node_modules directory.
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class nodemodules extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_nodemodules_name', 'report_security');
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $CFG;
+        $summary = get_string('check_nodemodules_info', 'report_security');
+        $details = get_string('check_nodemodules_details', 'report_security', ['path' => $CFG->dirroot . '/node_modules']);
+
+        if (is_dir($CFG->dirroot . '/node_modules')) {
+            $status = result::WARNING;
+        } else {
+            $status = result::OK;
+        }
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/environment/preventexecpath.php b/lib/classes/check/environment/preventexecpath.php
new file mode 100644 (file)
index 0000000..a6f90cd
--- /dev/null
@@ -0,0 +1,69 @@
+<?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/>.
+
+/**
+ * Verifies the status of preventexecpath
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\environment;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Verifies the status of preventexecpath
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class preventexecpath extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_preventexecpath_name', 'report_security');
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $CFG;
+        $details = get_string('check_preventexecpath_details', 'report_security');
+        if (empty($CFG->preventexecpath)) {
+            $status = result::WARNING;
+            $summary = get_string('check_preventexecpath_warning', 'report_security');
+        } else {
+            $status = result::OK;
+            $summary = get_string('check_preventexecpath_ok', 'report_security');
+        }
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/environment/unsecuredataroot.php b/lib/classes/check/environment/unsecuredataroot.php
new file mode 100644 (file)
index 0000000..c243e27
--- /dev/null
@@ -0,0 +1,80 @@
+<?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/>.
+
+/**
+ * Verifies fatal misconfiguration of dataroot
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\environment;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\result;
+
+/**
+ * Verifies fatal misconfiguration of dataroot
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class unsecuredataroot extends \core\check\check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_unsecuredataroot_name', 'report_security');
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+
+        global $CFG;
+        require_once($CFG->libdir.'/adminlib.php');
+
+        $details = get_string('check_unsecuredataroot_details', 'report_security');
+
+        $insecuredataroot = is_dataroot_insecure(true);
+
+        if ($insecuredataroot == INSECURE_DATAROOT_WARNING) {
+            $status = result::ERROR;
+            $summary = get_string('check_unsecuredataroot_warning', 'report_security', $CFG->dataroot);
+
+        } else if ($insecuredataroot == INSECURE_DATAROOT_ERROR) {
+            $status = result::CRITICAL;
+            $summary = get_string('check_unsecuredataroot_error', 'report_security', $CFG->dataroot);
+
+        } else {
+            $status = result::OK;
+            $summary = get_string('check_unsecuredataroot_ok', 'report_security');
+        }
+        return new result($status, $summary, $details);
+    }
+}
+
+
diff --git a/lib/classes/check/environment/vendordir.php b/lib/classes/check/environment/vendordir.php
new file mode 100644 (file)
index 0000000..55da2bc
--- /dev/null
@@ -0,0 +1,69 @@
+<?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/>.
+
+/**
+ * Check the presence of the vendor directory.
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\environment;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Check the presence of the vendor directory.
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class vendordir extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_vendordir_name', 'report_security');
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $CFG;
+        $details = get_string('check_vendordir_details', 'report_security', ['path' => $CFG->dirroot.'/vendor']);
+        $summary = get_string('check_vendordir_info', 'report_security');
+
+        if (is_dir($CFG->dirroot.'/vendor')) {
+            $status = result::WARNING;
+        } else {
+            $status = result::OK;
+        }
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/http/cookiesecure.php b/lib/classes/check/http/cookiesecure.php
new file mode 100644 (file)
index 0000000..3f403be
--- /dev/null
@@ -0,0 +1,90 @@
+<?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/>.
+
+/**
+ * Verifies if https enabled only secure cookies allowed
+ *
+ * This prevents redirections and sending of cookies to unsecure port.
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\http;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Verifies if https enabled only secure cookies allowed
+ *
+ * This prevents redirections and sending of cookies to unsecure port.
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cookiesecure extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_cookiesecure_name', 'report_security');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/settings.php?section=httpsecurity#admin-cookiesecure'),
+            get_string('httpsecurity', 'admin'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $CFG;
+        $details = get_string('check_cookiesecure_details', 'report_security');
+        if (!is_https()) {
+            $status = result::WARNING;
+            $summary = get_string('check_cookiesecure_http', 'report_security');
+            return new result($status, $summary, $details);
+        }
+
+        if (!is_moodle_cookie_secure()) {
+            $status = result::ERROR;
+            $summary = get_string('check_cookiesecure_error', 'report_security');
+        } else {
+            $status = result::OK;
+            $summary = get_string('check_cookiesecure_ok', 'report_security');
+        }
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/manager.php b/lib/classes/check/manager.php
new file mode 100644 (file)
index 0000000..ee02546
--- /dev/null
@@ -0,0 +1,103 @@
+<?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/>.
+
+/**
+ * Check API manager
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Check API manager
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class manager {
+
+    /**
+     * The list of valid check types
+     */
+    public const TYPES = ['security'];
+
+    /**
+     * Return all status checks
+     *
+     * @param string $type of checks to fetch
+     * @return array of check objects
+     */
+    public static function get_checks(string $type): array {
+        if (!in_array($type, self::TYPES)) {
+            throw new \moodle_exception("Invalid check type '$type'");
+        }
+        $method = 'get_' . $type . '_checks';
+        $checks = self::$method();
+        return $checks;
+    }
+
+    /**
+     * Return all security checks
+     *
+     * @return array of check objects
+     */
+    public static function get_security_checks(): array {
+        $checks = [
+            new environment\displayerrors(),
+            new environment\unsecuredataroot(),
+            new environment\vendordir(),
+            new environment\nodemodules(),
+            new environment\configrw(),
+            new environment\preventexecpath(),
+            new security\mediafilterswf(),
+            new security\embed(),
+            new security\openprofiles(),
+            new security\crawlers(),
+            new security\passwordpolicy(),
+            new security\emailchangeconfirmation(),
+            new security\webcron(),
+            new http\cookiesecure(),
+            new access\riskadmin(),
+            new access\riskxss(),
+            new access\riskbackup(),
+            new access\defaultuserrole(),
+            new access\guestrole(),
+            new access\frontpagerole(),
+        ];
+        // Any plugin can add security checks to this report by implementing a callback
+        // <component>_security_checks() which returns a check object.
+        $morechecks = get_plugins_with_function('security_checks', 'lib.php');
+        foreach ($morechecks as $plugintype => $plugins) {
+            foreach ($plugins as $plugin => $pluginfunction) {
+                $result = $pluginfunction();
+                foreach ($result as $check) {
+                    $check->set_component($plugintype . '_' . $plugin);
+                    $checks[] = $check;
+                }
+            }
+        }
+        return $checks;
+    }
+}
+
diff --git a/lib/classes/check/result.php b/lib/classes/check/result.php
new file mode 100644 (file)
index 0000000..2a0a8dc
--- /dev/null
@@ -0,0 +1,192 @@
+<?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/>.
+
+/**
+ * A check result class
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core\check;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A check object returns a result object
+ *
+ * Most checks can use this an instance of this directly but if you have a
+ * 'details' which is computationally expensive then extend this and overide
+ * the get_details() method so that it is only called when it will be needed.
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class result implements \renderable {
+
+    /**
+     * This is used to notify if a check does not apply.
+     *
+     * In most cases if a check doesn't apply a check object shouldn't be made.
+     * This state exists for when you always want visibilty of the check itself.
+     * Can be useful for a check which depends on another check and it helps
+     * focus on the other check which matters more.
+     */
+    const NA = 'na';
+
+    /**
+     * Ideally all checks should be ok.
+     */
+    const OK = 'ok';
+
+    /**
+     * This is used to show info for a check.
+     *
+     * This is equivalent to OK but could be used for alerting to potential
+     * future warnings such as a deprecation in a service.
+     */
+    const INFO = 'info';
+
+    /**
+     * This means we could not determine the state.
+     *
+     * An example might be an expensive check done via cron, and it has never run.
+     * It would be prudent to consider an unknown check as a warning or error.
+     */
+    const UNKNOWN = 'unknown';
+
+    /**
+     * Warnings
+     *
+     * Something is not ideal and should be addressed, eg usability or the
+     * speed of the site may be affected, but it may self heal (eg a load spike)
+     */
+    const WARNING = 'warning';
+
+    /**
+     * This is used to notify if a check failed.
+     *
+     * Something is wrong with a component and a feature is not working.
+     */
+    const ERROR = 'error';
+
+    /**
+     * This is used to notify if a check is a major critical issue.
+     *
+     * An error which is affecting everyone in a major way.
+     */
+    const CRITICAL = 'critical';
+
+    /**
+     * @var string $state - state
+     */
+    protected $state = self::UNKNOWN;
+
+    /**
+     * @var string summary - should be roughly 1 line of plain text and may change depending on the state.
+     */
+    protected $summary = '';
+
+    /**
+     * @var string details about check.
+     *
+     * This may be a large amount of preformatted html text, possibly describing all the
+     * different states and actions to address them.
+     */
+    protected $details = '';
+
+    /**
+     * Get the check reference label
+     *
+     * @return string must be globally unique
+     */
+    public function get_ref(): string {
+        $ref = $this->get_component();
+        if (!empty($ref)) {
+            $ref .= '_';
+        }
+        $ref .= $this->get_id();
+        return $ref;
+    }
+
+    /**
+     * Constructor
+     *
+     * @param int $status code
+     * @param string $summary a 1 liner summary
+     * @param string $details as a html chunk
+     */
+    public function __construct($status, $summary, $details = '') {
+        $this->status = $status;
+        $this->summary = $summary;
+        $this->details = $details;
+    }
+
+    /**
+     * Get the check status
+     *
+     * @return string one of the consts eg result::OK
+     */
+    public function get_status(): string {
+        return $this->status;
+    }
+
+    /**
+     * Summary of the check
+     * @return string formatted html
+     */
+    public function get_summary(): string {
+        return $this->summary;
+    }
+
+    /**
+     * Get the check detailed info
+     * @return string formatted html
+     */
+    public function get_details(): string {
+        return $this->details;
+    }
+
+    /**
+     * Export this data so it can be used as the context for a mustache template.
+     *
+     * @param renderer_base $output typically, the renderer that's calling this function
+     * @return stdClass data context for a mustache template
+     */
+    public function export_for_template(\renderer_base $output) {
+        return array(
+            'status'        => clean_text(get_string('status' . $this->status)),
+            'isna'          => $this->status === self::NA,
+            'isok'          => $this->status === self::OK,
+            'isinfo'        => $this->status === self::INFO,
+            'isunknown'     => $this->status === self::UNKNOWN,
+            'iswarning'     => $this->status === self::WARNING,
+            'iserror'       => $this->status === self::ERROR,
+            'iscritical'    => $this->status === self::CRITICAL,
+        );
+    }
+
+    /**
+     * Which mustache template?
+     *
+     * @return string path to mustache template
+     */
+    public function get_template_name(): string {
+        return 'core/check/result';
+    }
+}
+
diff --git a/lib/classes/check/security/crawlers.php b/lib/classes/check/security/crawlers.php
new file mode 100644 (file)
index 0000000..e6b0040
--- /dev/null
@@ -0,0 +1,89 @@
+<?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/>.
+
+/**
+ * Verifies web crawler (search engine) access
+ *
+ * Not combined with disabled guest access because attackers might gain guest
+ * access by modifying browser signature.
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\security;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Verifies web crawler (search engine) access
+ *
+ * Not combined with disabled guest access because attackers might gain guest
+ * access by modifying browser signature.
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class crawlers extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_crawlers_name', 'report_security');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/settings.php?section=sitepolicies#admin-opentowebcrawlers'),
+            get_string('sitepolicies', 'admin'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $CFG;
+        $details = get_string('check_crawlers_details', 'report_security');
+        if (empty($CFG->opentowebcrawlers)) {
+            $status = result::OK;
+            $summary = get_string('check_crawlers_ok', 'report_security');
+        } else if (!empty($CFG->guestloginbutton)) {
+            $status = result::INFO;
+            $summary = get_string('check_crawlers_info', 'report_security');
+        } else {
+            $status = result::ERROR;
+            $summary = get_string('check_crawlers_error', 'report_security');
+        }
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/security/emailchangeconfirmation.php b/lib/classes/check/security/emailchangeconfirmation.php
new file mode 100644 (file)
index 0000000..8c4f106
--- /dev/null
@@ -0,0 +1,86 @@
+<?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/>.
+
+/**
+ * Verifies email confirmation - spammers were changing mails very often
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\security;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Verifies email confirmation - spammers were changing mails very often
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class emailchangeconfirmation extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_emailchangeconfirmation_name', 'report_security');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/settings.php?section=sitepolicies#admin-emailchangeconfirmation'),
+            get_string('sitepolicies', 'admin'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+
+        global $CFG;
+        $details = get_string('check_emailchangeconfirmation_details', 'report_security');
+        if (empty($CFG->emailchangeconfirmation)) {
+            if (empty($CFG->allowemailaddresses)) {
+                $status = result::WARNING;
+                $summary = get_string('check_emailchangeconfirmation_error', 'report_security');
+            } else {
+                $status = result::INFO;
+                $summary = get_string('check_emailchangeconfirmation_info', 'report_security');
+            }
+        } else {
+            $status = result::OK;
+            $summary = get_string('check_emailchangeconfirmation_ok', 'report_security');
+        }
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/security/embed.php b/lib/classes/check/security/embed.php
new file mode 100644 (file)
index 0000000..bfdd767
--- /dev/null
@@ -0,0 +1,80 @@
+<?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/>.
+
+/**
+ * Verifies sloppy embedding - this should have been removed long ago!!
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\security;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Verifies sloppy embedding - this should have been removed long ago!!
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class embed extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_embed_name', 'report_security');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/settings.php?section=sitepolicies#admin-allowobjectembed'),
+            get_string('sitepolicies', 'admin'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $CFG;
+        $details = get_string('check_embed_details', 'report_security');
+        if (!empty($CFG->allowobjectembed)) {
+            $status = result::ERROR;
+            $summary = get_string('check_embed_error', 'report_security');
+        } else {
+            $status = result::OK;
+            $summary = get_string('check_embed_ok', 'report_security');
+        }
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/security/mediafilterswf.php b/lib/classes/check/security/mediafilterswf.php
new file mode 100644 (file)
index 0000000..78632b9
--- /dev/null
@@ -0,0 +1,83 @@
+<?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/>.
+
+/**
+ * Verifies sloppy swf embedding - this should have been removed long ago!!
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\security;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Verifies sloppy swf embedding - this should have been removed long ago!!
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mediafilterswf extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_mediafilterswf_name', 'report_security');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/settings.php?section=managemediaplayers'),
+            get_string('managemediaplayers', 'media'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        $details = get_string('check_mediafilterswf_details', 'report_security');
+
+        $activefilters = filter_get_globally_enabled();
+
+        $enabledmediaplayers = \core\plugininfo\media::get_enabled_plugins();
+        if (array_search('mediaplugin', $activefilters) !== false and array_key_exists('swf', $enabledmediaplayers)) {
+            $status = result::CRITICAL;
+            $summary = get_string('check_mediafilterswf_error', 'report_security');
+        } else {
+            $status = result::OK;
+            $summary = get_string('check_mediafilterswf_ok', 'report_security');
+        }
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/security/openprofiles.php b/lib/classes/check/security/openprofiles.php
new file mode 100644 (file)
index 0000000..b6d5faa
--- /dev/null
@@ -0,0 +1,80 @@
+<?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/>.
+
+/**
+ * Verifies open profiles - originally open by default, not anymore because spammer abused it a lot
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\security;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Verifies open profiles - originally open by default, not anymore because spammer abused it a lot
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class openprofiles extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_openprofiles_name', 'report_security');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/settings.php?section=sitepolicies#admin-forcelogin'),
+            get_string('sitepolicies', 'admin'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $CFG;
+        $details = get_string('check_openprofiles_details', 'report_security');
+        if (empty($CFG->forcelogin) and empty($CFG->forceloginforprofiles)) {
+            $status = result::WARNING;
+            $summary = get_string('check_openprofiles_error', 'report_security');
+        } else {
+            $status = result::OK;
+            $summary = get_string('check_openprofiles_ok', 'report_security');
+        }
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/security/passwordpolicy.php b/lib/classes/check/security/passwordpolicy.php
new file mode 100644 (file)
index 0000000..cc5d1b1
--- /dev/null
@@ -0,0 +1,80 @@
+<?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/>.
+
+/**
+ * Verifies if password policy set
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\security;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Verifies if password policy set
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class passwordpolicy extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_passwordpolicy_name', 'report_security');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/settings.php?section=sitepolicies#admin-passwordpolicy'),
+            get_string('sitepolicies', 'admin'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $CFG;
+        $details = get_string('check_passwordpolicy_details', 'report_security');
+        if (empty($CFG->passwordpolicy)) {
+            $status = result::WARNING;
+            $summary = get_string('check_passwordpolicy_error', 'report_security');
+        } else {
+            $status = result::OK;
+            $summary = get_string('check_passwordpolicy_ok', 'report_security');
+        }
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/security/webcron.php b/lib/classes/check/security/webcron.php
new file mode 100644 (file)
index 0000000..f1c6f17
--- /dev/null
@@ -0,0 +1,84 @@
+<?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/>.
+
+/**
+ * Verifies the status of web cron
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\security;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Verifies the status of web cron
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class webcron extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_webcron_name', 'report_security');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+           new \moodle_url('/admin/settings.php?section=sitepolicies#admin-cronclionly'),
+           get_string('sitepolicies', 'admin'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+
+        global $CFG;
+        $croncli = $CFG->cronclionly;
+        $cronremotepassword = $CFG->cronremotepassword;
+
+        if (empty($croncli) && empty($cronremotepassword)) {
+            $status = result::WARNING;
+            $summary = get_string('check_webcron_warning', 'report_security');
+        } else {
+            $status = result::OK;
+            $summary = get_string('check_webcron_ok', 'report_security');
+        }
+        $details = get_string('check_webcron_details', 'report_security');
+        return new result($status, $summary, $details);
+    }
+}
+
index c7f5033..b7fd89c 100644 (file)
@@ -40,9 +40,11 @@ class manager {
      * Given a component name, will load the list of tasks in the db/tasks.php file for that component.
      *
      * @param string $componentname - The name of the component to fetch the tasks for.
+     * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
+     *      If false, they are left as 'R'
      * @return \core\task\scheduled_task[] - List of scheduled tasks for this component.
      */
-    public static function load_default_scheduled_tasks_for_component($componentname) {
+    public static function load_default_scheduled_tasks_for_component($componentname, $expandr = true) {
         $dir = \core_component::get_component_directory($componentname);
 
         if (!$dir) {
@@ -65,7 +67,7 @@ class manager {
 
         foreach ($tasks as $task) {
             $record = (object) $task;
-            $scheduledtask = self::scheduled_task_from_record($record);
+            $scheduledtask = self::scheduled_task_from_record($record, $expandr);
             // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
             if ($scheduledtask) {
                 $scheduledtask->set_component($componentname);
@@ -318,9 +320,11 @@ class manager {
      * Utility method to create a task from a DB record.
      *
      * @param \stdClass $record
-     * @return \core\task\scheduled_task
+     * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
+     *      If false, they are left as 'R'
+     * @return \core\task\scheduled_task|false
      */
-    public static function scheduled_task_from_record($record) {
+    public static function scheduled_task_from_record($record, $expandr = true) {
         $classname = self::get_canonical_class_name($record->classname);
         if (!class_exists($classname)) {
             debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
@@ -342,10 +346,10 @@ class manager {
         }
         $task->set_blocking(!empty($record->blocking));
         if (isset($record->minute)) {
-            $task->set_minute($record->minute);
+            $task->set_minute($record->minute, $expandr);
         }
         if (isset($record->hour)) {
-            $task->set_hour($record->hour);
+            $task->set_hour($record->hour, $expandr);
         }
         if (isset($record->day)) {
             $task->set_day($record->day);
@@ -354,7 +358,7 @@ class manager {
             $task->set_month($record->month);
         }
         if (isset($record->dayofweek)) {
-            $task->set_day_of_week($record->dayofweek);
+            $task->set_day_of_week($record->dayofweek, $expandr);
         }
         if (isset($record->faildelay)) {
             $task->set_fail_delay($record->faildelay);
@@ -429,15 +433,18 @@ class manager {
      * This function load the default scheduled task details for a given classname.
      *
      * @param string $classname
-     * @return \core\task\scheduled_task or false
+     * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
+     *      If false, they are left as 'R'
+     * @return \core\task\scheduled_task|false
      */
-    public static function get_default_scheduled_task($classname) {
+    public static function get_default_scheduled_task($classname, $expandr = true) {
         $task = self::get_scheduled_task($classname);
         $componenttasks = array();
 
         // Safety check in case no task was found for the given classname.
         if ($task) {
-            $componenttasks = self::load_default_scheduled_tasks_for_component($task->get_component());
+            $componenttasks = self::load_default_scheduled_tasks_for_component(
+                    $task->get_component(), $expandr);
         }
 
         foreach ($componenttasks as $componenttask) {
index 42500ab..492e4c5 100644 (file)
@@ -106,9 +106,11 @@ abstract class scheduled_task extends task_base {
      * Setter for $minute. Accepts a special 'R' value
      * which will be translated to a random minute.
      * @param string $minute
+     * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
+     *      If false, they are left as 'R'
      */
-    public function set_minute($minute) {
-        if ($minute === 'R') {
+    public function set_minute($minute, $expandr = true) {
+        if ($minute === 'R' && $expandr) {
             $minute = mt_rand(self::HOURMIN, self::HOURMAX);
         }
         $this->minute = $minute;
@@ -126,9 +128,11 @@ abstract class scheduled_task extends task_base {
      * Setter for $hour. Accepts a special 'R' value
      * which will be translated to a random hour.
      * @param string $hour
+     * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
+     *      If false, they are left as 'R'
      */
-    public function set_hour($hour) {
-        if ($hour === 'R') {
+    public function set_hour($hour, $expandr = true) {
+        if ($hour === 'R' && $expandr) {
             $hour = mt_rand(self::HOURMIN, self::HOURMAX);
         }
         $this->hour = $hour;
@@ -177,9 +181,11 @@ abstract class scheduled_task extends task_base {
     /**
      * Setter for $dayofweek.
      * @param string $dayofweek
+     * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
+     *      If false, they are left as 'R'
      */
-    public function set_day_of_week($dayofweek) {
-        if ($dayofweek === 'R') {
+    public function set_day_of_week($dayofweek, $expandr = true) {
+        if ($dayofweek === 'R' && $expandr) {
             $dayofweek = mt_rand(self::DAYOFWEEKMIN, self::DAYOFWEEKMAX);
         }
         $this->dayofweek = $dayofweek;
index db3abeb..2258259 100644 (file)
@@ -498,7 +498,16 @@ class completion_info {
      */
     public function clear_criteria() {
         global $DB;
-        $DB->delete_records('course_completion_criteria', array('course' => $this->course_id));
+
+        // Remove completion criteria records for the course itself, and any records that refer to the course.
+        $select = 'course = :course OR (criteriatype = :type AND courseinstance = :courseinstance)';
+        $params = [
+            'course' => $this->course_id,
+            'type' => COMPLETION_CRITERIA_TYPE_COURSE,
+            'courseinstance' => $this->course_id,
+        ];
+
+        $DB->delete_records_select('course_completion_criteria', $select, $params);
         $DB->delete_records('course_completion_aggr_methd', array('course' => $this->course_id));
 
         $this->delete_course_completion_data();
index 6ce9568..dc266b3 100644 (file)
@@ -2212,5 +2212,51 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2020013000.01);
     }
 
+    if ($oldversion < 2020040200.01) {
+        // Clean up completion criteria records referring to courses that no longer exist.
+        $select = 'criteriatype = :type AND courseinstance NOT IN (SELECT id FROM {course})';
+        $params = ['type' => 8]; // COMPLETION_CRITERIA_TYPE_COURSE.
+
+        $DB->delete_records_select('course_completion_criteria', $select, $params);
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2020040200.01);
+    }
+
+    if ($oldversion < 2020040700.00) {
+        // Remove deprecated Mozilla OpenBadges backpack.
+        $url = 'https://backpack.openbadges.org';
+        $bp = $DB->get_record('badge_external_backpack', ['backpackapiurl' => $url]);
+        if ($bp) {
+            // Remove connections for users to this backpack.
+            $sql = "SELECT DISTINCT bb.id
+                      FROM {badge_backpack} bb
+                 LEFT JOIN {badge_external} be ON be. backpackid = bb.externalbackpackid
+                     WHERE bb.externalbackpackid = :backpackid";
+            $params = ['backpackid' => $bp->id];
+            $externalbackpacks = $DB->get_fieldset_sql($sql, $params);
+            if ($externalbackpacks) {
+                list($sql, $params) = $DB->get_in_or_equal($externalbackpacks);
+
+                // Delete user external collections references to this backpack.
+                $DB->execute("DELETE FROM {badge_external} WHERE backpackid " . $sql, $params);
+            }
+            $DB->delete_records('badge_backpack', ['externalbackpackid' => $bp->id]);
+
+            // Delete deprecated backpack entry.
+            $DB->delete_records('badge_external_backpack', ['backpackapiurl' => $url]);
+        }
+
+        // Set active external backpack to Badgr.io.
+        $url = 'https://api.badgr.io/v2';
+        if ($bp = $DB->get_record('badge_external_backpack', ['backpackapiurl' => $url])) {
+            set_config('badges_site_backpack', $bp->id);
+        } else {
+            unset_config('badges_site_backpack');
+        }
+
+        upgrade_main_savepoint(true, 2020040700.00);
+    }
+
     return true;
 }
diff --git a/lib/mdn-polyfills/readme_moodle.txt b/lib/mdn-polyfills/readme_moodle.txt
deleted file mode 100644 (file)
index 2c0983e..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-The steps are essentially:
-1) Install mdn-polyfills package
-
-    npm install --no-save  mdn-polyfills
-
-2) Join them all together:
-
-    cd node_modules/mdn-polyfills
-    cat CustomEvent.* Element.* Function.* HTMLCanvasElement.* MouseEvent.* Node.prototype.* NodeList.* > ../../lib/mdn-polyfills/polyfill.js
-
-3) Uninstall the package again
-
-    npm uninstall --no-save mdn-polyfills
index 2a894ee..65a99c4 100644 (file)
@@ -391,12 +391,11 @@ class renderer_base {
      * @return bool
      */
     public function should_display_main_logo($headinglevel = 1) {
-        global $PAGE;
 
         // Only render the logo if we're on the front page or login page and the we have a logo.
         $logo = $this->get_logo_url();
         if ($headinglevel == 1 && !empty($logo)) {
-            if ($PAGE->pagelayout == 'frontpage' || $PAGE->pagelayout == 'login') {
+            if ($this->page->pagelayout == 'frontpage' || $this->page->pagelayout == 'login') {
                 return true;
             }
         }
@@ -632,7 +631,7 @@ class core_renderer extends renderer_base {
      * @return string HTML fragment.
      */
     public function standard_head_html() {
-        global $CFG, $SESSION, $SITE, $PAGE;
+        global $CFG, $SESSION, $SITE;
 
         // Before we output any content, we need to ensure that certain
         // page components are set up.
@@ -728,7 +727,7 @@ class core_renderer extends renderer_base {
             $output .= "\n".$CFG->additionalhtmlhead;
         }
 
-        if ($PAGE->pagelayout == 'frontpage') {
+        if ($this->page->pagelayout == 'frontpage') {
             $summary = s(strip_tags(format_text($SITE->summary, FORMAT_HTML)));
             if (!empty($summary)) {
                 $output .= "<meta name=\"description\" content=\"$summary\" />\n";
@@ -867,7 +866,8 @@ class core_renderer extends renderer_base {
                     html_writer::link($purgeurl, get_string('purgecaches', 'admin')) . '</div>';
         }
         if (!empty($CFG->debugvalidators)) {
-            // NOTE: this is not a nice hack, $PAGE->url is not always accurate and $FULLME neither, it is not a bug if it fails. --skodak
+            // NOTE: this is not a nice hack, $this->page->url is not always accurate and
+            // $FULLME neither, it is not a bug if it fails. --skodak.
             $output .= '<div class="validators"><ul class="list-unstyled ml-1">
               <li><a href="http://validator.w3.org/check?verbose=1&amp;ss=1&amp;uri=' . urlencode(qualified_me()) . '">Validate HTML</a></li>
               <li><a href="http://www.contentquality.com/mynewtester/cynthia.exe?rptmode=-1&amp;url1=' . urlencode(qualified_me()) . '">Section 508 Check</a></li>
@@ -1252,13 +1252,7 @@ class core_renderer extends renderer_base {
      * Start output by sending the HTTP headers, and printing the HTML <head>
      * and the start of the <body>.
      *
-     * To control what is printed, you should set properties on $PAGE. If you
-     * are familiar with the old {@link print_header()} function from Moodle 1.9
-     * you will find that there are properties on $PAGE that correspond to most
-     * of the old parameters to could be passed to print_header.
-     *
-     * Not that, in due course, the remaining $navigation, $menu parameters here
-     * will be replaced by more properties of $PAGE, but that is still to do.
+     * To control what is printed, you should set properties on $PAGE.
      *
      * @return string HTML that you must output this, preferably immediately.
      */
@@ -1383,7 +1377,7 @@ class core_renderer extends renderer_base {
      * @return string HTML fragment
      */
     public function footer() {
-        global $CFG, $DB, $PAGE;
+        global $CFG, $DB;
 
         // Give plugins an opportunity to touch the page before JS is finalized.
         $pluginswithfunction = get_plugins_with_function('before_footer', 'lib.php');
@@ -1416,10 +1410,10 @@ class core_renderer extends renderer_base {
         }
         $footer = str_replace($this->unique_performance_info_token, $performanceinfo, $footer);
 
-        // Only show notifications when we have a $PAGE context id.
-        if (!empty($PAGE->context->id)) {
+        // Only show notifications when the current page has a context id.
+        if (!empty($this->page->context->id)) {
             $this->page->requires->js_call_amd('core/notification', 'init', array(
-                $PAGE->context->id,
+                $this->page->context->id,
                 \core\notification::fetch_as_array($this)
             ));
         }
@@ -1648,8 +1642,6 @@ class core_renderer extends renderer_base {
      * @return string the HTML to display
      */
     public function print_textarea($name, $id, $value, $rows, $cols) {
-        global $OUTPUT;
-
         editors_head_setup();
         $editor = editors_get_preferred_editor(FORMAT_HTML);
         $editor->set_text($value);
@@ -1663,7 +1655,7 @@ class core_renderer extends renderer_base {
             'cols' => $cols
         ];
 
-        return $OUTPUT->render_from_template('core_form/editor_textarea', $context);
+        return $this->render_from_template('core_form/editor_textarea', $context);
     }
 
     /**
@@ -1689,6 +1681,26 @@ class core_renderer extends renderer_base {
         return $this->render_from_template('core/action_menu', $context);
     }
 
+    /**
+     * Renders a Check API result
+     *
+     * @param result $result
+     * @return string HTML fragment
+     */
+    protected function render_check_result(core\check\result $result) {
+        return $this->render_from_template($result->get_template_name(), $result->export_for_template($this));
+    }
+
+    /**
+     * Renders a Check API result
+     *
+     * @param result $result
+     * @return string HTML fragment
+     */
+    public function check_result(core\check\result $result) {
+        return $this->render_check_result($result);
+    }
+
     /**
      * Renders an action_menu_link item.
      *
@@ -2614,7 +2626,7 @@ class core_renderer extends renderer_base {
      *       client_id=>uniqid(),
      *       acepted_types=>'*',
      *       return_types=>FILE_INTERNAL,
-     *       context=>$PAGE->context
+     *       context=>current page context
      * @return string HTML fragment
      */
     public function file_picker($options) {
@@ -2629,7 +2641,6 @@ class core_renderer extends renderer_base {
      * @return string
      */
     public function render_file_picker(file_picker $fp) {
-        global $CFG, $OUTPUT, $USER;
         $options = $fp->options;
         $client_id = $options->client_id;
         $strsaved = get_string('filesaved', 'repository');
@@ -2637,7 +2648,7 @@ class core_renderer extends renderer_base {
         $strloading  = get_string('loading', 'repository');
         $strdndenabled = get_string('dndenabled_inbox', 'moodle');
         $strdroptoupload = get_string('droptoupload', 'moodle');
-        $icon_progress = $OUTPUT->pix_icon('i/loading_small', $strloading).'';
+        $iconprogress = $this->pix_icon('i/loading_small', $strloading).'';
 
         $currentfile = $options->currentfile;
         if (empty($currentfile)) {
@@ -2662,7 +2673,7 @@ class core_renderer extends renderer_base {
         }
         $html = <<<EOD
 <div class="filemanager-loading mdl-align" id='filepicker-loading-{$client_id}'>
-$icon_progress
+$iconprogress
 </div>
 <div id="filepicker-wrapper-{$client_id}" class="mdl-left w-100" style="display:none">
     <div>
@@ -4259,13 +4270,13 @@ EOD;
      * @return string HTML to display the main header.
      */
     public function full_header() {
-        global $PAGE;
 
-        if ($PAGE->include_region_main_settings_in_header_actions() && !$PAGE->blocks->is_block_present('settings')) {
+        if ($this->page->include_region_main_settings_in_header_actions() &&
+                !$this->page->blocks->is_block_present('settings')) {
             // Only include the region main settings if the page has requested it and it doesn't already have
             // the settings block on it. The region main settings are included in the settings block and
             // duplicating the content causes behat failures.
-            $PAGE->add_header_action(html_writer::div(
+            $this->page->add_header_action(html_writer::div(
                 $this->region_main_settings_menu(),
                 'd-print-none',
                 ['id' => 'region-main-settings-menu']
@@ -4275,11 +4286,11 @@ EOD;
         $header = new stdClass();
         $header->settingsmenu = $this->context_header_settings_menu();
         $header->contextheader = $this->context_header();
-        $header->hasnavbar = empty($PAGE->layout_options['nonavbar']);
+        $header->hasnavbar = empty($this->page->layout_options['nonavbar']);
         $header->navbar = $this->navbar();
         $header->pageheadingbutton = $this->page_heading_button();
         $header->courseheader = $this->course_header();
-        $header->headeractions = $PAGE->get_header_actions();
+        $header->headeractions = $this->page->get_header_actions();
         return $this->render_from_template('core/full_header', $header);
     }
 
@@ -4727,7 +4738,6 @@ EOD;
      * @return string HTML fragment
      */
     public function render_progress_bar(progress_bar $bar) {
-        global $PAGE;
         $data = $bar->export_for_template($this);
         return $this->render_from_template('core/progress_bar', $data);
     }
index 82f84d7..961fda1 100644 (file)
@@ -1608,8 +1608,8 @@ class page_requirements_manager {
             $output .= html_writer::script('', $this->js_fix_url('/lib/babel-polyfill/polyfill.min.js'));
         }
 
-        // Include the MDN Polyfill.
-        $output .= html_writer::script('', $this->js_fix_url('/lib/mdn-polyfills/polyfill.js'));
+        // Include the Polyfills.
+        $output .= html_writer::script('', $this->js_fix_url('/lib/polyfills/polyfill.js'));
 
         // YUI3 JS needs to be loaded early in the body. It should be cached well by the browser.
         $output .= $this->get_yui3lib_headcode();
similarity index 64%
rename from lib/mdn-polyfills/polyfill.js
rename to lib/polyfills/polyfill.js
index c74b588..6b478b7 100644 (file)
@@ -16,3 +16,4 @@ HTMLCanvasElement.prototype.toBlob||(HTMLCanvasElement.prototype.toBlob=function
 !function(){function t(){null!==this.parentNode&&this.parentNode.removeChild(this)}[Element.prototype,CharacterData.prototype,DocumentType.prototype].forEach(function(e){e.hasOwnProperty("remove")||Object.defineProperty(e,"remove",{configurable:!0,enumerable:!0,writable:!0,value:t})})}();
 !function(){var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};function t(){var e,t=this.parentNode,o=arguments.length;if(t)for(o||t.removeChild(this);o--;)"object"!==(void 0===(e=arguments[o])?"undefined":r(e))?e=this.ownerDocument.createTextNode(e):e.parentNode&&e.parentNode.removeChild(e),o?t.insertBefore(this.previousSibling,e):t.replaceChild(e,this)}[Element.prototype,CharacterData.prototype,DocumentType.prototype].forEach(function(e){e.hasOwnProperty("replaceWith")||Object.defineProperty(e,"replaceWith",{configurable:!0,enumerable:!0,writable:!0,value:t})})}();
 window.NodeList&&!NodeList.prototype.forEach&&(NodeList.prototype.forEach=function(o,t){t=t||window;for(var i=0;i<this.length;i++)o.call(t,this[i],i,this)});
+(function(t){var e=function(){try{return!!Symbol.iterator}catch(e){return false}};var r=e();var n=function(t){var e={next:function(){var e=t.shift();return{done:e===void 0,value:e}}};if(r){e[Symbol.iterator]=function(){return e}}return e};var i=function(e){return encodeURIComponent(e).replace(/%20/g,"+")};var o=function(e){return decodeURIComponent(String(e).replace(/\+/g," "))};var a=function(){var a=function(e){Object.defineProperty(this,"_entries",{writable:true,value:{}});var t=typeof e;if(t==="undefined"){}else if(t==="string"){if(e!==""){this._fromString(e)}}else if(e instanceof a){var r=this;e.forEach(function(e,t){r.append(t,e)})}else if(e!==null&&t==="object"){if(Object.prototype.toString.call(e)==="[object Array]"){for(var n=0;n<e.length;n++){var i=e[n];if(Object.prototype.toString.call(i)==="[object Array]"||i.length!==2){this.append(i[0],i[1])}else{throw new TypeError("Expected [string, any] as entry at index "+n+" of URLSearchParams's input")}}}else{for(var o in e){if(e.hasOwnProperty(o)){this.append(o,e[o])}}}}else{throw new TypeError("Unsupported input's type for URLSearchParams")}};var e=a.prototype;e.append=function(e,t){if(e in this._entries){this._entries[e].push(String(t))}else{this._entries[e]=[String(t)]}};e.delete=function(e){delete this._entries[e]};e.get=function(e){return e in this._entries?this._entries[e][0]:null};e.getAll=function(e){return e in this._entries?this._entries[e].slice(0):[]};e.has=function(e){return e in this._entries};e.set=function(e,t){this._entries[e]=[String(t)]};e.forEach=function(e,t){var r;for(var n in this._entries){if(this._entries.hasOwnProperty(n)){r=this._entries[n];for(var i=0;i<r.length;i++){e.call(t,r[i],n,this)}}}};e.keys=function(){var r=[];this.forEach(function(e,t){r.push(t)});return n(r)};e.values=function(){var t=[];this.forEach(function(e){t.push(e)});return n(t)};e.entries=function(){var r=[];this.forEach(function(e,t){r.push([t,e])});return n(r)};if(r){e[Symbol.iterator]=e.entries}e.toString=function(){var r=[];this.forEach(function(e,t){r.push(i(t)+"="+i(e))});return r.join("&")};t.URLSearchParams=a};var s=function(){try{var e=t.URLSearchParams;return new e("?a=1").toString()==="a=1"&&typeof e.prototype.set==="function"}catch(e){return false}};if(!s()){a()}var f=t.URLSearchParams.prototype;if(typeof f.sort!=="function"){f.sort=function(){var r=this;var n=[];this.forEach(function(e,t){n.push([t,e]);if(!r._entries){r.delete(t)}});n.sort(function(e,t){if(e[0]<t[0]){return-1}else if(e[0]>t[0]){return+1}else{return 0}});if(r._entries){r._entries={}}for(var e=0;e<n.length;e++){this.append(n[e][0],n[e][1])}}}if(typeof f._fromString!=="function"){Object.defineProperty(f,"_fromString",{enumerable:false,configurable:false,writable:false,value:function(e){if(this._entries){this._entries={}}else{var r=[];this.forEach(function(e,t){r.push(t)});for(var t=0;t<r.length;t++){this.delete(r[t])}}e=e.replace(/^\?/,"");var n=e.split("&");var i;for(var t=0;t<n.length;t++){i=n[t].split("=");this.append(o(i[0]),i.length>1?o(i[1]):"")}}})}})(typeof global!=="undefined"?global:typeof window!=="undefined"?window:typeof self!=="undefined"?self:this);(function(h){var e=function(){try{var e=new h.URL("b","http://a");e.pathname="c d";return e.href==="http://a/c%20d"&&e.searchParams}catch(e){return false}};var t=function(){var t=h.URL;var e=function(e,t){if(typeof e!=="string")e=String(e);var r=document,n;if(t&&(h.location===void 0||t!==h.location.href)){r=document.implementation.createHTMLDocument("");n=r.createElement("base");n.href=t;r.head.appendChild(n);try{if(n.href.indexOf(t)!==0)throw new Error(n.href)}catch(e){throw new Error("URL unable to set base "+t+" due to "+e)}}var i=r.createElement("a");i.href=e;if(n){r.body.appendChild(i);i.href=i.href}if(i.protocol===":"||!/:/.test(i.href)){throw new TypeError("Invalid URL")}Object.defineProperty(this,"_anchorElement",{value:i});var o=new h.URLSearchParams(this.search);var a=true;var s=true;var f=this;["append","delete","set"].forEach(function(e){var t=o[e];o[e]=function(){t.apply(o,arguments);if(a){s=false;f.search=o.toString();s=true}}});Object.defineProperty(this,"searchParams",{value:o,enumerable:true});var c=void 0;Object.defineProperty(this,"_updateSearchParams",{enumerable:false,configurable:false,writable:false,value:function(){if(this.search!==c){c=this.search;if(s){a=false;this.searchParams._fromString(this.search);a=true}}}})};var r=e.prototype;var n=function(t){Object.defineProperty(r,t,{get:function(){return this._anchorElement[t]},set:function(e){this._anchorElement[t]=e},enumerable:true})};["hash","host","hostname","port","protocol"].forEach(function(e){n(e)});Object.defineProperty(r,"search",{get:function(){return this._anchorElement["search"]},set:function(e){this._anchorElement["search"]=e;this._updateSearchParams()},enumerable:true});Object.defineProperties(r,{toString:{get:function(){var e=this;return function(){return e.href}}},href:{get:function(){return this._anchorElement.href.replace(/\?$/,"")},set:function(e){this._anchorElement.href=e;this._updateSearchParams()},enumerable:true},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\/?)/,"/")},set:function(e){this._anchorElement.pathname=e},enumerable:true},origin:{get:function(){var e={"http:":80,"https:":443,"ftp:":21}[this._anchorElement.protocol];var t=this._anchorElement.port!=e&&this._anchorElement.port!=="";return this._anchorElement.protocol+"//"+this._anchorElement.hostname+(t?":"+this._anchorElement.port:"")},enumerable:true},password:{get:function(){return""},set:function(e){},enumerable:true},username:{get:function(){return""},set:function(e){},enumerable:true}});e.createObjectURL=function(e){return t.createObjectURL.apply(t,arguments)};e.revokeObjectURL=function(e){return t.revokeObjectURL.apply(t,arguments)};h.URL=e};if(!e()){t()}if(h.location!==void 0&&!("origin"in h.location)){var r=function(){return h.location.protocol+"//"+h.location.hostname+(h.location.port?":"+h.location.port:"")};try{Object.defineProperty(h.location,"origin",{get:r,enumerable:true})}catch(e){setInterval(function(){h.location.origin=r()},100)}}})(typeof global!=="undefined"?global:typeof window!=="undefined"?window:typeof self!=="undefined"?self:this);
diff --git a/lib/polyfills/readme_moodle.txt b/lib/polyfills/readme_moodle.txt
new file mode 100644 (file)
index 0000000..a60fd04
--- /dev/null
@@ -0,0 +1,15 @@
+The steps are essentially:
+1) Install mdn-polyfills and url-polyfill packages
+
+    npm install --no-save mdn-polyfills url-polyfill
+
+2) Join them all together:
+
+    cd node_modules/mdn-polyfills
+    cat CustomEvent.* Element.* Function.* HTMLCanvasElement.* MouseEvent.* Node.prototype.* NodeList.* > ../../lib/polyfills/polyfill.js
+    cd ../url-polyfill/
+    cat url-polyfill.min.js >> ../../lib/polyfills/polyfill.js
+
+3) Uninstall the packages again
+
+    npm uninstall --no-save mdn-polyfills url-polyfill
index d00b78f..2a30ca5 100644 (file)
Binary files a/lib/table/amd/build/dynamic.min.js and b/lib/table/amd/build/dynamic.min.js differ
index 2d81312..a6d77b2 100644 (file)
Binary files a/lib/table/amd/build/dynamic.min.js.map and b/lib/table/amd/build/dynamic.min.js.map differ
index d07cd46..849507d 100644 (file)
Binary files a/lib/table/amd/build/local/dynamic/repository.min.js and b/lib/table/amd/build/local/dynamic/repository.min.js differ
index 7629807..88acdf0 100644 (file)
Binary files a/lib/table/amd/build/local/dynamic/repository.min.js.map and b/lib/table/amd/build/local/dynamic/repository.min.js.map differ
index e45e5ff..7e047e0 100644 (file)
Binary files a/lib/table/amd/build/local/dynamic/selectors.min.js and b/lib/table/amd/build/local/dynamic/selectors.min.js differ
index 6af6cf1..525cf30 100644 (file)
Binary files a/lib/table/amd/build/local/dynamic/selectors.min.js.map and b/lib/table/amd/build/local/dynamic/selectors.min.js.map differ
index d3b9d06..4cddc4d 100644 (file)
@@ -38,7 +38,7 @@ const checkTableIsDynamic = tableRoot => {
         throw new Error("The table specified is not a dynamic table and cannot be updated");
     }
 
-    if (!tableRoot.matches(Selectors.table.region)) {
+    if (!tableRoot.matches(Selectors.main.region)) {
         // The table is not a dynamic table.
         throw new Error("The table specified is not a dynamic table and cannot be updated");
     }
@@ -73,6 +73,8 @@ export const refreshTableContent = tableRoot => {
             sortOrder: tableRoot.dataset.tableSortOrder,
             joinType: filterset.jointype,
             filters: filterset.filters,
+            firstinitial: tableRoot.dataset.tableFirstInitial,
+            lastinitial: tableRoot.dataset.tableLastInitial,
         }
     )
     .then(data => {
@@ -88,6 +90,8 @@ export const updateTable = (tableRoot, {
     sortBy = null,
     sortOrder = null,
     filters = null,
+    firstInitial = null,
+    lastInitial = null,
 } = {}, refreshContent = true) => {
     checkTableIsDynamic(tableRoot);
 
@@ -97,6 +101,15 @@ export const updateTable = (tableRoot, {
         tableRoot.dataset.tableSortOrder = sortOrder;
     }
 
+    // Update initials.
+    if (firstInitial !== null) {
+        tableRoot.dataset.tableFirstInitial = firstInitial;
+    }
+
+    if (lastInitial !== null) {
+        tableRoot.dataset.tableLastInitial = lastInitial;
+    }
+
     // Update filters.
     if (filters) {
         tableRoot.dataset.tableFilters = JSON.stringify(filters);
@@ -133,6 +146,28 @@ export const setFilters = (tableRoot, filters, refreshContent = true) =>
 export const setSortOrder = (tableRoot, sortBy, sortOrder, refreshContent = true) =>
     updateTable(tableRoot, {sortBy, sortOrder}, refreshContent);
 
+/**
+ * Update the first initial to show.
+ *
+ * @param {HTMLElement} tableRoot
+ * @param {String} firstInitial
+ * @param {Bool} refreshContent
+ * @returns {Promise}
+ */
+export const setFirstInitial = (tableRoot, firstInitial, refreshContent = true) =>
+    updateTable(tableRoot, {firstInitial}, refreshContent);
+
+/**
+ * Update the last initial to show.
+ *
+ * @param {HTMLElement} tableRoot
+ * @param {String} lastInitial
+ * @param {Bool} refreshContent
+ * @returns {Promise}
+ */
+export const setLastInitial = (tableRoot, lastInitial, refreshContent = true) =>
+    updateTable(tableRoot, {lastInitial}, refreshContent);
+
 /**
  * Set up listeners to handle table updates.
  */
@@ -144,7 +179,7 @@ export const init = () => {
     watching = true;
 
     document.addEventListener('click', e => {
-        const tableRoot = e.target.closest(Selectors.table.region);
+        const tableRoot = e.target.closest(Selectors.main.region);
 
         if (!tableRoot) {
             return;
@@ -156,5 +191,19 @@ export const init = () => {
 
             setSortOrder(tableRoot, sortableLink.dataset.sortby, sortableLink.dataset.sortorder);
         }
+
+        const firstInitialLink = e.target.closest(Selectors.initialsBar.links.firstInitial);
+        if (firstInitialLink !== null) {
+            e.preventDefault();
+
+            setFirstInitial(tableRoot, firstInitialLink.dataset.initial);
+        }
+
+        const lastInitialLink = e.target.closest(Selectors.initialsBar.links.lastInitial);
+        if (lastInitialLink !== null) {
+            e.preventDefault();
+
+            setLastInitial(tableRoot, lastInitialLink.dataset.initial);
+        }
     });
 };
index 4fe7219..6743775 100644 (file)
@@ -30,6 +30,9 @@ import {call as fetchMany} from 'core/ajax';
  * @method fetch
  * @param {String} handler The name of the handler
  * @param {String} uniqueid The unique id of the table
+ * @param {Object} filters The filters to apply when searching
+ * @param {String} firstinitial The first name initial to filter on
+ * @param {String} lastinitial The last name initial to filter on
  * @param {Number} params parameters to request table
  * @return {Promise} Resolved with requested table view
  */
@@ -37,7 +40,9 @@ export const fetch = (handler, uniqueid, {
         sortBy = null,
         sortOrder = null,
         joinType = null,
-        filters = {}
+        filters = {},
+        firstinitial = null,
+        lastinitial = null,
     } = {}
 ) => {
     return fetchMany([{
@@ -49,6 +54,8 @@ export const fetch = (handler, uniqueid, {
             sortorder: sortOrder,
             jointype: joinType,
             filters,
+            firstinitial,
+            lastinitial,
         },
     }])[0];
 };
index 68a0a3b..6803571 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 export default {
-    table: {
+    main: {
         region: '[data-region="core_table/dynamic"]',
+    },
+    table: {
         links: {
             sortableColumn: 'a[data-sortable="1"]',
         },
     },
+    initialsBar: {
+        links: {
+            firstInitial: '.firstinitial [data-initial]',
+            lastInitial: '.lastinitial [data-initial]',
+        },
+    },
 };
index 8cef49c..b029ff5 100644 (file)
@@ -86,6 +86,18 @@ class fetch extends external_api {
                 VALUE_OPTIONAL
             ),
             'jointype' => new external_value(PARAM_INT, 'Type of join to join all filters together', VALUE_REQUIRED),
+            'firstinitial' => new external_value(
+                PARAM_ALPHANUMEXT,
+                'The first initial to sort filter on',
+                VALUE_REQUIRED,
+                null
+            ),
+            'lastinitial' => new external_value(
+                PARAM_ALPHANUMEXT,
+                'The last initial to sort filter on',
+                VALUE_REQUIRED,
+                null
+            ),
         ]);
     }
 
@@ -98,11 +110,21 @@ class fetch extends external_api {
      * @param string $sortorder The sort order.
      * @param array $filters The filters that will be applied in the request.
      * @param string $jointype The join type.
+     * @param string $firstinitial The first name initial to filter on
+     * @param string $lastinitial The last name initial to filter on
      *
      * @return array
      */
-    public static function execute(string $handler, string $uniqueid, string $sortby, string $sortorder,
-            array $filters = [], string $jointype = null) {
+    public static function execute(
+        string $handler,
+        string $uniqueid,
+        string $sortby,
+        string $sortorder,
+        ?array $filters = null,
+        ?string $jointype = null,
+        ?string $firstinitial = null,
+        ?string $lastinitial = null
+    ) {
 
         global $PAGE;
 
@@ -117,6 +139,8 @@ class fetch extends external_api {
             'sortorder' => $sortorder,
             'filters' => $filters,
             'jointype' => $jointype,
+            'firstinitial' => $firstinitial,
+            'lastinitial' => $lastinitial,
         ] = self::validate_parameters(self::execute_parameters(), [
             'handler' => $handler,
             'uniqueid' => $uniqueid,
@@ -124,6 +148,8 @@ class fetch extends external_api {
             'sortorder' => $sortorder,
             'filters' => $filters,
             'jointype' => $jointype,
+            'firstinitial' => $firstinitial,
+            'lastinitial' => $lastinitial,
         ]);
 
         $filterset = new \core_user\table\participants_filterset();
@@ -139,6 +165,14 @@ class fetch extends external_api {
         $instance->set_filterset($filterset);
         $instance->set_sorting($sortby, $sortorder);
 
+        if ($firstinitial !== null) {
+            $instance->set_first_initial($firstinitial);
+        }
+
+        if ($lastinitial !== null) {
+            $instance->set_last_initial($lastinitial);
+        }
+
         $context = $instance->get_context();
 
         self::validate_context($context);
index 95fbf66..0203543 100644 (file)
@@ -95,6 +95,12 @@ class flexible_table {
      */
     protected $sortorder;
 
+    /** @var string The manually set first name initial preference */
+    protected $ifirst;
+
+    /** @var string The manually set last name initial preference */
+    protected $ilast;
+
     var $use_pages      = false;
     var $use_initials   = false;
 
@@ -525,16 +531,7 @@ class flexible_table {
         }
 
         $this->set_sorting_preferences();
-
-        $ilast = optional_param($this->request[TABLE_VAR_ILAST], null, PARAM_RAW);
-        if (!is_null($ilast) && ($ilast ==='' || strpos(get_string('alphabet', 'langconfig'), $ilast) !== false)) {
-            $this->prefs['i_last'] = $ilast;
-        }
-
-        $ifirst = optional_param($this->request[TABLE_VAR_IFIRST], null, PARAM_RAW);
-        if (!is_null($ifirst) && ($ifirst === '' || strpos(get_string('alphabet', 'langconfig'), $ifirst) !== false)) {
-            $this->prefs['i_first'] = $ifirst;
-        }
+        $this->set_initials_preferences();
 
         // Save user preferences if they have changed.
         if ($this->prefs != $oldprefs) {
@@ -1002,12 +999,18 @@ class flexible_table {
     function print_nothing_to_display() {
         global $OUTPUT;
 
+        // Render the dynamic table header.
+        echo $this->get_dynamic_table_html_start();
+
         // Render button to allow user to reset table preferences.
         echo $this->render_reset_button();
 
         $this->print_initials_bar();
 
         echo $OUTPUT->heading(get_string('nothingtodisplay'));
+
+        // Render the dynamic table footer.
+        echo $this->get_dynamic_table_html_end();
     }
 
     /**
@@ -1172,12 +1175,8 @@ class flexible_table {
                 echo $OUTPUT->render($pagingbar);
             }
 
-            // Dynamic Table content.
-            if (is_a($this, \core_table\dynamic::class)) {
-                echo html_writer::end_tag('div');
-
-                $PAGE->requires->js_call_amd('core_table/dynamic', 'init');
-            }
+            // Render the dynamic table footer.
+            echo $this->get_dynamic_table_html_end();
         }
     }
 
@@ -1353,6 +1352,31 @@ class flexible_table {
         }
     }
 
+    /**
+     * Fill in the preferences for the initials bar.
+     */
+    protected function set_initials_preferences(): void {
+        $ifirst = $this->ifirst;
+        $ilast = $this->ilast;
+
+        if ($ifirst === null) {
+            $ifirst = optional_param($this->request[TABLE_VAR_IFIRST], null, PARAM_RAW);
+        }
+
+        if ($ilast === null) {
+            $ilast = optional_param($this->request[TABLE_VAR_ILAST], null, PARAM_RAW);
+        }
+
+        if (!is_null($ifirst) && ($ifirst === '' || strpos(get_string('alphabet', 'langconfig'), $ifirst) !== false)) {
+            $this->prefs['i_first'] = $ifirst;
+        }
+
+        if (!is_null($ilast) && ($ilast === '' || strpos(get_string('alphabet', 'langconfig'), $ilast) !== false)) {
+            $this->prefs['i_last'] = $ilast;
+        }
+
+    }
+
     /**
      * Set the preferred table sorting attributes.
      *
@@ -1364,6 +1388,24 @@ class flexible_table {
         $this->sortorder = $sortorder;
     }
 
+    /**
+     * Set the preferred first name initial in an initials bar.
+     *
+     * @param string $initial The character to set
+     */
+    public function set_first_initial(string $initial): void {
+        $this->ifirst = $initial;
+    }
+
+    /**
+     * Set the preferred last name initial in an initials bar.
+     *
+     * @param string $initial The character to set
+     */
+    public function set_last_initial(string $initial): void {
+        $this->ilast = $initial;
+    }
+
     /**
      * Generate the HTML for the sort icon. This is a helper method used by {@link sort_link()}.
      * @param bool $isprimary whether an icon is needed (it is only needed for the primary sort column.)
@@ -1446,23 +1488,55 @@ class flexible_table {
     }
 
     /**
-     * This function is not part of the public api.
+     * Get the dynamic table start wrapper.
+     * If this is not a dynamic table, then an empty string is returned making this safe to blindly call.
+     *
+     * @return string
      */
-    function start_html() {
-        global $OUTPUT;
-
+    protected function get_dynamic_table_html_start(): string {
         if (is_a($this, \core_table\dynamic::class)) {
             $sortdata = $this->get_sort_order();
-            echo html_writer::start_tag('div', [
+            return html_writer::start_tag('div', [
                 'data-region' => 'core_table/dynamic',
                 'data-table-handler' => get_class($this),
                 'data-table-uniqueid' => $this->uniqueid,
                 'data-table-filters' => json_encode($this->get_filterset()),
                 'data-table-sort-by' => $sortdata['sortby'],
                 'data-table-sort-order' => $sortdata['sortorder'],
+                'data-table-first-initial' => $this->prefs['i_first'],
+                'data-table-last-initial' => $this->prefs['i_last'],
             ]);
         }
 
+        return '';
+    }
+
+    /**
+     * Get the dynamic table end wrapper.
+     * If this is not a dynamic table, then an empty string is returned making this safe to blindly call.
+     *
+     * @return string
+     */
+    protected function get_dynamic_table_html_end(): string {