Merge branch 'MDL-40081' of https://github.com/paulholden/moodle
authorSara Arjona <sara@moodle.com>
Tue, 7 Apr 2020 13:09:56 +0000 (15:09 +0200)
committerSara Arjona <sara@moodle.com>
Tue, 7 Apr 2020 13:09:56 +0000 (15:09 +0200)
142 files changed:
.eslintignore
.stylelintignore
admin/tasklogs.php
admin/templates/tasklogs.mustache
admin/tool/analytics/classes/output/renderer.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/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/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

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 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..e48348a 100644 (file)
@@ -30,6 +30,7 @@ $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}';
index 3afd20c..418fd7e 100644 (file)
@@ -36,9 +36,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,7 +69,7 @@ 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');
@@ -77,35 +78,46 @@ class tool_task_renderer extends plugin_renderer_base {
         $plugindisabledstr = get_string('plugindisabled', 'tool_task');
         $runnabletasks = 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);
+            list($type) = 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);
+                    if (!$plugininfo->is_enabled()) {
+                        $componentcell->text .= ' ' . html_writer::span(
+                                get_string('disabled', 'tool_task'), 'badge badge-secondary');
+                    }
+                    $componentcell->text .= "\n" . html_writer::span($plugininfo->component, 'task-class text-ltr');
                 } else {
                     $componentcell = new html_table_cell($component);
                 }
@@ -127,59 +139,100 @@ class tool_task_renderer extends plugin_renderer_base {
             }
 
             $runnow = '';
-            if ( ! $disabled && get_config('tool_task', 'enablerunnow') && $runnabletasks ) {
+            if (!$disabled && get_config('tool_task', 'enablerunnow') && $runnabletasks ) {
                 $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($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';
-
+                        $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) {
+            $this->page->requires->js_init_code(
+                    'document.querySelector("tr.table-primary").scrollIntoView({block: "center"});');
+        }
         return html_writer::table($table);
     }
 
+    /**
+     * 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..78b0be5 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,14 @@ if ($action == 'edit') {
 
 if ($task) {
     $mform = new tool_task_edit_scheduled_task_form(null, $task);
+    $nexturl = new moodle_url($PAGE->url, ['lastchanged' => $taskname]);
 }
 
 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,9 +77,9 @@ 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();
@@ -97,8 +89,9 @@ if ($mform && ($mform->is_cancelled() || !empty($CFG->preventscheduledtaskchange
     }
 
 } else {
+    $renderer = $PAGE->get_renderer('tool_task');
     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..6b97b68 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,6 +26,7 @@ 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"
@@ -38,8 +40,10 @@ Feature: Manage scheduled tasks
     And I press "Save changes"
     Then 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 +54,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;
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 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);
 $filters['dateto'] = optional_param_array('dateto', ['enabled' => 0], PARAM_INT);
 
-$download = optional_param('download', '', PARAM_ALPHA);
-
-$cm = null;
 $modinfo = get_fast_modinfo($courseid);
-
-if (!isset($modinfo->instances['forum'][$forumid])) {
-    throw new \moodle_exception("A valid forum ID is required to generate a summary report.");
+$course = $modinfo->get_course();
+$courseforums = $modinfo->instances['forum'];
+$cms = [];
+
+// Determine which forums the user has access to in the course.
+$accessallforums = false;
+$allforumidsincourse = array_keys($courseforums);
+$forumsvisibletouser = [];
+$forumselectoptions = [0 => get_string('forumselectcourseoption', 'forumreport_summary')];
+
+foreach ($courseforums as $courseforumid => $courseforum) {
+    if ($courseforum->uservisible) {
+        $forumsvisibletouser[$courseforumid] = $courseforum;
+        $forumselectoptions[$courseforumid] = $courseforum->name;
+    }
 }
 
-$foruminfo = $modinfo->instances['forum'][$forumid];
-$forumname = $foruminfo->name;
-$cm = $foruminfo->get_course_module_record();
+if ($forumid) {
+    if (!isset($forumsvisibletouser[$forumid])) {
+        throw new \moodle_exception('A valid forum ID is required to generate a summary report.');
+    }
 
-require_login($courseid, false, $cm);
-$context = \context_module::instance($cm->id);
+    $filters['forums'] = [$forumid];
+    $title = $forumsvisibletouser[$forumid]->name;
+    $forumcm = $forumsvisibletouser[$forumid];
+    $cms[] = $forumcm;
+
+    require_login($courseid, false, $forumcm);
+    $context = $forumcm->context;
+    $canexport = !$download && has_capability('mod/forum:exportforum', $context);
+    $redirecturl = new moodle_url('/mod/forum/view.php', ['id' => $forumid]);
+    $numforums = 1;
+    $pageurlparams['forumid'] = $forumid;
+    $iscoursereport = false;
+} else {
+    // Course level report.
+    require_login($courseid, false);
 
-// This capability is required to view any version of the report.
-if (!has_capability("forumreport/summary:view", $context)) {
-    $redirecturl = new moodle_url("/mod/forum/view.php");
-    $redirecturl->param('id', $forumid);
-    redirect($redirecturl);
-}
+    $filters['forums'] = array_keys($forumsvisibletouser);
 
-$course = $modinfo->get_course();
+    // Fetch the forum CMs for the course.
+    foreach ($forumsvisibletouser as $visibleforum) {
+        $cms[] = $visibleforum;
+    }
 
-$urlparams = [
-    'courseid' => $courseid,
-    'forumid' => $forumid,
-    'perpage' => $perpage,
-];
-$url = new moodle_url("/mod/forum/report/summary/index.php", $urlparams);
+    $context = \context_course::instance($courseid);
+    $title = $course->fullname;
+    // Export currently only supports single forum exports.
+    $canexport = false;
+    $redirecturl = new moodle_url('/course/view.php', ['id' => $courseid]);
+    $numforums = count($forumsvisibletouser);
+    $iscoursereport = true;
 
-$PAGE->set_url($url);
+    // Specify whether user has access to all forums in the course.
+    $accessallforums = empty(array_diff($allforumidsincourse, $filters['forums']));
+}
+
+$pageurl = new moodle_url('/mod/forum/report/summary/index.php', $pageurlparams);
+
+$PAGE->set_url($pageurl);
 $PAGE->set_pagelayout('report');
-$PAGE->set_title($forumname);
+$PAGE->set_title($title);
 $PAGE->set_heading($course->fullname);
-$PAGE->navbar->add(get_string('nodetitle', "forumreport_summary"));
+$PAGE->navbar->add(get_string('nodetitle', 'forumreport_summary'));
 
-// Prepare and display the report.
 $allowbulkoperations = !$download && !empty($CFG->messaging) && has_capability('moodle/course:bulkmessaging', $context);
-$canseeprivatereplies = has_capability('mod/forum:readprivatereplies', $context);
-$canexport = !$download && has_capability('mod/forum:exportforum', $context);
+$canseeprivatereplies = false;
+$hasviewall = false;
+$privatereplycapcount = 0;
+$viewallcount = 0;
+$canview = false;
+
+foreach ($cms as $cm) {
+    $forumcontext = $cm->context;
+
+    // This capability is required in at least one of the given contexts to view any version of the report.
+    if (has_capability('forumreport/summary:view', $forumcontext)) {
+        $canview = true;
+    }
+
+    if (has_capability('mod/forum:readprivatereplies', $forumcontext)) {
+        $privatereplycapcount++;
+    }
+
+    if (has_capability('forumreport/summary:viewall', $forumcontext)) {
+        $viewallcount++;
+    }
+}
 
+if (!$canview) {
+    redirect($redirecturl);
+}
+
+// Only use private replies if user has that cap in all forums in the report.
+if ($numforums === $privatereplycapcount) {
+    $canseeprivatereplies = true;
+}
+
+// Will only show all users if user has the cap for all forums in the report.
+if ($numforums === $viewallcount) {
+    $hasviewall = true;
+}
+
+// Prepare and display the report.
 $table = new \forumreport_summary\summary_table($courseid, $filters, $allowbulkoperations,
-        $canseeprivatereplies, $perpage, $canexport);
-$table->baseurl = $url;
+        $canseeprivatereplies, $perpage, $canexport, $iscoursereport, $accessallforums);
+$table->baseurl = $pageurl;
 
 $eventparams = [
     'context' => $context,
     'other' => [
         'forumid' => $forumid,
-        'hasviewall' => has_capability('forumreport/summary:viewall', $context),
+        'hasviewall' => $hasviewall,
     ],
 ];
 
@@ -101,16 +167,23 @@ if ($download) {
     \forumreport_summary\event\report_viewed::create($eventparams)->trigger();
 
     echo $OUTPUT->header();
-    echo $OUTPUT->heading(get_string('summarytitle', 'forumreport_summary', $forumname), 2, 'p-b-2');
+    echo $OUTPUT->heading(get_string('summarytitle', 'forumreport_summary', $title), 2, 'p-b-2');
 
     if (!empty($filters['groups'])) {
         \core\notification::info(get_string('viewsdisclaimer', 'forumreport_summary'));
     }
 
+    // Allow switching to course report (or other forum user has access to).
+    $reporturl = new moodle_url('/mod/forum/report/summary/index.php', ['courseid' => $courseid]);
+    $forumselect = new single_select($reporturl, 'forumid', $forumselectoptions, $forumid, '');
+    $forumselect->set_label(get_string('forumselectlabel', 'forumreport_summary'));
+    echo $OUTPUT->render($forumselect);
+
     // Render the report filters form.
     $renderer = $PAGE->get_renderer('forumreport_summary');
 
-    echo $renderer->render_filters_form($cm, $url, $filters);
+    unset($filters['forums']);
+    echo $renderer->render_filters_form($course, $cms, $pageurl, $filters);
     $table->show_download_buttons_at(array(TABLE_P_BOTTOM));
     echo $renderer->render_summary_table($table);
     echo $OUTPUT->footer();
index b741274..d9051b4 100644 (file)
@@ -38,16 +38,18 @@ $string['filter:groupsbuttonlabel'] = 'Open the groups filter';
 $string['filter:groupsname'] = 'Groups';
 $string['filter:groupscountall'] = 'Groups (all)';
 $string['filter:groupscountnumber'] = 'Groups ({$a})';
+$string['forumselectlabel'] = 'Forum selected';
+$string['forumselectcourseoption'] = 'All forums in course';
 $string['latestpost'] = 'Most recent post';
 $string['exportposts'] = 'Export posts';
 $string['exportpostslabel'] = 'Export posts for {$a}';
-$string['nodetitle'] = 'Summary report';
+$string['nodetitle'] = 'Forum summary report';
 $string['pluginname'] = 'Forum summary report';
 $string['postcount'] = 'Number of discussions posted';
 $string['privacy:metadata'] = 'The Forum summary report plugin does not store any personal data.';
 $string['replycount'] = 'Number of replies posted';
 $string['summary:viewall'] = 'Access summary report data for each user within a given forum or forums';
 $string['summary:view'] = 'Access summary report within a given forum or forums';
-$string['summarytitle'] = 'Summary report - {$a}';
+$string['summarytitle'] = 'Forum summary report - {$a}';
 $string['viewsdisclaimer'] = 'Number of views column is not filtered by group';
 $string['wordcount'] = 'Word count';
index 1c61dae..06d0448 100644 (file)
@@ -37,13 +37,14 @@ class forumreport_summary_renderer extends plugin_renderer_base {
     /**
      * Render the filters available for the forum summary report.
      *
-     * @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 $filters Optional array of currently applied filter values.
      * @return string The filter form HTML.
      */
-    public function render_filters_form(stdClass $cm, moodle_url $actionurl, array $filters = []): string {
-        $renderable = new \forumreport_summary\output\filters($cm, $actionurl, $filters);
+    public function render_filters_form(stdClass $course, array $cms, moodle_url $actionurl, array $filters = []): string {
+        $renderable = new \forumreport_summary\output\filters($course, $cms, $actionurl, $filters);
         $templatecontext = $renderable->export_for_template($this);
 
         return $this->render_from_template('forumreport_summary/filters', $templatecontext);
index 09dde0e..f0b9f2e 100644 (file)
@@ -61,7 +61,7 @@
     }
 }}
 
-<div class="pb-4" data-report-id="{{uniqid}}">
+<div class="pb-4 pt-4" data-report-id="{{uniqid}}">
     <form id="filtersform" name="filtersform" method="post" action="{{actionurl}}">
         <input type="hidden" name="submitted" value="true">
 
index beac0d5..86fb4bb 100644 (file)
@@ -43,7 +43,7 @@ Feature: Message users in the summary report
     When I log in as "teacher1"
     And I am on "Course 1" course homepage
     And I follow "forum1"
-    And I navigate to "Summary report" in current page administration
+    And I navigate to "Forum summary report" in current page administration
     And I click on "Select 'Student 1'" "checkbox"
     And I click on "Select 'Student 3'" "checkbox"
     And I set the field "With selected users..." to "Send a message"
@@ -65,7 +65,7 @@ Feature: Message users in the summary report
     When I log in as "teacher1"
     And I am on "Course 1" course homepage
     And I follow "forum1"
-    And I navigate to "Summary report" in current page administration
+    And I navigate to "Forum summary report" in current page administration
     And I click on "Select all" "checkbox"
     And I set the field "With selected users..." to "Send a message"
     Then I should see "Send message to 3 people"
@@ -79,6 +79,6 @@ Feature: Message users in the summary report
     And I log in as "teacher1"
     And I am on "Course 1" course homepage
     And I follow "forum1"
-    And I navigate to "Summary report" in current page administration
+    And I navigate to "Forum summary report" in current page administration
     Then I should not see "With selected users..."
     And I should not see "Select all"
diff --git a/mod/forum/report/summary/tests/behat/course_summary.feature b/mod/forum/report/summary/tests/behat/course_summary.feature
new file mode 100644 (file)
index 0000000..473bdad
--- /dev/null
@@ -0,0 +1,120 @@
+@mod @mod_forum @forumreport @forumreport_summary
+Feature: Course level forum summary report
+  In order to gain an overview of students' forum activities across a course
+  As a teacher
+  I should be able to prepare a summary report of all forums in a course
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+      | student1 | Student   | 1        | student1@example.com |
+      | student2 | Student   | 2        | student2@example.com |
+      | student3 | Student   | 3        | student3@example.com |
+    And the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+      | Course 2 | C2        |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | teacher1 | C2     | editingteacher |
+      | student1 | C1     | student        |
+      | student2 | C1     | student        |
+      | student2 | C2     | student        |
+      | student3 | C2     | student        |
+    And the following "activities" exist:
+      | activity | name   | description | course | idnumber |
+      | forum    | forum1 | C1 forum 1  | C1     | forum1   |
+      | forum    | forum2 | C1 forum 2  | C1     | forum2   |
+      | forum    | forum3 | C1 forum 3  | C1     | forum3   |
+      | forum    | forum4 | C2 forum 1  | C2     | forum4   |
+    And the following forum discussions exist in course "Course 1":
+      | user     | forum  | name        | message      | created                 |
+      | teacher1 | forum1 | discussion1 | Discussion 1 | ##2018-01-14 09:00:00## |
+      | teacher1 | forum2 | discussion2 | Discussion 2 | ##2019-03-27 12:10:00## |
+      | teacher1 | forum3 | discussion3 | Discussion 3 | ##2019-12-25 15:20:00## |
+      | teacher1 | forum3 | discussion4 | Discussion 4 | ##2019-12-26 09:30:00## |
+      | student1 | forum2 | discussion5 | Discussion 5 | ##2019-06-06 18:40:00## |
+      | student1 | forum3 | discussion6 | Discussion 6 | ##2020-01-25 11:50:00## |
+    And the following forum replies exist in course "Course 1":
+      | user     | forum  | discussion  | subject | message | created                 |
+      | teacher1 | forum1 | discussion1 | Re d1   | Reply 1 | ##2018-02-15 11:10:00## |
+      | teacher1 | forum2 | discussion5 | Re d5   | Reply 2 | ##2019-06-09 18:20:00## |
+      | teacher1 | forum2 | discussion5 | Re d5   | Reply 3 | ##2019-07-10 09:30:00## |
+      | student1 | forum1 | discussion1 | Re d1   | Reply 4 | ##2018-01-25 16:40:00## |
+      | student1 | forum2 | discussion2 | Re d6   | Reply 5 | ##2019-03-28 11:50:00## |
+      | student1 | forum3 | discussion4 | Re d4   | Reply 6 | ##2019-12-30 20:00:00## |
+    And the following forum discussions exist in course "Course 2":
+      | user     | forum  | name        | message      | created                 |
+      | teacher1 | forum4 | discussion7 | Discussion 7 | ##2020-01-29 15:00:00## |
+      | student2 | forum4 | discussion8 | Discussion 8 | ##2020-02-02 16:00:00## |
+    And the following forum replies exist in course "Course 2":
+      | user     | forum  | discussion  | subject | message | created                 |
+      | teacher1 | forum4 | discussion8 | Re d8   | Reply 7 | ##2020-02-03 09:45:00## |
+      | student2 | forum4 | discussion7 | Re d7   | Reply 8 | ##2020-02-04 13:50:00## |
+
+  Scenario: Course forum summary report can be viewed by teacher and contains accurate data
+    When I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I follow "forum2"
+    And I navigate to "Forum summary report" in current page administration
+    And I should see "Export posts"
+    And the following should exist in the "forumreport_summary_table" table:
+    # |                      | Discussions | Replies |                                    |                                   |
+      | First name / Surname | -3-         | -4-     | Earliest post                      | Most recent post                  |
+      | Student 1            | 1           |&nb