Merge branch 'MDL-68286-master' of git://github.com/andrewnicols/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 7 Apr 2020 22:37:02 +0000 (00:37 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 7 Apr 2020 22:37:02 +0000 (00:37 +0200)
147 files changed:
.eslintignore
.stylelintignore
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/renderer.php
blocks/admin_bookmarks/tests/behat/bookmark_admin_pages.feature
blocks/badges/block_badges.php
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/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/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/tests/check_test.php [new file with mode: 0644]
lib/tests/completionlib_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 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 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 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 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';
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..558f346 100644 (file)
@@ -2212,5 +2212,16 @@ 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);
+    }
+
     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 34cd2fe..a10fff1 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
diff --git a/lib/templates/check/result.mustache b/lib/templates/check/result.mustache
new file mode 100644 (file)
index 0000000..e1fb695
--- /dev/null
@@ -0,0 +1,57 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core/result
+
+    Moodle Check API result template.
+
+    The purpose of this template is to render result.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * status eg Ok, Warning
+
+    Example context (json):
+    { "status": "OK"}
+}}
+
+{{#isna}}
+    {{> core/check/result/na}}
+{{/isna}}
+{{#isok}}
+    {{> core/check/result/ok}}
+{{/isok}}
+{{#isinfo}}
+    {{> core/check/result/info}}
+{{/isinfo}}
+{{#isunknown}}
+    {{> core/check/result/unknown}}
+{{/isunknown}}
+{{#iswarning}}
+    {{> core/check/result/warning}}
+{{/iswarning}}
+{{#iserror}}
+    {{> core/check/result/error}}
+{{/iserror}}
+{{#iscritical}}
+    {{> core/check/result/critical}}
+{{/iscritical}}
diff --git a/lib/templates/check/result/critical.mustache b/lib/templates/check/result/critical.mustache
new file mode 100644 (file)
index 0000000..c4c2e5a
--- /dev/null
@@ -0,0 +1,36 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core/check/result/critical
+
+    Moodle Check API result template.
+
+    The purpose of this template is to render result.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * status eg Ok, Warning
+
+    Example context (json):
+    { "status": "OK"}
+}}
+<span class="badge badge-danger">{{status}}</span>
diff --git a/lib/templates/check/result/error.mustache b/lib/templates/check/result/error.mustache
new file mode 100644 (file)
index 0000000..c4c2e5a
--- /dev/null
@@ -0,0 +1,36 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core/check/result/critical
+
+    Moodle Check API result template.
+
+    The purpose of this template is to render result.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * status eg Ok, Warning
+
+    Example context (json):
+    { "status": "OK"}
+}}
+<span class="badge badge-danger">{{status}}</span>
diff --git a/lib/templates/check/result/info.mustache b/lib/templates/check/result/info.mustache
new file mode 100644 (file)
index 0000000..c0b4ead
--- /dev/null
@@ -0,0 +1,36 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core/check/result/info
+
+    Moodle Check API result template.
+
+    The purpose of this template is to render result.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * status eg Ok, Warning
+
+    Example context (json):
+    { "status": "OK"}
+}}
+<span class="badge badge-info">{{status}}</span>
diff --git a/lib/templates/check/result/na.mustache b/lib/templates/check/result/na.mustache
new file mode 100644 (file)
index 0000000..348e578
--- /dev/null
@@ -0,0 +1,36 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core/check/result/na
+
+    Moodle Check API result template.
+
+    The purpose of this template is to render result.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * status eg Ok, Warning
+
+    Example context (json):
+    { "status": "OK"}
+}}
+<span class="badge badge-secondary">{{status}}</span>
diff --git a/lib/templates/check/result/ok.mustache b/lib/templates/check/result/ok.mustache
new file mode 100644 (file)
index 0000000..dfafcb2
--- /dev/null
@@ -0,0 +1,36 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core/check/result/ok
+
+    Moodle Check API result template.
+
+    The purpose of this template is to render result.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * status eg Ok, Warning
+
+    Example context (json):
+    { "status": "OK"}
+}}
+<span class="badge badge-success">{{status}}</span>
diff --git a/lib/templates/check/result/unknown.mustache b/lib/templates/check/result/unknown.mustache
new file mode 100644 (file)
index 0000000..959e6a9
--- /dev/null
@@ -0,0 +1,36 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core/check/result/unknown
+
+    Moodle Check API result template.
+
+    The purpose of this template is to render result.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * status eg Ok, Warning
+
+    Example context (json):
+    { "status": "OK"}
+}}
+<span class="badge badge-success">{{status}}</span>
diff --git a/lib/templates/check/result/warning.mustache b/lib/templates/check/result/warning.mustache
new file mode 100644 (file)
index 0000000..99926dc
--- /dev/null
@@ -0,0 +1,36 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core/check/result/warning
+
+    Moodle Check API result template.
+
+    The purpose of this template is to render result.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * status eg Ok, Warning
+
+    Example context (json):
+    { "status": "OK"}
+}}
+<span class="badge badge-warning">{{status}}</span>
diff --git a/lib/tests/check_test.php b/lib/tests/check_test.php
new file mode 100644 (file)
index 0000000..3fa527d
--- /dev/null
@@ -0,0 +1,64 @@
+<?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 unit tests
+ *
+ * @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
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\result;
+
+/**
+ * Example unit tests for check API
+ *
+ * @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 check_testcase extends advanced_testcase {
+
+    /**
+     * A simple example showing how a check and result object works
+     *
+     * Conceptually a check is analgous to a unit test except at runtime
+     * instead of build time so many checks in real life such as testing
+     * an API is connecting aren't viable to unit test.
+     */
+    public function test_passwordpolicy() {
+        global $CFG;
+        $prior = $CFG->passwordpolicy;
+
+        $check = new core\check\security\passwordpolicy();
+
+        $CFG->passwordpolicy = false;
+        $result = $check->get_result();
+        $this->assertEquals($result->status, result::WARNING);
+
+        $CFG->passwordpolicy = true;
+        $result = $check->get_result();
+        $this->assertEquals($result->status, result::OK);
+
+        $CFG->passwordpolicy = $prior;
+    }
+}
+
index 208aeb1..53e4071 100644 (file)
@@ -875,6 +875,44 @@ class core_completionlib_testcase extends advanced_testcase {
         $this->assertFalse($c2->has_activities());
     }
 
+    /**
+     * Test that data is cleaned up when we delete courses that are set as completion criteria for other courses
+     *
+     * @return void
+     */
+    public function test_course_delete_prerequisite() {
+        global $DB;
+
+        $this->setup_data();
+
+        $courseprerequisite = $this->getDataGenerator()->create_course(['enablecompletion' => true]);
+
+        $criteriadata = (object) [
+            'id' => $this->course->id,
+            'criteria_course' => [$courseprerequisite->id],
+        ];
+
+        /** @var completion_criteria_course $criteria */
+        $criteria = completion_criteria::factory(['criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE]);
+        $criteria->update_config($criteriadata);
+
+        // Sanity test.
+        $this->assertTrue($DB->record_exists('course_completion_criteria', [
+            'course' => $this->course->id,
+            'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
+            'courseinstance' => $courseprerequisite->id,
+        ]));
+
+        // Deleting the prerequisite course should remove the completion criteria.
+        delete_course($courseprerequisite, false);
+
+        $this->assertFalse($DB->record_exists('course_completion_criteria', [
+            'course' => $this->course->id,
+            'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
+            'courseinstance' => $courseprerequisite->id,
+        ]));
+    }
+
     /**
      * Test course module completion update event.
      */
index e2c5f4a..9c01c71 100644 (file)
     <version>7.7.0</version>
   </library>
   <library>
-    <location>mdn-polyfills</location>
+    <location>polyfills</location>
     <name>mdn-polyfill</name>
     <license>MIT</license>
     <version>5.19.0</version>
     <license>MIT</license>
     <version>4.1.0</version>
   </library>
+  <library>
+    <location>polyfills</location>
+    <name>url-polyfill</name>
+    <license>MIT</license>
+    <version>1.1.8</version>
+  </library>
 </libraries>
index b647892..002af63 100644 (file)
@@ -36,6 +36,8 @@ information provided here is intended especially for developers.
 * grade_item::update_final_grade() can now take an optional parameter to set the grade->timemodified. If not present the current time will carry on being used.
 * lib/outputrequirementslib::get_jsrev now is public, it can be called from other classes.
 * H5P libraries have been moved from /lib/h5p to h5p/h5plib as an h5plib plugintype.
+* mdn-polyfills has been renamed to polyfills. The reason there is no polyfill from the MDN is
+  because there is no example polyfills on the MDN for this functionality.
 
 === 3.8 ===
 * Add CLI option to notify all cron tasks to stop: admin/cli/cron.php --stop
index fd38391..5d98c7c 100644 (file)
Binary files a/lib/yui/build/moodle-core-blocks/moodle-core-blocks-debug.js and b/lib/yui/build/moodle-core-blocks/moodle-core-blocks-debug.js differ
index dd97f89..89360fb 100644 (file)
Binary files a/lib/yui/build/moodle-core-blocks/moodle-core-blocks-min.js and b/lib/yui/build/moodle-core-blocks/moodle-core-blocks-min.js differ
index 3a4ef1a..bed210b 100644 (file)
Binary files a/lib/yui/build/moodle-core-blocks/moodle-core-blocks.js and b/lib/yui/build/moodle-core-blocks/moodle-core-blocks.js differ
index 19d0fe9..dc7fe32 100644 (file)
Binary files a/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js and b/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js differ
index 63bbd2c..6e90583 100644 (file)
Binary files a/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js and b/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js differ
index 19d0fe9..dc7fe32 100644 (file)
Binary files a/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop.js and b/lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop.js differ
index 1db4d31..59539ff 100644 (file)
@@ -66,6 +66,8 @@ MANAGER.prototype = {
         this.groups = ['block'];
         this.samenodeclass = CSS.BLOCK;
         this.parentnodeclass = CSS.BLOCKREGION;
+        // Detect the direction of travel.
+        this.detectkeyboarddirection = true;
 
         // Add relevant classes and ID to 'content' block region on Dashboard page.
         var myhomecontent = Y.Node.all('body#' + CSS.MYINDEX + ' #' + CSS.REGIONMAIN + ' > .' + CSS.REGIONCONTENT);
index 71152f4..ba6deee 100644 (file)
@@ -99,6 +99,15 @@ Y.extend(DRAGDROP, Y.Base, {
      */
     lastdroptarget: null,
 
+    /**
+     * Should the direction of a keyboard drag and drop item be detected.
+     *
+     * @property detectkeyboarddirection
+     * @type Boolean
+     * @default false
+     */
+    detectkeyboarddirection: false,
+
     /**
      * Listeners.
      *
@@ -418,7 +427,7 @@ Y.extend(DRAGDROP, Y.Base, {
             var className = node.getAttribute("class").split(' ').join(', .');
 
             if (node.drop && node.drop.inGroup(this.groups) && node.drop.get('node') !== dragcontainer &&
-                    node.next(className) !== dragcontainer) {
+                    !(node.next(className) === dragcontainer && !this.detectkeyboarddirection)) {
                 // This is a drag and drop target with the same class as the grabbed node.
                 validdrop = true;
             } else {
@@ -426,7 +435,7 @@ Y.extend(DRAGDROP, Y.Base, {
                 var i, j;
                 for (i = 0; i < elementgroups.length; i++) {
                     for (j = 0; j < this.groups.length; j++) {
-                        if (elementgroups[i] === this.groups[j] && !(node == dragcontainer ||
+                        if (elementgroups[i] === this.groups[j] && !node.ancestor('.yui3-dd-proxy') && !(node == dragcontainer ||
                             node.next(className) === dragcontainer || node.get('children').item(0) == dragcontainer)) {
                                 // This is a parent node of the grabbed node (used for dropping in empty sections).
                                 validdrop = true;
@@ -566,6 +575,16 @@ Y.extend(DRAGDROP, Y.Base, {
         M.core.dragdrop.dropui.hide();
         // Cancel the event.
         e.preventDefault();
+        // Detect the direction of travel.
+        if (this.detectkeyboarddirection && dragcontainer.getY() > droptarget.getY()) {
+            // We can detect the keyboard direction and it is going up.
+            this.absgoingup = true;
+            this.goingup = true;
+        } else {
+            // The default behaviour is to treat everything as moving down.
+            this.absgoingup = false;
+            this.goingup = false;
+        }
         // Convert to drag drop events.
         var dragevent = new this.simulated_drag_drop_event(dragcontainer, dragcontainer);
         var dropevent = new this.simulated_drag_drop_event(dragcontainer, droptarget);
@@ -574,10 +593,7 @@ Y.extend(DRAGDROP, Y.Base, {
         this.global_drop_over(dropevent);
 
         if (droptarget.hasClass(this.parentnodeclass) && droptarget.contains(dragcontainer)) {
-            // The global_drop_over function does not handle the case where an item was moved up, without the
-            // 'goingup' variable being set, as is the case wih keyboard drag/drop. We must detect this case and
-            // apply it after the drop_over, but before the drop_hit event in order for it to be moved to the
-            // correct location.
+            // Handle the case where an item is dropped into a container (for example an activity into a new section).
             droptarget.prepend(dragcontainer);
         }
 
index 14ce357..3587193 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js differ
index 6b855e8..ef8fce6 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js differ
index 14ce357..3587193 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js differ
index dfeb368..61f23ea 100644 (file)
@@ -1084,16 +1084,22 @@ EDITOR.prototype = {
      * @method edit_move
      */
     edit_move: function(e) {
-        e.preventDefault();
         var bounds = this.get_canvas_bounds(),
             canvas = this.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
             drawingregion = this.get_dialogue_element(SELECTOR.DRAWINGREGION),
             clientpoint = new M.assignfeedback_editpdf.point(e.clientX + canvas.get('docScrollX'),
                                                              e.clientY + canvas.get('docScrollY')),
             point = this.get_canvas_coordinates(clientpoint),
+            activeelement = document.activeElement,
             diffX,
             diffY;
 
+        if (activeelement.type === 'textarea') {
+            return;
+        }
+
+        e.preventDefault();
+
         // Ignore events out of the canvas area.
         if (point.x < 0 || point.x > bounds.width || point.y < 0 || point.y > bounds.height) {
             return;
index 632b35e..85a2b90 100644 (file)
@@ -183,8 +183,6 @@ class renderer extends plugin_renderer_base {
      * @return array The array containing the content of the book chapter and visibility information
      */
     public function render_print_book_chapter($chapter, $chapters, $book, $cm) {
-        global $OUTPUT;
-
         $context = context_module::instance($cm->id);
         $title = book_get_chapter_title($chapter->id, $chapters, $book, $context);
 
@@ -194,9 +192,9 @@ class renderer extends plugin_renderer_base {
         $bookchapter .= html_writer::start_div('book_chapter p-t-1', ['id' => 'ch' . $chapter->id]);
         if (!$book->customtitles) {
             if (!$chapter->subchapter) {
-                $bookchapter .= $OUTPUT->heading($title, 2, 'text-center p-b-2');
+                $bookchapter .= $this->output->heading($title, 2, 'text-center p-b-2');
             } else {
-                $bookchapter .= $OUTPUT->heading($title, 3, 'text-center p-b-2');
+                $bookchapter .= $this->output->heading($title, 3, 'text-center p-b-2');
             }
         }
 
index b214b23..f00fa36 100644 (file)
@@ -222,7 +222,7 @@ $navclasses = book_get_nav_classes();
 
 if ($book->navstyle) {
     // Upper navigation.
-    echo '<div class="navtop clearfix ' . $navclasses[$book->navstyle] . '">' . $chnavigation . '</div>';
+    echo '<div class="navtop border-top py-3 clearfix ' . $navclasses[$book->navstyle] . '">' . $chnavigation . '</div>';
 }
 
 // The chapter itself.
@@ -251,7 +251,7 @@ if (core_tag_tag::is_enabled('mod_book', 'book_chapters')) {
 
 if ($book->navstyle) {
     // Lower navigation.
-    echo '<div class="navbottom clearfix ' . $navclasses[$book->navstyle] . '">' . $chnavigation . '</div>';
+    echo '<div class="navbottom py-3 border-bottom clearfix ' . $navclasses[$book->navstyle] . '">' . $chnavigation . '</div>';
 }
 
 echo $OUTPUT->footer();
index c780cbf..cdaae58 100644 (file)
@@ -129,16 +129,14 @@ class mod_choice_renderer extends plugin_renderer_base {
     /**
      * Returns HTML to display choices result
      * @param object $choices
-     * @param bool $forcepublish
      * @return string
      */
     public function display_publish_name_vertical($choices) {
-        global $PAGE, $OUTPUT;
         $html ='';
         $html .= html_writer::tag('h3',format_string(get_string("responses", "choice")));
 
         $attributes = array('method'=>'POST');
-        $attributes['action'] = new moodle_url($PAGE->url);
+        $attributes['action'] = new moodle_url($this->page->url);
         $attributes['id'] = 'attemptsform';
 
         if ($choices->viewresponsecapability) {
@@ -205,7 +203,7 @@ class mod_choice_renderer extends plugin_renderer_base {
                     'labelclasses' => 'accesshide',
                 ]);
 
-                $celltext .= html_writer::div($OUTPUT->render($mastercheckbox));
+                $celltext .= html_writer::div($this->output->render($mastercheckbox));
             }
             $numberofuser = 0;
             if (!empty($options->user) && count($options->user) > 0) {
@@ -267,7 +265,7 @@ class mod_choice_renderer extends plugin_renderer_base {
                                 'label' => $userfullname . ' ' . $options->text,
                                 'labelclasses' => 'accesshide',
                             ]);
-                            $checkbox = $OUTPUT->render($slavecheckbox);
+                            $checkbox = $this->output->render($slavecheckbox);
                         }
                         $userimage = $this->output->user_picture($user, array('courseid' => $choices->courseid, 'link' => false));
                         $profileurl = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $choices->courseid));
@@ -299,9 +297,10 @@ class mod_choice_renderer extends plugin_renderer_base {
                 'label' => get_string('selectall'),
                 'classes' => 'btn-secondary mr-1'
             ], true);
-            $actiondata .= $OUTPUT->render($selectallcheckbox);
+            $actiondata .= $this->output->render($selectallcheckbox);
 
-            $actionurl = new moodle_url($PAGE->url, array('sesskey'=>sesskey(), 'action'=>'delete_confirmation()'));
+            $actionurl = new moodle_url($this->page->url,
+                    ['sesskey' => sesskey(), 'action' => 'delete_confirmation()']);
             $actionoptions = array('delete' => get_string('delete'));
             foreach ($choices->options as $optionid => $option) {
                 if ($optionid > 0) {
@@ -338,7 +337,6 @@ class mod_choice_renderer extends plugin_renderer_base {
      * @return string
      */
     public function display_publish_anonymous_horizontal($choices) {
-        global $CHOICE_COLUMN_HEIGHT;
         debugging(__FUNCTION__.'() is deprecated. Please use mod_choice_renderer::display_publish_anonymous() instead.',
                 DEBUG_DEVELOPER);
         return $this->display_publish_anonymous($choices, CHOICE_DISPLAY_VERTICAL);
@@ -351,7 +349,6 @@ class mod_choice_renderer extends plugin_renderer_base {
      * @return string
      */
     public function display_publish_anonymous_vertical($choices) {
-        global $CHOICE_COLUMN_WIDTH;
         debugging(__FUNCTION__.'() is deprecated. Please use mod_choice_renderer::display_publish_anonymous() instead.',
                 DEBUG_DEVELOPER);
         return $this->display_publish_anonymous($choices, CHOICE_DISPLAY_HORIZONTAL);
@@ -367,7 +364,6 @@ class mod_choice_renderer extends plugin_renderer_base {
      * @return string the rendered chart.
      */
     public function display_publish_anonymous($choices, $displaylayout) {
-        global $OUTPUT;
         $count = 0;
         $data = [];
         $numberofuser = 0;
@@ -396,7 +392,7 @@ class mod_choice_renderer extends plugin_renderer_base {
         $chart->set_labels($data['labels']);
         $yaxis = $chart->get_yaxis(0, true);
         $yaxis->set_stepsize(max(1, round(max($data['series']) / 10)));
-        return $OUTPUT->render($chart);
+        return $this->output->render($chart);
     }
 }
 
index 3585b18..e97cba1 100644 (file)
@@ -52,12 +52,16 @@ class data_field_multimenu extends data_field_base {
         $str = '<div title="'.s($this->field->description).'">';
         $str .= '<input name="field_' . $this->field->id . '[xxx]" type="hidden" value="xxx"/>'; // hidden field - needed for empty selection
 
-        $str .= '<label for="field_' . $this->field->id . '" class="accesshide">';
-        $str .= html_writer::span($this->field->name);
+        $str .= '<label for="field_' . $this->field->id . '">';
+        $str .= '<legend><span class="accesshide">' . $this->field->name;
+
         if ($this->field->required) {
+            $str .= '&nbsp;' . get_string('requiredelement', 'form') . '</span></legend>';
             $str .= '<div class="inline-req">';
             $str .= $OUTPUT->pix_icon('req', get_string('requiredelement', 'form'));
             $str .= '</div>';
+        } else {
+            $str .= '</span></legend>';
         }
         $str .= '</label>';
         $str .= '<select name="field_' . $this->field->id . '[]" id="field_' . $this->field->id . '"';
index cc95336..9374eb5 100644 (file)
@@ -58,13 +58,16 @@ class report_downloaded extends \core\event\base {
      * @return string
      */
     public function get_description() {
-        if ($this->other['hasviewall']) {
-            return "The user with id '{$this->userid}' downloaded the summary report for the forum with " .
-                    "course module id '{$this->contextinstanceid}'.";
+        $whose = $this->other['hasviewall'] ? 'the' : 'their own';
+        $description = "The user with id '{$this->userid}' downloaded {$whose} summary report for ";
+
+        if ($this->other['forumid']) {
+            $description .= "the forum with course module id '{$this->contextinstanceid}'.";
         } else {
-            return "The user with id '{$this->userid}' downloaded their own summary report for the forum with " .
-                    "course module id '{$this->contextinstanceid}'.";
+            $description .= "the course with id '{$this->contextinstanceid}'.";
         }
+
+        return $description;
     }
 
     /**
@@ -72,8 +75,13 @@ class report_downloaded extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/mod/forum/report/summary/index.php',
-                ['courseid' => $this->courseid, 'forumid' => $this->other['forumid']]);
+        $params = ['courseid' => $this->courseid];
+
+        if (!empty($this->other['forumid'])) {
+            $params['forumid'] = $this->other['forumid'];
+        }
+
+        return new \moodle_url('/mod/forum/report/summary/index.php', $params);
     }
 
     /**
index 5f313d1..991dd32 100644 (file)
@@ -58,14 +58,16 @@ class report_viewed extends \core\event\base {
      * @return string
      */
     public function get_description() {
-        if ($this->other['hasviewall']) {
-            return "The user with id '{$this->userid}' viewed the summary report for the forum with " .
-                    "course module id '{$this->contextinstanceid}'.";
+        $whose = $this->other['hasviewall'] ? 'the' : 'their own';
+        $description = "The user with id '{$this->userid}' viewed {$whose} summary report for ";
 
+        if ($this->other['forumid']) {
+            $description .= "the forum with course module id '{$this->contextinstanceid}'.";
         } else {
-            return "The user with id '{$this->userid}' viewed their own summary report for the forum with " .
-                    "course module id '{$this->contextinstanceid}'.";
+            $description .= "the course with id '{$this->contextinstanceid}'.";
         }
+
+        return $description;
     }
 
     /**
@@ -74,8 +76,13 @@ class report_viewed extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/mod/forum/report/summary/index.php',
-                ['courseid' => $this->courseid, 'forumid' => $this->other['forumid']]);
+        $params = ['courseid' => $this->courseid];
+
+        if (!empty($this->other['forumid'])) {
+            $params['forumid'] = $this->other['forumid'];
+        }
+
+        return new \moodle_url('/mod/forum/report/summary/index.php', $params);
     }
 
     /**
index 4fe3398..54a6eea 100644 (file)
@@ -42,11 +42,19 @@ defined('MOODLE_INTERNAL') || die();
 class filters implements renderable, templatable {
 
     /**
-     * Course module the report is being run within.
+     * Course modules the report relates to.
+     * Array of stdClass objects
      *
-     * @var stdClass $cm
+     * @var array $cms
      */
-    protected $cm;
+    protected $cms;
+
+    /**
+     * Course ID where the report is being generated.
+     *
+     * @var int $courseid
+     */
+    protected $courseid;
 
     /**
      * Moodle URL used as the form action on the generate button.
@@ -98,13 +106,15 @@ class filters implements renderable, templatable {
     /**
      * Builds renderable filter data.
      *
-     * @param stdClass $cm The course module object.
+     * @param stdClass $course The course object.
+     * @param array $cms Array of course module objects.
      * @param moodle_url $actionurl The form action URL.
      * @param array $filterdata (optional) Associative array of data that has been set on available filters, if any,
-     *                                      in the format filtertype => [values]
+     *                                     in the format filtertype => [values]
      */
-    public function __construct(stdClass $cm, moodle_url $actionurl, array $filterdata = []) {
-        $this->cm = $cm;
+    public function __construct(stdClass $course, array $cms, moodle_url $actionurl, array $filterdata = []) {
+        $this->cms = $cms;
+        $this->courseid = $course->id;
         $this->actionurl = $actionurl;
 
         // Prepare groups filter data.
@@ -126,26 +136,54 @@ class filters implements renderable, templatable {
     protected function prepare_groups_data(array $groupsdata): void {
         global $DB, $USER;
 
-        $groupmode = groups_get_activity_groupmode($this->cm);
-        $context = \context_module::instance($this->cm->id);
-        $aag = has_capability('moodle/site:accessallgroups', $context);
         $groupsavailable = [];
+        $allowedgroupsobj = [];
+
+        $usergroups = groups_get_all_groups($this->courseid, $USER->id);
+        $coursegroups = groups_get_all_groups($this->courseid);
+        $forumids = [];
+        $allgroups = false;
+        $hasgroups = false;
+
+        // Check if any forum gives the user access to all groups and no groups.
+        foreach ($this->cms as $cm) {
+            $forumids[] = $cm->instance;
+
+            // Only need to check for all groups access if not confirmed by a previous check.
+            if (!$allgroups) {
+                $groupmode = groups_get_activity_groupmode($cm);
+
+                // If no groups mode enabled on the forum, nothing to prepare.
+                if (!in_array($groupmode, [VISIBLEGROUPS, SEPARATEGROUPS])) {
+                    continue;
+                }
+
+                $hasgroups = true;
+
+                // Fetch for the current cm's forum.
+                $context = \context_module::instance($cm->id);
+                $aag = has_capability('moodle/site:accessallgroups', $context);
+
+                if ($groupmode == VISIBLEGROUPS || $aag) {
+                    $allgroups = true;
+                }
+            }
+        }
 
         // If no groups mode enabled, nothing to prepare.
-        if (!in_array($groupmode, [VISIBLEGROUPS, SEPARATEGROUPS])) {
+        if (!$hasgroups) {
             return;
         }
 
-        if ($groupmode == VISIBLEGROUPS || $aag) {
-            // Any groups, and no groups.
-            $allowedgroupsobj = groups_get_all_groups($this->cm->course, 0, $this->cm->groupingid);
+        // Any groups, and no groups.
+        if ($allgroups) {
             $nogroups = new stdClass();
             $nogroups->id = -1;
             $nogroups->name = get_string('groupsnone');
-            $allowedgroupsobj[] = $nogroups;
+
+            $allowedgroupsobj = $coursegroups + [$nogroups];
         } else {
-            // Only assigned groups.
-            $allowedgroupsobj = groups_get_all_groups($this->cm->course, $USER->id, $this->cm->groupingid);
+            $allowedgroupsobj = $usergroups;
         }
 
         foreach ($allowedgroupsobj as $group) {
@@ -159,17 +197,16 @@ class filters implements renderable, templatable {
         $this->groupsavailable = $groupsavailable;
         $this->groupsselected = $groupsselected;
 
-        // If export links will require discussion filtering, find and set the discussion IDs.
         $groupsselectedcount = count($groupsselected);
         if ($groupsselectedcount > 0 && $groupsselectedcount < count($groupsavailable)) {
+            list($forumidin, $forumidparams) = $DB->get_in_or_equal($forumids, SQL_PARAMS_NAMED);
             list($groupidin, $groupidparams) = $DB->get_in_or_equal($groupsselected, SQL_PARAMS_NAMED);
-            $dwhere = "course = :courseid AND forum = :forumid AND groupid {$groupidin}";
-            $dparams = [
-                'courseid' => $this->cm->course,
-                'forumid' => $this->cm->instance,
-            ];
-            $dparams += $groupidparams;
-            $discussionids = $DB->get_fieldset_select('forum_discussions', 'DISTINCT id', $dwhere, $dparams);
+
+            $discussionswhere = "course = :courseid AND forum {$forumidin} AND groupid {$groupidin}";
+            $discussionsparams = ['courseid' => $this->courseid];
+            $discussionsparams += $forumidparams + $groupidparams;
+
+            $discussionids = $DB->get_fieldset_select('forum_discussions', 'DISTINCT id', $discussionswhere, $discussionsparams);
 
             foreach ($discussionids as $discussionid) {
                 $this->discussionids[] = ['discid' => $discussionid];
index 7cc4ba5..2e5e7a6 100644 (file)
@@ -62,8 +62,17 @@ class summary_table extends table_sql {
     /** @var array The values available for pagination size per page. */
     protected $perpageoptions = [50, 100, 200];
 
-    /** @var \stdClass The course module object of the forum being reported on. */
-    protected $cm;
+    /** @var int The course ID containing the forum(s) being reported on. */
+    protected $courseid;
+
+    /** @var bool True if reporting on all forums in course user has access to, false if reporting on a single forum */
+    protected $iscoursereport = false;
+
+    /** @var bool True if user has access to all forums in the course (and is running course report), otherwise false. */
+    protected $accessallforums = false;
+
+    /** @var \stdClass The course module object(s) of the forum(s) being reported on. */
+    protected $cms = [];
 
     /**
      * @var int The user ID if only one user's summary will be generated.
@@ -77,9 +86,14 @@ class summary_table extends table_sql {
     protected $logreader = null;
 
     /**
-     * @var \context|null
+     * @var array of \context objects for the forums included in the report.
      */
-    protected $context = null;
+    protected $forumcontexts = [];
+
+    /**
+     * @var context_course|context_module The context where the report is being run (either a specific forum or the course).
+     */
+    protected $userfieldscontext = null;
 
     /** @var bool Whether the user has the capability/capabilities to perform bulk operations. */
     protected $allowbulkoperations = false;
@@ -108,25 +122,24 @@ class summary_table extends table_sql {
      * @param bool $canseeprivatereplies Whether the user can see all private replies or not.
      * @param int $perpage The number of rows to display per page.
      * @param bool $canexport Is the user allowed to export records?
+     * @param bool $iscoursereport Whether the user is running a course level report
+     * @param bool $accessallforums If user is running a course level report, do they have access to all forums in the course?
      */
     public function __construct(int $courseid, array $filters, bool $allowbulkoperations,
-            bool $canseeprivatereplies, int $perpage, bool $canexport) {
-        global $USER, $OUTPUT;
-
-        $forumid = $filters['forums'][0];
+            bool $canseeprivatereplies, int $perpage, bool $canexport, bool $iscoursereport, bool $accessallforums) {
+        global $OUTPUT;
 
-        parent::__construct("summaryreport_{$courseid}_{$forumid}");
+        $uniqueid = $courseid . ($iscoursereport ? '' : '_' . $filters['forums'][0]);
+        parent::__construct("summaryreport_{$uniqueid}");
 
-        $this->cm = get_coursemodule_from_instance('forum', $forumid, $courseid);
-        $this->context = \context_module::instance($this->cm->id);
+        $this->courseid = $courseid;
+        $this->iscoursereport = $iscoursereport;
+        $this->accessallforums = $accessallforums;
         $this->allowbulkoperations = $allowbulkoperations;
         $this->canseeprivatereplies = $canseeprivatereplies;
         $this->perpage = $perpage;
 
-        // Only show their own summary unless they have permission to view all.
-        if (!has_capability('forumreport/summary:viewall', $this->context)) {
-            $this->userid = $USER->id;
-        }
+        $this->set_forum_properties($filters['forums']);
 
         $columnheaders = [];
 
@@ -180,6 +193,37 @@ class summary_table extends table_sql {
         $this->define_base_sql();
     }
 
+    /**
+     * Sets properties that are determined by forum filter values.
+     *
+     * @param array $forumids The forum IDs passed in by the filter.
+     * @return void
+     */
+    protected function set_forum_properties(array $forumids): void {
+        global $USER;
+
+        // Course context if reporting on all forums in the course the user has access to.
+        if ($this->iscoursereport) {
+            $this->userfieldscontext = \context_course::instance($this->courseid);
+        }
+
+        foreach ($forumids as $forumid) {
+            $cm = get_coursemodule_from_instance('forum', $forumid, $this->courseid);
+            $this->cms[] = $cm;
+            $this->forumcontexts[$cm->id] = \context_module::instance($cm->id);
+
+            // Set forum context if not reporting on course.
+            if (!isset($this->userfieldscontext)) {
+                $this->userfieldscontext = $this->forumcontexts[$cm->id];
+            }
+
+            // Only show own summary unless they have permission to view all in every forum being reported.
+            if (empty($this->userid) && !has_capability('forumreport/summary:viewall', $this->forumcontexts[$cm->id])) {
+                $this->userid = $USER->id;
+            }
+        }
+    }
+
     /**
      * Provides the string name of each filter type, to be used by errors.
      * Note: This does not use language strings as the value is injected into error strings.
@@ -230,7 +274,7 @@ class summary_table extends table_sql {
         }
 
         global $OUTPUT;
-        return $OUTPUT->user_picture($data, array('size' => 35, 'courseid' => $this->cm->course, 'includefullname' => true));
+        return $OUTPUT->user_picture($data, array('courseid' => $this->courseid, 'includefullname' => true));
     }
 
     /**
@@ -302,7 +346,7 @@ class summary_table extends table_sql {
         }
 
         $params = [
-            'id' => $this->cm->instance, // Forum id.
+            'id' => $this->cms[0]->instance, // Forum id.
             'userids[]' => $data->userid, // User id.
         ];
 
@@ -381,14 +425,15 @@ class summary_table extends table_sql {
 
         switch($filtertype) {
             case self::FILTER_FORUM:
-                // Requires exactly one forum ID.
-                if (count($values) != 1) {
+                // Requires at least one forum ID.
+                if (empty($values)) {
                     $paramcounterror = true;
                 } else {
                     // No select fields required - displayed in title.
                     // No extra joins required, forum is already joined.
-                    $this->sql->filterwhere .= ' AND f.id = :forumid';
-                    $this->sql->params['forumid'] = $values[0];
+                    list($forumidin, $forumidparams) = $DB->get_in_or_equal($values, SQL_PARAMS_NAMED);
+                    $this->sql->filterwhere .= " AND f.id {$forumidin}";
+                    $this->sql->params += $forumidparams;
                 }
 
                 break;
@@ -498,13 +543,12 @@ class summary_table extends table_sql {
     protected function define_base_sql(): void {
         global $USER;
 
-        $userfields = get_extra_user_fields($this->context);
+        $userfields = get_extra_user_fields($this->userfieldscontext);
         $userfieldssql = \user_picture::fields('u', $userfields);
 
         // Define base SQL query format.
         $this->sql->basefields = ' ue.userid AS userid,
                                    e.courseid AS courseid,
-                                   f.id as forumid,
                                    SUM(CASE WHEN p.parent = 0 THEN 1 ELSE 0 END) AS postcount,
                                    SUM(CASE WHEN p.parent != 0 THEN 1 ELSE 0 END) AS replycount,
                                    ' . $userfieldssql . ',
@@ -543,10 +587,10 @@ class summary_table extends table_sql {
 
         $this->sql->basewhere = 'e.courseid = :courseid';
 
-        $this->sql->basegroupby = 'ue.userid, e.courseid, f.id, u.id, ' . $userfieldssql;
+        $this->sql->basegroupby = 'ue.userid, e.courseid, u.id, ' . $userfieldssql;
 
         if ($this->logreader) {
-            $this->fill_log_summary_temp_table($this->context->id);
+            $this->fill_log_summary_temp_table();
 
             $this->sql->basefields .= ', CASE WHEN tmp.viewcount IS NOT NULL THEN tmp.viewcount ELSE 0 END AS viewcount';
             $this->sql->basefromjoins .= ' LEFT JOIN {' . self::LOG_SUMMARY_TEMP_TABLE . '} tmp ON tmp.userid = u.id ';
@@ -561,7 +605,7 @@ class summary_table extends table_sql {
 
         $this->sql->params += [
             'component' => 'mod_forum',
-            'courseid' => $this->cm->course,
+            'courseid' => $this->courseid,
         ] + $privaterepliesparams;
 
         // Handle if a user is limited to viewing their own summary.
@@ -642,8 +686,10 @@ class summary_table extends table_sql {
      * @return void.
      */
     protected function apply_filters(array $filters): void {
-        // Apply the forums filter.
-        $this->add_filter(self::FILTER_FORUM, $filters['forums']);
+        // Apply the forums filter if not reporting on every forum in a course.
+        if (!$this->accessallforums) {
+            $this->add_filter(self::FILTER_FORUM, $filters['forums']);
+        }
 
         // Apply groups filter.
         $this->add_filter(self::FILTER_GROUPS, $filters['groups']);
@@ -719,10 +765,9 @@ class summary_table extends table_sql {
     /**
      * Fills the log summary temp table.
      *
-     * @param int $contextid
      * @return null
      */
-    protected function fill_log_summary_temp_table(int $contextid) {
+    protected function fill_log_summary_temp_table() {
         global $DB;
 
         $this->create_log_summary_temp_table();
@@ -740,11 +785,19 @@ class summary_table extends table_sql {
         $datewhere = $this->sql->filterbase['dateslog'] ?? '';
         $dateparams = $this->sql->filterbase['dateslogparams'] ?? [];
 
-        $params = ['contextid' => $contextid] + $dateparams;
+        $contextids = [];
+
+        foreach ($this->forumcontexts as $forumcontext) {
+            $contextids[] = $forumcontext->id;
+        }
+
+        list($contextidin, $contextidparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
+
+        $params = $contextidparams + $dateparams;
         $sql = "INSERT INTO {" . self::LOG_SUMMARY_TEMP_TABLE . "} (userid, viewcount)
                      SELECT userid, COUNT(*) AS viewcount
                        FROM {" . $logtable . "}
-                      WHERE contextid = :contextid
+                      WHERE contextid {$contextidin}
                             $datewhere
                             $nonanonymous
                    GROUP BY userid";
@@ -797,42 +850,64 @@ class summary_table extends table_sql {
     protected function get_filter_groups(array $groups): array {
         global $USER;
 
-        $groupmode = groups_get_activity_groupmode($this->cm);
-        $aag = has_capability('moodle/site:accessallgroups', $this->context);
+        $usergroups = groups_get_all_groups($this->courseid, $USER->id);
+        $coursegroupsobj = groups_get_all_groups($this->courseid);
+        $allgroups = false;
+        $allowedgroupsobj = [];
         $allowedgroups = [];
         $filtergroups = [];
 
-        // Filtering only valid if a forum groups mode is enabled.
-        if (in_array($groupmode, [VISIBLEGROUPS, SEPARATEGROUPS])) {
-            $allgroupsobj = groups_get_all_groups($this->cm->course, 0, $this->cm->groupingid);
-            $allgroups = [];
+        foreach ($this->cms as $cm) {
+            // Only need to check for all groups access if not confirmed by a previous check.
+            if (!$allgroups) {
+                $groupmode = groups_get_activity_groupmode($cm);
 
-            foreach ($allgroupsobj as $group) {
-                $allgroups[] = $group->id;
-            }
+                // If no groups mode enabled on the forum, nothing to prepare.
+                if (!in_array($groupmode, [VISIBLEGROUPS, SEPARATEGROUPS])) {
+                    continue;
+                }
 
-            if ($groupmode == VISIBLEGROUPS || $aag) {
-                $nogroups = new \stdClass();
-                $nogroups->id = -1;
-                $nogroups->name = get_string('groupsnone');
+                $aag = has_capability('moodle/site:accessallgroups', $this->forumcontexts[$cm->id]);
 
-                // Any groups and no groups.
-                $allowedgroupsobj = $allgroupsobj + [$nogroups];
-            } else {
-                // Only assigned groups.
-                $allowedgroupsobj = groups_get_all_groups($this->cm->course, $USER->id, $this->cm->groupingid);
-            }
+                if ($groupmode == VISIBLEGROUPS || $aag) {
+                    $allgroups = true;
 
-            foreach ($allowedgroupsobj as $group) {
-                $allowedgroups[] = $group->id;
+                    // All groups in course fetched, no need to continue checking for others.
+                    break;
+                }
             }
+        }
 
-            // If not all groups in course are selected, filter by allowed groups submitted.
-            if (!empty($groups) && !empty(array_diff($allowedgroups, $groups))) {
+        if ($allgroups) {
+            $nogroups = new \stdClass();
+            $nogroups->id = -1;
+            $nogroups->name = get_string('groupsnone');
+
+            // Any groups and no groups.
+            $allowedgroupsobj = $coursegroupsobj + [$nogroups];
+        } else {
+            $allowedgroupsobj = $usergroups;
+        }
+
+        foreach ($allowedgroupsobj as $group) {
+            $allowedgroups[] = $group->id;
+        }
+
+        // If not all groups in course are selected, filter by allowed groups submitted.
+        if (!empty($groups)) {
+            if (!empty(array_diff($allowedgroups, $groups))) {
                 $filtergroups = array_intersect($groups, $allowedgroups);
-            } else if (!empty(array_diff($allgroups, $allowedgroups))) {
+            } else {
+                $coursegroups = [];
+
+                foreach ($coursegroupsobj as $group) {
+                    $coursegroups[] = $group->id;
+                }
+
                 // If user's 'all groups' is a subset of the course groups, filter by all groups available to them.
-                $filtergroups = $allowedgroups;
+                if (!empty(array_diff($coursegroups, $allowedgroups))) {
+                    $filtergroups = $allowedgroups;
+                }
             }
         }
 
@@ -863,13 +938,21 @@ class summary_table extends table_sql {
         global $DB;
 
         if (is_null($this->showwordcharcounts)) {
+            $forumids = [];
+
+            foreach ($this->cms as $cm) {
+                $forumids[] = $cm->instance;
+            }
+
+            list($forumidin, $forumidparams) = $DB->get_in_or_equal($forumids, SQL_PARAMS_NAMED);
+
             // This should be really fast.
             $sql = "SELECT 'x'
                       FROM {forum_posts} fp
                       JOIN {forum_discussions} fd ON fd.id = fp.discussion
-                     WHERE fd.forum = :forumid AND (fp.wordcount IS NULL OR fp.charcount IS NULL)";
+                     WHERE fd.forum {$forumidin} AND (fp.wordcount IS NULL OR fp.charcount IS NULL)";
 
-            if ($DB->record_exists_sql($sql, ['forumid' => $this->cm->instance])) {
+            if ($DB->record_exists_sql($sql, $forumidparams)) {
                 $this->showwordcharcounts = false;
             } else {
                 $this->showwordcharcounts = true;
index 274adcb..54ec58b 100644 (file)
@@ -29,68 +29,134 @@ if (isguestuser()) {
 }
 
 $courseid = required_param('courseid', PARAM_INT);
-$forumid = required_param('forumid', PARAM_INT);
+$forumid = optional_param('forumid', 0, PARAM_INT);
 $perpage = optional_param('perpage', \forumreport_summary\summary_table::DEFAULT_PER_PAGE, PARAM_INT);
+$download = optional_param('download', '', PARAM_ALPHA);
 $filters = [];
+$pageurlparams = [
+    'courseid' => $courseid,
+    'perpage' => $perpage,
+];
 
 // Establish filter values.
-$filters['forums'] = [$forumid];
 $filters['groups'] = optional_param_array('filtergroups', [], PARAM_INT);
 $filters['datefrom'] = optional_param_array('datefrom', ['enabled' => 0], PARAM_INT);
&n