Merge branch 'MDL-68231-master' of https://github.com/snake/moodle
authorSara Arjona <sara@moodle.com>
Mon, 6 Apr 2020 11:22:18 +0000 (13:22 +0200)
committerSara Arjona <sara@moodle.com>
Mon, 6 Apr 2020 11:22:18 +0000 (13:22 +0200)
259 files changed:
admin/index.php
admin/settings/subsystems.php
admin/tasklogs.php
admin/templates/tasklogs.mustache
admin/tool/analytics/classes/output/renderer.php
admin/tool/dataprivacy/tests/privacy_provider_test.php [new file with mode: 0644]
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
admin/tool/usertours/classes/manager.php
admin/tool/usertours/db/upgrade.php
admin/tool/usertours/tests/manager_test.php
admin/tool/usertours/version.php
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/helper/async_helper.class.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/classes/local/repository/content_item_readonly_repository.php
course/classes/management_renderer.php
course/format/renderer.php
course/format/singleactivity/lib.php
course/format/singleactivity/tests/behat/edit_format_course.feature [new file with mode: 0644]
course/format/topics/renderer.php
filter/displayh5p/db/upgrade.php
filter/displayh5p/lang/en/filter_displayh5p.php
filter/displayh5p/settings.php
filter/displayh5p/version.php
grade/grading/form/guide/renderer.php
grade/report/user/renderer.php
h5p/h5plib/v124/joubel/core/h5p.classes.php
h5p/h5plib/v124/joubel/core/readme_moodle.txt
install/lang/el/error.php
install/lang/el/install.php
install/lang/pt/install.php
install/lang/sd_ap/langconfig.php [new file with mode: 0644]
lang/en/admin.php
lang/en/moodle.php
lang/en/xapi.php [new file with mode: 0644]
lib/adminlib.php
lib/authlib.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/event/database_text_field_content_replaced.php [new file with mode: 0644]
lib/classes/task/manager.php
lib/classes/task/scheduled_task.php
lib/components.json
lib/db/services.php
lib/editor/atto/plugins/align/yui/build/moodle-atto_align-button/moodle-atto_align-button-debug.js
lib/editor/atto/plugins/align/yui/build/moodle-atto_align-button/moodle-atto_align-button-min.js
lib/editor/atto/plugins/align/yui/build/moodle-atto_align-button/moodle-atto_align-button.js
lib/editor/atto/plugins/align/yui/src/button/js/button.js
lib/editor/atto/plugins/h5p/lang/en/atto_h5p.php
lib/editor/atto/plugins/h5p/lang/en/deprecated.txt [new file with mode: 0644]
lib/editor/atto/plugins/h5p/lib.php
lib/editor/atto/plugins/h5p/tests/behat/h5p.feature
lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button-debug.js
lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button-min.js
lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button.js
lib/editor/atto/plugins/h5p/yui/src/button/js/button.js
lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button-debug.js
lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button-min.js
lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button.js
lib/editor/atto/plugins/rtl/yui/src/button/js/button.js
lib/editor/atto/tests/behat/behat_editor_atto.php
lib/editor/atto/tests/behat/direction.feature [new file with mode: 0644]
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/src/editor/js/autosave.js
lib/editor/atto/yui/src/editor/js/clean.js
lib/editor/atto/yui/src/editor/js/editor.js
lib/editor/atto/yui/src/editor/js/styling.js
lib/editor/atto/yui/src/editor/js/textarea.js
lib/environmentlib.php
lib/form/templates/element-group.mustache
lib/minify/matthiasmullie-minify/src/CSS.php
lib/minify/readme_moodle.txt
lib/moodlelib.php
lib/navigationlib.php
lib/outputrenderers.php
lib/pagelib.php
lib/questionlib.php
lib/table/amd/build/dynamic.min.js [new file with mode: 0644]
lib/table/amd/build/dynamic.min.js.map [new file with mode: 0644]
lib/table/amd/build/local/dynamic/repository.min.js [new file with mode: 0644]
lib/table/amd/build/local/dynamic/repository.min.js.map [new file with mode: 0644]
lib/table/amd/build/local/dynamic/selectors.min.js [new file with mode: 0644]
lib/table/amd/build/local/dynamic/selectors.min.js.map [new file with mode: 0644]
lib/table/amd/src/dynamic.js [new file with mode: 0644]
lib/table/amd/src/local/dynamic/repository.js [new file with mode: 0644]
lib/table/amd/src/local/dynamic/selectors.js [new file with mode: 0644]
lib/table/classes/dynamic.php
lib/table/classes/external/dynamic/fetch.php [new file with mode: 0644]
lib/table/classes/local/filter/filter.php
lib/table/classes/local/filter/filterset.php
lib/table/tests/external/dynamic/fetch_test.php [new file with mode: 0644]
lib/tablelib.php
lib/templates/check/result.mustache [new file with mode: 0644]
lib/templates/check/result/critical.mustache [new file with mode: 0644]
lib/templates/check/result/error.mustache [new file with mode: 0644]
lib/templates/check/result/info.mustache [new file with mode: 0644]
lib/templates/check/result/na.mustache [new file with mode: 0644]
lib/templates/check/result/ok.mustache [new file with mode: 0644]
lib/templates/check/result/unknown.mustache [new file with mode: 0644]
lib/templates/check/result/warning.mustache [new file with mode: 0644]
lib/tests/authlib_test.php
lib/tests/check_test.php [new file with mode: 0644]
lib/tests/component_test.php
lib/tests/events_test.php
lib/tests/questionlib_test.php
lib/upgradelib.php
lib/xapi/classes/external/post_statement.php [new file with mode: 0644]
lib/xapi/classes/handler.php [new file with mode: 0644]
lib/xapi/classes/iri.php [new file with mode: 0644]
lib/xapi/classes/local/statement.php [new file with mode: 0644]
lib/xapi/classes/local/statement/item.php [new file with mode: 0644]
lib/xapi/classes/local/statement/item_activity.php [new file with mode: 0644]
lib/xapi/classes/local/statement/item_actor.php [new file with mode: 0644]
lib/xapi/classes/local/statement/item_agent.php [new file with mode: 0644]
lib/xapi/classes/local/statement/item_definition.php [new file with mode: 0644]
lib/xapi/classes/local/statement/item_group.php [new file with mode: 0644]
lib/xapi/classes/local/statement/item_object.php [new file with mode: 0644]
lib/xapi/classes/local/statement/item_verb.php [new file with mode: 0644]
lib/xapi/classes/privacy/provider.php [new file with mode: 0644]
lib/xapi/classes/xapi_exception.php [new file with mode: 0644]
lib/xapi/tests/coverage.php [new file with mode: 0644]
lib/xapi/tests/external/post_statement_test.php [new file with mode: 0644]
lib/xapi/tests/fixtures/handler.php [new file with mode: 0644]
lib/xapi/tests/fixtures/xapi_test_statement_post.php [new file with mode: 0644]
lib/xapi/tests/handler_test.php [new file with mode: 0644]
lib/xapi/tests/helper.php [new file with mode: 0644]
lib/xapi/tests/iri_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement/item_activity_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement/item_actor_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement/item_agent_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement/item_definition_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement/item_group_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement/item_object_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement/item_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement/item_verb_test.php [new file with mode: 0644]
lib/xapi/tests/local/statement_test.php [new file with mode: 0644]
login/lib.php
login/tests/lib_test.php
mod/assign/module.js
mod/assign/styles.css
mod/book/tool/print/classes/output/renderer.php
mod/book/view.php
mod/choice/renderer.php
mod/data/field/multimenu/field.class.php
mod/folder/renderer.php
mod/forum/classes/local/exporters/discussion.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/import.php
mod/lesson/renderer.php
mod/lti/edit_form.php
mod/lti/lang/en/deprecated.txt
mod/lti/lang/en/lti.php
mod/lti/locallib.php
mod/lti/service/memberships/classes/local/resources/linkmemberships.php
mod/lti/service/memberships/classes/local/service/memberships.php
mod/lti/tests/locallib_test.php
mod/url/locallib.php
mod/url/tests/lib_test.php
mod/wiki/renderer.php
mod/workshop/renderer.php
phpunit.xml.dist
privacy/export_files/general.js
question/behaviour/interactivecountback/tests/walkthrough_test.php
question/classes/bank/edit_menu_column.php
question/engine/tests/helpers.php
question/import.php
question/question.php
question/tests/bank_view_test.php
question/tests/behat/duplicate_questions.feature [moved from question/tests/behat/copy_questions.feature with 67% similarity]
question/type/ddimageortext/rendererbase.php
question/type/ddmarker/renderer.php
question/type/ddwtos/renderer.php
question/type/essay/renderer.php
question/type/match/question.php
question/type/match/questiontype.php
question/type/match/tests/backup_test.php [new file with mode: 0644]
question/type/match/tests/helper.php
question/type/match/tests/question_test.php
question/type/match/tests/questiontype_test.php
question/type/match/tests/walkthrough_test.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
theme/boost/scss/moodle/core.scss
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/boost/templates/flat_navigation.mustache
theme/classic/scss/preset/default.scss
theme/classic/style/moodle.css
user/classes/participants_table.php
user/index.php
user/lib.php
user/renderer.php
user/tests/behat/full_name_display.feature [new file with mode: 0644]
user/tests/behat/view_participants_groups.feature [new file with mode: 0644]
version.php

index c95c73c..c391089 100644 (file)
@@ -226,7 +226,7 @@ if (!core_tables_exist()) {
     }
     if (empty($confirmrelease)) {
         require_once($CFG->libdir.'/environmentlib.php');
-        list($envstatus, $environment_results) = check_moodle_environment(normalize_version($release), ENV_SELECT_RELEASE);
+        list($envstatus, $environmentresults) = check_moodle_environment(normalize_version($release), ENV_SELECT_RELEASE);
         $strcurrentrelease = get_string('currentrelease');
 
         $PAGE->navbar->add($strcurrentrelease);
@@ -235,7 +235,7 @@ if (!core_tables_exist()) {
         $PAGE->set_cacheable(false);
 
         $output = $PAGE->get_renderer('core', 'admin');
-        echo $output->install_environment_page($maturity, $envstatus, $environment_results, $release);
+        echo $output->install_environment_page($maturity, $envstatus, $environmentresults, $release);
         die();
     }
 
@@ -358,9 +358,9 @@ if (!$cache and $version > $CFG->version) {  // upgrade
         echo $output->upgrade_confirm_page($a->newversion, $maturity, $testsite);
         die();
 
-    } else if (empty($confirmrelease)){
+    } else if (empty($confirmrelease)) {
         require_once($CFG->libdir.'/environmentlib.php');
-        list($envstatus, $environment_results) = check_moodle_environment($release, ENV_SELECT_RELEASE);
+        list($envstatus, $environmentresults) = check_moodle_environment($release, ENV_SELECT_RELEASE);
         $strcurrentrelease = get_string('currentrelease');
 
         $PAGE->navbar->add($strcurrentrelease);
@@ -368,7 +368,7 @@ if (!$cache and $version > $CFG->version) {  // upgrade
         $PAGE->set_heading($strcurrentrelease);
         $PAGE->set_cacheable(false);
 
-        echo $output->upgrade_environment_page($release, $envstatus, $environment_results);
+        echo $output->upgrade_environment_page($release, $envstatus, $environmentresults);
         die();
 
     } else if (empty($confirmplugins)) {
@@ -533,7 +533,10 @@ if (!$cache and $branch <> $CFG->branch) {  // Update the branch
 
 if (!$cache and moodle_needs_upgrading()) {
 
-    $PAGE->set_url(new moodle_url($PAGE->url, array('confirmplugincheck' => $confirmplugins)));
+    $PAGE->set_url(new moodle_url($PAGE->url, array(
+        'confirmrelease' => $confirmrelease,
+        'confirmplugincheck' => $confirmplugins,
+    )));
 
     check_upgrade_key($upgradekeyhash);
 
@@ -543,7 +546,21 @@ if (!$cache and moodle_needs_upgrading()) {
         $pluginman = core_plugin_manager::instance();
         $output = $PAGE->get_renderer('core', 'admin');
 
-        if (!$confirmplugins) {
+        if (empty($confirmrelease)) {
+            require_once($CFG->libdir . '/environmentlib.php');
+
+            list($envstatus, $environmentresults) = check_moodle_environment($release, ENV_SELECT_RELEASE);
+            $strcurrentrelease = get_string('currentrelease');
+
+            $PAGE->navbar->add($strcurrentrelease);
+            $PAGE->set_title($strcurrentrelease);
+            $PAGE->set_heading($strcurrentrelease);
+            $PAGE->set_cacheable(false);
+
+            echo $output->upgrade_environment_page($release, $envstatus, $environmentresults);
+            die();
+
+        } else if (!$confirmplugins) {
             $strplugincheck = get_string('plugincheck');
 
             $PAGE->navbar->add($strplugincheck);
@@ -802,7 +819,7 @@ $SESSION->admin_critical_warning = ($insecuredataroot==INSECURE_DATAROOT_ERROR);
 $adminroot = admin_get_root();
 
 // Check if there are any new admin settings which have still yet to be set
-if (any_new_admin_settings($adminroot)){
+if (any_new_admin_settings($adminroot)) {
     redirect('upgradesettings.php');
 }
 
index 01de61c..14a1331 100644 (file)
@@ -74,4 +74,7 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
             new lang_string('configallowemojipickerincompatible', 'admin')
         ));
     }
+
+    $optionalsubsystems->add(new admin_setting_configcheckbox('enablemoodlenet', new lang_string('enablemoodlenet', 'admin'),
+        new lang_string('enablemoodlenet_desc', 'admin'), 1, 1, 0));
 }
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;
     }
diff --git a/admin/tool/dataprivacy/tests/privacy_provider_test.php b/admin/tool/dataprivacy/tests/privacy_provider_test.php
new file mode 100644 (file)
index 0000000..e671fa7
--- /dev/null
@@ -0,0 +1,187 @@
+<?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/>.
+
+/**
+ * Tests for the plugin privacy provider
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2020 Paul Holden <paulh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use core_privacy\local\request\userlist;
+use core_privacy\local\request\writer;
+use core_privacy\tests\provider_testcase;
+use tool_dataprivacy\api;
+use tool_dataprivacy\local\helper;
+use tool_dataprivacy\privacy\provider;
+
+/**
+ * Privacy provider tests
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2020 Paul Holden <paulh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_dataprivacy_privacy_provider_testcase extends provider_testcase {
+
+    /**
+     * Test provider get_contexts_for_userid method
+     *
+     * @return void
+     */
+    public function test_get_contexts_for_userid() {
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        $context = context_user::instance($user->id);
+
+        // Returned context list should contain a single item.
+        $contextlist = $this->get_contexts_for_userid($user->id, 'tool_dataprivacy');
+        $this->assertCount(1, $contextlist);
+
+        // We should have the user context of our test user.
+        $this->assertSame($context, $contextlist->current());
+    }
+
+    /**
+     * Test provider get_users_in_context method
+     *
+     * @return void
+     */
+    public function test_get_users_in_context() {
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        $context = context_user::instance($user->id);
+
+        $userlist = new userlist($context, 'tool_dataprivacy');
+        provider::get_users_in_context($userlist);
+
+        $this->assertEquals([$user->id], $userlist->get_userids());
+    }
+
+    /**
+     * Test provider get_users_in_context method for a non-user context
+     *
+     * @return void
+     */
+    public function test_get_users_in_context_non_user_context() {
+        $context = context_system::instance();
+
+        $userlist = new userlist($context, 'tool_dataprivacy');
+        provider::get_users_in_context($userlist);
+
+        $this->assertEmpty($userlist);
+    }
+
+    /**
+     * Test provider export_user_data method
+     *
+     * @return void
+     */
+    public function test_export_user_data() {
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        $context = context_user::instance($user->id);
+
+        $this->setUser($user);
+
+        // Create an export request, approve it.
+        $requestexport = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT,
+            'Please export my stuff');
+        api::update_request_status($requestexport->get('id'), api::DATAREQUEST_STATUS_APPROVED);
+
+        // Create a deletion request, reject it.
+        $requestdelete = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
+        api::update_request_status($requestdelete->get('id'), api::DATAREQUEST_STATUS_REJECTED, 0, 'Nope');
+
+        $this->export_context_data_for_user($user->id, $context, 'tool_dataprivacy');
+
+        /** @var \core_privacy\tests\request\content_writer $writer */
+        $writer = writer::with_context($context);
+        $this->assertTrue($writer->has_any_data());
+
+        /** @var stdClass[] $data */
+        $data = (array) $writer->get_data([
+            get_string('privacyandpolicies', 'admin'),
+            get_string('datarequests', 'tool_dataprivacy'),
+        ]);
+
+        $this->assertCount(2, $data);
+
+        $strs = get_strings(['requesttypeexportshort', 'requesttypedeleteshort',
+            'statusapproved', 'statusrejected', 'creationmanual'], 'tool_dataprivacy');
+
+        // First item is the approved export request.
+        $this->assertEquals($strs->requesttypeexportshort, $data[0]->type);
+        $this->assertEquals($strs->statusapproved, $data[0]->status);
+        $this->assertEquals($strs->creationmanual, $data[0]->creationmethod);
+        $this->assertEquals($requestexport->get('comments'), $data[0]->comments);
+        $this->assertEmpty($data[0]->dpocomment);
+        $this->assertNotEmpty($data[0]->timecreated);
+
+        // Next is the rejected deletion request.
+        $this->assertEquals($strs->requesttypedeleteshort, $data[1]->type);
+        $this->assertEquals($strs->statusrejected, $data[1]->status);
+        $this->assertEquals($strs->creationmanual, $data[1]->creationmethod);
+        $this->assertEmpty($data[1]->comments);
+        $this->assertContains('Nope', $data[1]->dpocomment);
+        $this->assertNotEmpty($data[1]->timecreated);
+    }
+
+    /**
+     * Test class export_user_preferences method
+     *
+     * @return void
+     */
+    public function test_export_user_preferences() {
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        // Set filters preference.
+        $filters = [
+            helper::FILTER_TYPE . ':' . api::DATAREQUEST_TYPE_EXPORT,
+            helper::FILTER_STATUS . ':' . api::DATAREQUEST_STATUS_PENDING,
+        ];
+        set_user_preference(helper::PREF_REQUEST_FILTERS, json_encode($filters), $user);
+
+        // Set paging preference.
+        set_user_preference(helper::PREF_REQUEST_PERPAGE, 6, $user);
+
+        provider::export_user_preferences($user->id);
+
+        /** @var \core_privacy\tests\request\content_writer $writer */
+        $writer = writer::with_context(context_system::instance());
+        $this->assertTrue($writer->has_any_data());
+
+        /** @var stdClass[] $preferences */
+        $preferences = (array) $writer->get_user_preferences('tool_dataprivacy');
+        $this->assertCount(2, $preferences);
+
+        $this->assertEquals((object) [
+            'value' => '1:1, 2:0',
+            'description' => 'Type: Export, Status: Pending',
+        ], $preferences[helper::PREF_REQUEST_FILTERS]);
+
+        $this->assertEquals(6, $preferences[helper::PREF_REQUEST_PERPAGE]->value);
+    }
+}
\ No newline at end of file
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"
index 40d2595..8eef2d8 100644 (file)
@@ -762,6 +762,13 @@ class manager {
      * @param   int     $direction
      */
     protected static function _move_tour(tour $tour, $direction) {
+        // We can't move the first tour higher, nor the last tour any lower.
+        if (($tour->is_first_tour() && $direction == helper::MOVE_UP) ||
+                ($tour->is_last_tour() && $direction == helper::MOVE_DOWN)) {
+
+            return;
+        }
+
         $currentsortorder   = $tour->get_sortorder();
         $targetsortorder    = $currentsortorder + $direction;
 
@@ -890,6 +897,9 @@ class manager {
         }
         $existingtourrecords->close();
 
+        // Ensure we correct the sortorder in any existing tours, prior to adding latest shipped tours.
+        helper::reset_tour_sortorder();
+
         foreach (array_reverse($shippedtours) as $filename => $version) {
             $filepath = $CFG->dirroot . "/{$CFG->admin}/tool/usertours/tours/" . $filename;
             $tourjson = file_get_contents($filepath);
index a6855ea..db3ccd8 100644 (file)
@@ -61,5 +61,12 @@ function xmldb_tool_usertours_upgrade($oldversion) {
     // Automatically generated Moodle v3.8.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2020031900) {
+        // Updating shipped tours will fix broken sortorder records in existing tours.
+        manager::update_shipped_tours();
+
+        upgrade_plugin_savepoint(true, 2020031900, 'tool', 'usertours');
+    }
+
     return true;
 }
index bffecb3..913c3e1 100644 (file)
@@ -122,6 +122,83 @@ class tool_usertours_manager_testcase extends advanced_testcase {
         $rcm->invokeArgs($manager, $arguments);
     }
 
+    /**
+     * Data provider for test_move_tour
+     *
+     * @return array
+     */
+    public function move_tour_provider() {
+        $alltours = [
+            ['name' => 'Tour 1'],
+            ['name' => 'Tour 2'],
+            ['name' => 'Tour 3'],
+        ];
+
+        return [
+            'Move up' => [
+                $alltours,
+                'Tour 2',
+                \tool_usertours\helper::MOVE_UP,
+                0,
+            ],
+            'Move down' => [
+                $alltours,
+                'Tour 2',
+                \tool_usertours\helper::MOVE_DOWN,
+                2,
+            ],
+            'Move up (first)' => [
+                $alltours,
+                'Tour 1',
+                \tool_usertours\helper::MOVE_UP,
+                0,
+            ],
+            'Move down (last)' => [
+                $alltours,
+                'Tour 3',
+                \tool_usertours\helper::MOVE_DOWN,
+                2,
+            ],
+        ];
+    }
+
+    /**
+     * Test moving tours (changing sortorder)
+     *
+     * @dataProvider move_tour_provider
+     *
+     * @param array $alltours
+     * @param string $movetourname
+     * @param int $direction
+     * @param int $expectedsortorder
+     * @return void
+     */
+    public function test_move_tour($alltours, $movetourname, $direction, $expectedsortorder) {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Clear out existing tours so ours are the only ones, otherwise we can't predict the sortorder.
+        $DB->delete_records('tool_usertours_tours');
+
+        foreach ($alltours as $tourconfig) {
+            $this->helper_create_tour((object) $tourconfig);
+        }
+
+        // Load our tour to move.
+        $record = $DB->get_record('tool_usertours_tours', ['name' => $movetourname]);
+        $tour = \tool_usertours\tour::load_from_record($record);
+
+        // Call protected method via reflection.
+        $class = new ReflectionClass(\tool_usertours\manager::class);
+        $method = $class->getMethod('_move_tour');
+        $method->setAccessible(true);
+        $method->invokeArgs(null, [$tour, $direction]);
+
+        // Assert expected sortorder.
+        $this->assertEquals($expectedsortorder, $tour->get_sortorder());
+    }
+
     /**
      * Data Provider for get_matching_tours tests.
      *
index c56c1ff..aa140f9 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2019120400;            // The current module version (Date: YYYYMMDDXX).
+$plugin->version   = 2020031900;            // The current module version (Date: YYYYMMDDXX).
 $plugin->requires  = 2019111200;            // Requires this Moodle version.
 $plugin->component = 'tool_usertours';      // Full name of the plugin (used for diagnostics).
diff --git a/auth/none/classes/check/noauth.php b/auth/none/classes/check/noauth.php
new file mode 100644 (file)
index 0000000..87384a2
--- /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 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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+        $this->id = 'noauth';
+        $this->name = get_string('check_noauth_name', 'auth_none');
+        $this->actionlink = 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('check_noauth_error', 'auth_none');
+        } else {
+            $status = result::OK;
+            $summary = get_string('check_noauth_ok', 'auth_none');
+        }
+        $details = get_string('check_noauth_details', 'auth_none');
+
+        return new result($status, $summary, $details);
+    }
+}
+
index a4ba89d..34a2a24 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['check_noauth_details'] = '<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['check_noauth_error'] = 'The No authentication plugin cannot be used on production sites.';
+$string['check_noauth_name'] = 'No authentication';
+$string['check_noauth_ok'] = '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 b36e515..309c478 100644 (file)
@@ -305,10 +305,17 @@ class async_helper  {
         $tabledata = array();
 
         // Get relevant backup ids based on context instance id.
-        $select = 'itemid = ? AND execution = ? AND status < ? AND status > ?';
-        $params = array($instanceid, backup::EXECUTION_DELAYED, backup::STATUS_FINISHED_ERR, backup::STATUS_NEED_PRECHECK);
-        $backups = $DB->get_records_select('backup_controllers', $select, $params, 'timecreated DESC', 'id, backupid, timecreated');
+        $select = 'itemid = :itemid AND execution = :execution AND status < :status1 AND status > :status2 ' .
+            'AND operation = :operation';
+        $params = [
+            'itemid' => $instanceid,
+            'execution' => backup::EXECUTION_DELAYED,
+            'status1' => backup::STATUS_FINISHED_ERR,
+            'status2' => backup::STATUS_NEED_PRECHECK,
+            'operation' => 'backup',
+        ];
 
+        $backups = $DB->get_records_select('backup_controllers', $select, $params, 'timecreated DESC', 'id, backupid, timecreated');
         foreach ($backups as $backup) {
             $bc = \backup_controller::load_controller($backup->backupid);  // Get the backup controller.
             $filename = $bc->get_plan()->get_setting('filename')->get_value();
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 8152c6d..3704b97 100644 (file)
@@ -185,7 +185,7 @@ class content_item_readonly_repository implements content_item_readonly_reposito
      * @return array the array of content items.
      */
     public function find_all(): array {
-        global $OUTPUT, $DB;
+        global $OUTPUT, $DB, $CFG;
 
         // Get all modules so we know which plugins are enabled and able to add content.
         // Only module plugins may add content items.
@@ -194,6 +194,10 @@ class content_item_readonly_repository implements content_item_readonly_reposito
 
         // Now, generate the content_items.
         foreach ($modules as $modid => $mod) {
+            // Exclude modules if the code doesn't exist.
+            if (!file_exists("$CFG->dirroot/mod/$mod->name/lib.php")) {
+                continue;
+            }
             // Create the content item for the module itself.
             // If the module chooses to implement the hook, this may be thrown away.
             $help = $this->get_core_module_help_string($mod->name);
@@ -241,7 +245,7 @@ class content_item_readonly_repository implements content_item_readonly_reposito
      * @return array the array of content_item objects
      */
     public function find_all_for_course(\stdClass $course, \stdClass $user): array {
-        global $OUTPUT, $DB;
+        global $OUTPUT, $DB, $CFG;
 
         // Get all modules so we know which plugins are enabled and able to add content.
         // Only module plugins may add content items.
@@ -253,7 +257,10 @@ class content_item_readonly_repository implements content_item_readonly_reposito
 
         // Now, generate the content_items.
         foreach ($modules as $modid => $mod) {
-
+            // Exclude modules if the code doesn't exist.
+            if (!file_exists("$CFG->dirroot/mod/$mod->name/lib.php")) {
+                continue;
+            }
             // Create the content item for the module itself.
             // If the module chooses to implement the hook, this may be thrown away.
             $help = $this->get_core_module_help_string($mod->name);
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 d3af501..88aba73 100644 (file)
@@ -154,9 +154,9 @@ class format_singleactivity extends format_base {
 
         if ($fetchtypes) {
             $availabletypes = $this->get_supported_activities();
-            if ($this->course) {
+            if ($this->courseid) {
                 // The course exists. Test against the course.
-                $testcontext = context_course::instance($this->course->id);
+                $testcontext = context_course::instance($this->courseid);
             } else if ($this->categoryid) {
                 // The course does not exist yet, but we have a category ID that we can test against.
                 $testcontext = context_coursecat::instance($this->categoryid);
diff --git a/course/format/singleactivity/tests/behat/edit_format_course.feature b/course/format/singleactivity/tests/behat/edit_format_course.feature
new file mode 100644 (file)
index 0000000..3372196
--- /dev/null
@@ -0,0 +1,27 @@
+@format @format_singleactivity
+Feature: Edit format course to Single Activity format
+  In order to set the format course to single activity course
+  As a teacher
+  I need to edit the course settings and see the dropdown type activity
+
+  Scenario: Edit a format course as a teacher
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | summary | format |
+      | Course 1 | C1 | <p>Course summary</p> | topics |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    When I navigate to "Edit settings" in current page administration
+    And I set the following fields to these values:
+      | Course full name  | My first course |
+      | Course short name | myfirstcourse |
+      | Format | Single activity format |
+    And I press "Update format"
+    Then I should see "Forum" in the "Type of activity" "field"
+    And I press "Save and display"
+    And I should see "Adding a new Forum"
\ No newline at end of file
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 97c4a3b..734f1c1 100644 (file)
@@ -45,5 +45,17 @@ function xmldb_filter_displayh5p_upgrade($oldversion) {
     // Automatically generated Moodle v3.8.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2020031700) {
+        // References to h5p.org has to be removed as default value for the allowedsources in the filter because H5P is going
+        // to close it down completely so that only the author can see the test content.
+        $h5porgurl = 'https://h5p.org/h5p/embed/[id]';
+        $config = get_config('filter_displayh5p', 'allowedsources');
+        if (strpos($config, $h5porgurl) !== false) {
+            set_config('allowedsources', str_replace($h5porgurl, '', $config), 'filter_displayh5p');
+        }
+
+        upgrade_plugin_savepoint(true, 2020031700, 'filter', 'displayh5p');
+    }
+
     return true;
 }
index dfb1f12..d8fc431 100644 (file)
@@ -27,6 +27,10 @@ defined('MOODLE_INTERNAL') || die;
 $string['allowedsourceslist'] = 'Allowed sources';
 $string['allowedsourceslistdesc'] = 'A list of URLs from which users can embed H5P content. If none are specified, all URLs will remain as links and not be displayed as embedded H5P content.
 
-\'[id]\' is a placeholder for the H5P content ID in the external source.';
+\'[id]\' is a placeholder for the H5P content ID in the external source.
+For example:
+
+- H5P.com: https://[xxxxxx].h5p.com/content/[id]
+- Wordpress: http://myserver/wp-admin/admin-ajax.php?action=h5p_embed&id=[id]';
 $string['filtername'] = 'Display H5P';
 $string['privacy:metadata'] = 'The display H5P filter does not store any personal data.';
index 3bed622..329155a 100644 (file)
@@ -30,5 +30,5 @@ if ($ADMIN->fulltree) {
             get_string('allowedsourceslist',
             'filter_displayh5p'),
             get_string('allowedsourceslistdesc', 'filter_displayh5p'),
-            "https://h5p.org/h5p/embed/[id]"));
+            ''));
 }
index 88a3038..e936737 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version  = 2019111800;
+$plugin->version  = 2020031700;
 $plugin->requires = 2019111200;
 $plugin->component = 'filter_displayh5p';
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 7654b2d..7b78d06 100644 (file)
@@ -3215,21 +3215,23 @@ class H5PCore {
    * @return string
    */
   private static function hashToken($action, $time_factor) {
-    if (!isset($_SESSION['h5p_token'])) {
+    global $SESSION;
+
+    if (!isset($SESSION->h5p_token)) {
       // Create an unique key which is used to create action tokens for this session.
       if (function_exists('random_bytes')) {
-        $_SESSION['h5p_token'] = base64_encode(random_bytes(15));
+        $SESSION->h5p_token = base64_encode(random_bytes(15));
       }
       else if (function_exists('openssl_random_pseudo_bytes')) {
-        $_SESSION['h5p_token'] = base64_encode(openssl_random_pseudo_bytes(15));
+        $SESSION->h5p_token = base64_encode(openssl_random_pseudo_bytes(15));
       }
       else {
-        $_SESSION['h5p_token'] = uniqid('', TRUE);
+        $SESSION->h5p_token = uniqid('', TRUE);
       }
     }
 
     // Create hash and return
-    return substr(hash('md5', $action . $time_factor . $_SESSION['h5p_token']), -16, 13);
+    return substr(hash('md5', $action . $time_factor . $SESSION->h5p_token), -16, 13);
   }
 
   /**
index 846d9da..85463e4 100644 (file)
@@ -16,8 +16,7 @@ Added:
 
 Downloaded version: 1.24 release
 
-
-=== 3.8 ===
+Changes:
 1. In order to allow the dependency path to be overridden by child H5PCore classes, a couple of minor changes have been added to the
 h5p.classes.php file:
     - Into the getDependenciesFiles method, the line 2435:
@@ -44,6 +43,12 @@ and 1 ocurrence in h5p-metadata.class.php.
 
 3. Another PR has been sent to H5P library (https://github.com/h5p/h5p-php-library/pull/69) to fix some php74 minor problems. The same fix is being applied locally by MDL-67077. Once we import a new version, if it includes de fix, this won't be needed to reapply and can be removed.
 
+4. Replace the $_SESSION references to $SESSION. That implies that the information is saved to backends, so only the Moodle one should be used by core (core should be free from $_SESSION and always use $SESSION).
+h5p.classes.php file:
+  - Into hashToken method:
+    Declare the global $SESSION.
+    Change all the $_SESSION by $SESSION.
+A script for testing this part can be found in MDL-68068
 
 The point 2 from above won't be needed once the mbstring extension becomes mandatory in Moodle. A request has been
-sent to MDL-65809.
+sent to MDL-65809.
\ No newline at end of file
index 6a24c91..77160aa 100644 (file)
@@ -44,7 +44,7 @@ $string['dmlexceptiononinstall'] = '<p>Παρουσιάστηκε κάποιο 
 $string['downloadedfilecheckfailed'] = 'Αποτυχία ελέγχου αρχείου που έγινε λήψη';
 $string['invalidmd5'] = 'Η μεταβλητή ελέγχου ήταν λανθασμένη - δοκιμάστε ξανά';
 $string['missingrequiredfield'] = 'Κάποιο απαιτούμενο πεδίο λείπει';
-$string['remotedownloaderror'] = '<p>Η λήψη του στοιχείου λογισμικού στον εξυπηρετητή σας απέτυχε. Παρακαλούμε επαληθεύστε τις ρυθμίσεις του διακομιστή μεσολάβησης (proxy)· η επέκταση PHP cURL συνιστάται θερμά.</p><br /><p>Πρέπει να κατεβάσετε το αρχείο <a href="{$a->url}">{$a->url}</a> χειροκίνητα, να το αντιγράψετε στο «{$a->dest}» στον εξυπηρετητή σας και να το αποσυμπιέσετε εκεί.</p>';
+$string['remotedownloaderror'] = '<p>Η λήψη του στοιχείου λογισμικού στον εξυπηρετητή σας απέτυχε. Παρακαλούμε επαληθεύστε τις ρυθμίσεις του διακομιστή μεσολάβησης· η επέκταση PHP cURL συνιστάται ιδιαίτερα.</p><br /><p>Πρέπει να κατεβάσετε το αρχείο <a href="{$a->url}">{$a->url}</a> χειροκίνητα, να το αντιγράψετε στο «{$a->dest}» στον εξυπηρετητή σας και να το αποσυμπιέσετε εκεί.</p>';
 $string['wrongdestpath'] = 'Λανθασμένο μονοπάτι προορισμού.';
 $string['wrongsourcebase'] = 'Λανθασμένη βάση πηγής URL.';
 $string['wrongzipfilename'] = 'Λανθασμένο όνομα αρχείου ZIP.';
index 18e5cd0..b810035 100644 (file)
@@ -54,7 +54,7 @@ $string['memorylimithelp'] = '<p>Το όριο μνήμης της PHP στον
 
 <p>Αυτό μπορεί να προκαλέσει προβλήματα μνήμης στο Moodle στη συνέχεια, ειδικά αν έχετε πολλά ενεργοποιημένα αρθρώματα και/ή πολλούς χρήστες.</p>
 
-<p>ΠÏ\81οÏ\84είνεται η ρύθμιση της PHP με μεγαλύτερο όριο, αν αυτό είναι δυνατό, π.χ. 40M. Υπάρχουν πολλοί τρόποι να το κάνετε αυτό, τους οποίους μπορείτε να δοκιμάσετε:</p>
+<p>ΣÏ\85νιÏ\83Ï\84άται η ρύθμιση της PHP με μεγαλύτερο όριο, αν αυτό είναι δυνατό, π.χ. 40M. Υπάρχουν πολλοί τρόποι να το κάνετε αυτό, τους οποίους μπορείτε να δοκιμάσετε:</p>
 <ol>
 <li>Αν έχετε τη δυνατότητα, κάνετε επαναμεταγλώττιση της PHP με την παράμετρο <i>--enable-memory-limit</i>. Αυτό θα επιτρέψει στο Moodle να ορίσει μόνο του το όριο μνήμης.</li>
 <li>Αν έχετε πρόσβαση στο αρχείο php.ini, μπορείτε να αλλάξετε τη ρύθμιση <b>memory_limit</b> σε 40M. Αν δεν έχετε πρόσβαση ζητήστε από το διαχειριστή να το κάνει για εσάς.</li>
index 61f701b..5d5524e 100644 (file)
@@ -34,7 +34,7 @@ $string['admindirname'] = 'Pasta de administração';
 $string['availablelangs'] = 'Pacotes linguísticos disponíveis';
 $string['chooselanguagehead'] = 'Selecione um idioma';
 $string['chooselanguagesub'] = 'Selecione o idioma a utilizar durante a instalação. Poderá depois selecionar outro(s) idioma(s) para o site e para os utilizadores.';
-$string['clialreadyconfigured'] = 'O ficheiro config.php já existe, use admin/cli/install_database.php para instalar o Moodle para este site.';
+$string['clialreadyconfigured'] = 'O ficheiro config.php já existe. Use \'admin/cli/install_database.php\' para instalar o Moodle para este site.';
 $string['clialreadyinstalled'] = 'O ficheiro config.php já existe, use admin/cli/install_database.php para atualizar o Moodle para este site.';
 $string['cliinstallheader'] = 'Programa para instalação do Moodle <b>{$a}</b> através da linha de comandos';
 $string['databasehost'] = 'Servidor da base de dados';
diff --git a/install/lang/sd_ap/langconfig.php b/install/lang/sd_ap/langconfig.php
new file mode 100644 (file)
index 0000000..c1a6e6b
--- /dev/null
@@ -0,0 +1,34 @@
+<?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/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['thisdirection'] = 'rtl';
+$string['thislanguage'] = 'سنڌي‎';
index 629745d..d4a8f67 100644 (file)
@@ -538,6 +538,8 @@ $string['enableglobalsearch_desc'] = 'If enabled, data will be indexed and synch
 $string['enablegravatar'] = 'Enable Gravatar';
 $string['enablegravatar_help'] = 'When enabled Moodle will attempt to fetch a user profile picture from Gravatar if the user has not uploaded an image.';
 $string['enablemobilewebservice'] = 'Enable web services for mobile devices';
+$string['enablemoodlenet'] = 'Enable integration with MoodleNet instances';
+$string['enablemoodlenet_desc'] = 'If enabled, and provided the MoodleNet plugin is installed, users can import content from MoodleNet into this site.';
 $string['enablerecordcache'] = 'Enable record cache';
 $string['enablerssfeeds'] = 'Enable RSS feeds';
 $string['enablesafebrowserintegration'] = 'Enable Safe Exam Browser integration';
index bec67cc..7aa080a 100644 (file)
@@ -790,6 +790,7 @@ $string['eventcourseviewed'] = 'Course viewed';
 $string['eventdashboardreset'] = 'Dashboard reset';
 $string['eventdashboardsreset'] = 'Dashboards reset';
 $string['eventdashboardviewed'] = 'Dashboard viewed';
+$string['eventdatabasetextfieldcontentreplaced'] = 'Database global search and replace';
 $string['eventemailfailed'] = 'Email failed to send';
 $string['eventname'] = 'Event name';
 $string['eventrecentactivityviewed'] = 'Recent activity viewed';
@@ -998,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';
@@ -1986,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/lang/en/xapi.php b/lang/en/xapi.php
new file mode 100644 (file)
index 0000000..5b99897
--- /dev/null
@@ -0,0 +1,26 @@
+<?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/>.
+
+/**
+ * Strings for xapi library, language 'en'
+ *
+ * @package   core_xapi
+ * @copyright 2020 Ferran Recio
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['eventxapipost'] = 'Post xAPI statement';
+$string['privacy:metadata'] = 'The xAPI library does not store any personal data.';
index 3f0dc85..1713735 100644 (file)
@@ -8873,6 +8873,17 @@ function db_replace($search, $replace) {
         echo $OUTPUT->notification("...finished", 'notifysuccess');
     }
 
+    // Trigger an event.
+    $eventargs = [
+        'context' => context_system::instance(),
+        'other' => [
+            'search' => $search,
+            'replace' => $replace
+        ]
+    ];
+    $event = \core\event\database_text_field_content_replaced::create($eventargs);
+    $event->trigger();
+
     purge_all_caches();
 
     return true;
index 1d2f972..d1604e6 100644 (file)
@@ -1027,14 +1027,25 @@ function signup_validate_data($data, $files) {
         $errors['email'] = get_string('invalidemail');
 
     } else if (empty($CFG->allowaccountssameemail)) {
-        // Make a case-insensitive query for the given email address.
-        $select = $DB->sql_equal('email', ':email', false) . ' AND mnethostid = :mnethostid';
+        // Emails in Moodle as case-insensitive and accents-sensitive. Such a combination can lead to very slow queries
+        // on some DBs such as MySQL. So we first get the list of candidate users in a subselect via more effective
+        // accent-insensitive query that can make use of the index and only then we search within that limited subset.
+        $sql = "SELECT 'x'
+                  FROM {user}
+                 WHERE " . $DB->sql_equal('email', ':email1', false, true) . "
+                   AND id IN (SELECT id
+                                FROM {user}
+                               WHERE " . $DB->sql_equal('email', ':email2', false, false) . "
+                                 AND mnethostid = :mnethostid)";
+
         $params = array(
-            'email' => $data['email'],
+            'email1' => $data['email'],
+            'email2' => $data['email'],
             'mnethostid' => $CFG->mnet_localhost_id,
         );
+
         // If there are other user(s) that already have the same email, show an error.
-        if ($DB->record_exists_select('user', $select, $params)) {
+        if ($DB->record_exists_sql($sql, $params)) {
             $forgotpasswordurl = new moodle_url('/login/forgot_password.php');
             $forgotpasswordlink = html_writer::link($forgotpasswordurl, get_string('emailexistshintlink'));
             $errors['email'] = get_string('emailexists') . ' ' . get_string('emailexistssignuphint', 'moodle', $forgotpasswordlink);
diff --git a/lib/classes/check/access/defaultuserrole.php b/lib/classes/check/access/defaultuserrole.php
new file mode 100644 (file)
index 0000000..c33f62f
--- /dev/null
@@ -0,0 +1,102 @@
+<?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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+        global $CFG;
+        $this->id = 'defaultuserrole';
+        $this->name = get_string('check_defaultuserrole_name', 'report_security');
+        $this->actionlink = 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..8defa5f
--- /dev/null
@@ -0,0 +1,102 @@
+<?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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+        $this->id = 'frontpagerole';
+        $this->name = get_string('check_frontpagerole_name', 'report_security');
+        $this->actionlink = 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..cea0038
--- /dev/null
@@ -0,0 +1,101 @@
+<?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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+
+        $this->id = 'guestrole';
+        $this->name = get_string('check_guestrole_name', 'report_security');
+        $this->actionlink = 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..9a67a25
--- /dev/null
@@ -0,0 +1,81 @@
+<?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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+        $this->id = 'riskadmin';
+        $this->name = get_string('check_riskadmin_name', 'report_security');
+        $this->actionlink = 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..6b33b24
--- /dev/null
@@ -0,0 +1,61 @@
+<?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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+
+        $this->id = 'riskbackup';
+        $this->name = get_string('check_riskbackup_name', 'report_security');
+        $this->actionlink = 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..a959d20
--- /dev/null
@@ -0,0 +1,68 @@
+<?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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+
+        $this->id = 'riskxss';
+        $this->name = get_string('check_riskxss_name', 'report_security');
+        $this->actionlink = 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..97ecfd1
--- /dev/null
@@ -0,0 +1,126 @@
+<?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';
+
+    /**
+     * @var string $id - Should be unique identifier within a component.
+     */
+    protected $id = '';
+
+    /**
+     * @var string $name - Name for the check, should be the same regardless of state.
+     */
+    protected $name = '';
+
+    /**
+     * @var action_link - an optional link to a place to address the check.
+     */
+    protected $actionlink = null;
+
+    /**
+     * 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
+     *
+     * @return string must be unique for it's component
+     */
+    public function get_id(): string {
+        return $this->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 {
+        return $this->name;
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return $this->actionlink;
+    }
+
+    /**
+     * 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..c7f6be3
--- /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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+        global $CFG;
+        $this->id = 'configrw';
+        $this->name = 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..4db5272
--- /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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+
+        $this->id = 'displayerrors';
+        $this->name = 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..1d72bf1
--- /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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+        global $CFG;
+        $this->id = 'nodemodules';
+        $this->name = 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..0452a51
--- /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 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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+
+        global $CFG;
+        $this->id = 'preventexecpath';
+        $this->name = 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..ef1ea1f
--- /dev/null
@@ -0,0 +1,82 @@
+<?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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+
+        global $CFG;
+
+        $this->id = 'unsecuredataroot';
+        $this->name = 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..6c3274c
--- /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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+        global $CFG;
+        $this->id = 'vendordir';
+        $this->name = 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..0a63cbd
--- /dev/null
@@ -0,0 +1,82 @@
+<?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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+
+        $this->id = 'cookiesecure';
+        $this->name = get_string('check_cookiesecure_name', 'report_security');
+        $this->actionlink = 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..2ed2b5b
--- /dev/null
@@ -0,0 +1,81 @@
+<?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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+
+        $this->id = 'crawlers';
+        $this->name = get_string('check_crawlers_name', 'report_security');
+        $this->actionlink = 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..6328b9e
--- /dev/null
@@ -0,0 +1,77 @@
+<?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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+        $this->id = 'emailchangeconfirmation';
+        $this->name = get_string('check_emailchangeconfirmation_name', 'report_security');
+        $this->actionlink = 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..d8e305c
--- /dev/null
@@ -0,0 +1,71 @@
+<?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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+        $this->id = 'embed';
+        $this->name = get_string('check_embed_name', 'report_security');
+        $this->actionlink = 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..38dabc3
--- /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 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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+        global $CFG;
+        $this->id = 'mediafilterswf';
+        $this->name = get_string('check_mediafilterswf_name', 'report_security');
+        $this->actionlink = 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..dc252c7
--- /dev/null
@@ -0,0 +1,72 @@
+<?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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+
+        $this->id = 'openprofiles';
+        $this->name = get_string('check_openprofiles_name', 'report_security');
+        $this->actionlink = 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..8d1000a
--- /dev/null
@@ -0,0 +1,71 @@
+<?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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+        $this->id = 'passwordpolicy';
+        $this->name = get_string('check_passwordpolicy_name', 'report_security');
+        $this->actionlink = 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..0b4f862
--- /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 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 {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+        $this->id = 'webcron';
+        $this->name = get_string('check_webcron_name', 'report_security');
+        $this->actionlink = 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);
+    }
+}
+
diff --git a/lib/classes/event/database_text_field_content_replaced.php b/lib/classes/event/database_text_field_content_replaced.php
new file mode 100644 (file)
index 0000000..ed66f1a
--- /dev/null
@@ -0,0 +1,85 @@
+<?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/>.
+
+/**
+ * The database text field content replaced event.
+ *
+ * @package   core
+ * @copyright 2020 Mark Nelson <mdjnelson@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The database text field content replaced event class.
+ *
+ * @property-read array $other {
+ *      Extra information about the event.
+ *
+ *      - string search: The value being searched for.
+ *      - string replace: The replacement value that replaces found search value.
+ * }
+ *
+ * @package   core
+ * @copyright 2020 Mark Nelson <mdjnelson@gmail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class database_text_field_content_replaced extends \core\event\base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventdatabasetextfieldcontentreplaced');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' replaced the string '" . $this->other['search'] . "' " .
+            "with the string '" . $this->other['replace'] . "' in the database.";
+    }
+
+    /**
+     * Custom validation.
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['search'])) {
+            throw new \coding_exception('The \'search\' value must be set in other.');
+        }
+        if (!isset($this->other['replace'])) {
+            throw new \coding_exception('The \'replace\' value must be set in other.');
+        }
+    }
+}
index c7f5033..b7fd89c 100644 (file)
@@ -40,9 +40,11 @@ class manager {
      * Given a component name, will load the list of tasks in the db/tasks.php file for that component.
      *
      * @param string $componentname - The name of the component to fetch the tasks for.
+     * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
+     *      If false, they are left as 'R'
      * @return \core\task\scheduled_task[] - List of scheduled tasks for this component.
      */
-    public static function load_default_scheduled_tasks_for_component($componentname) {
+    public static function load_default_scheduled_tasks_for_component($componentname, $expandr = true) {
         $dir = \core_component::get_component_directory($componentname);
 
         if (!$dir) {
@@ -65,7 +67,7 @@ class manager {
 
         foreach ($tasks as $task) {
             $record = (object) $task;
-            $scheduledtask = self::scheduled_task_from_record($record);
+            $scheduledtask = self::scheduled_task_from_record($record, $expandr);
             // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
             if ($scheduledtask) {
                 $scheduledtask->set_component($componentname);
@@ -318,9 +320,11 @@ class manager {
      * Utility method to create a task from a DB record.
      *
      * @param \stdClass $record
-     * @return \core\task\scheduled_task
+     * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
+     *      If false, they are left as 'R'
+     * @return \core\task\scheduled_task|false
      */
-    public static function scheduled_task_from_record($record) {
+    public static function scheduled_task_from_record($record, $expandr = true) {
         $classname = self::get_canonical_class_name($record->classname);
         if (!class_exists($classname)) {
             debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
@@ -342,10 +346,10 @@ class manager {
         }
         $task->set_blocking(!empty($record->blocking));
         if (isset($record->minute)) {
-            $task->set_minute($record->minute);
+            $task->set_minute($record->minute, $expandr);
         }
         if (isset($record->hour)) {
-            $task->set_hour($record->hour);
+            $task->set_hour($record->hour, $expandr);
         }
         if (isset($record->day)) {
             $task->set_day($record->day);
@@ -354,7 +358,7 @@ class manager {
             $task->set_month($record->month);
         }
         if (isset($record->dayofweek)) {
-            $task->set_day_of_week($record->dayofweek);
+            $task->set_day_of_week($record->dayofweek, $expandr);
         }
         if (isset($record->faildelay)) {
             $task->set_fail_delay($record->faildelay);
@@ -429,15 +433,18 @@ class manager {
      * This function load the default scheduled task details for a given classname.
      *
      * @param string $classname
-     * @return \core\task\scheduled_task or false
+     * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
+     *      If false, they are left as 'R'
+     * @return \core\task\scheduled_task|false
      */
-    public static function get_default_scheduled_task($classname) {
+    public static function get_default_scheduled_task($classname, $expandr = true) {
         $task = self::get_scheduled_task($classname);
         $componenttasks = array();
 
         // Safety check in case no task was found for the given classname.
         if ($task) {
-            $componenttasks = self::load_default_scheduled_tasks_for_component($task->get_component());
+            $componenttasks = self::load_default_scheduled_tasks_for_component(
+                    $task->get_component(), $expandr);
         }
 
         foreach ($componenttasks as $componenttask) {
index 42500ab..492e4c5 100644 (file)
@@ -106,9 +106,11 @@ abstract class scheduled_task extends task_base {
      * Setter for $minute. Accepts a special 'R' value
      * which will be translated to a random minute.
      * @param string $minute
+     * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
+     *      If false, they are left as 'R'
      */
-    public function set_minute($minute) {
-        if ($minute === 'R') {
+    public function set_minute($minute, $expandr = true) {
+        if ($minute === 'R' && $expandr) {
             $minute = mt_rand(self::HOURMIN, self::HOURMAX);
         }
         $this->minute = $minute;
@@ -126,9 +128,11 @@ abstract class scheduled_task extends task_base {
      * Setter for $hour. Accepts a special 'R' value
      * which will be translated to a random hour.
      * @param string $hour
+     * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
+     *      If false, they are left as 'R'
      */
-    public function set_hour($hour) {
-        if ($hour === 'R') {
+    public function set_hour($hour, $expandr = true) {
+        if ($hour === 'R' && $expandr) {
             $hour = mt_rand(self::HOURMIN, self::HOURMAX);
         }
         $this->hour = $hour;
@@ -177,9 +181,11 @@ abstract class scheduled_task extends task_base {
     /**
      * Setter for $dayofweek.
      * @param string $dayofweek
+     * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
+     *      If false, they are left as 'R'
      */
-    public function set_day_of_week($dayofweek) {
-        if ($dayofweek === 'R') {
+    public function set_day_of_week($dayofweek, $expandr = true) {
+        if ($dayofweek === 'R' && $expandr) {
             $dayofweek = mt_rand(self::DAYOFWEEKMIN, self::DAYOFWEEKMAX);
         }
         $this->dayofweek = $dayofweek;
index 1457de2..f1c2d8a 100644 (file)
         "timezones": null,
         "user": "user",
         "userkey": "lib\/userkey",
-        "webservice": "webservice"
+        "webservice": "webservice",
+        "xapi": "lib\/xapi"
     }
 }
index 9442f9c..a9c8106 100644 (file)
@@ -2744,6 +2744,24 @@ $functions = array(
         'capabilities'  => '',
         'services'      => [MOODLE_OFFICIAL_MOBILE_SERVICE],
     ],
+    'core_table_dynamic_fetch' => [
+        'classname' => 'core_table\external\dynamic\fetch',
+        'methodname' => 'execute',
+        'description' => 'Fetch a dynamic table view raw html',
+        'type' => 'read',
+        'ajax' => true,
+        'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
+    ],
+    'core_xapi_statement_post' => [
+        'classname'     => 'core_xapi\external\post_statement',
+        'methodname'    => 'execute',
+        'classpath'     => '',
+        'description'   => 'Post an xAPI statement.',
+        'type'          => 'write',
+        'ajax'          => 'true',
+        'capabilities'  => '',
+        'services'      => [MOODLE_OFFICIAL_MOBILE_SERVICE],
+    ],
 );
 
 $services = array(
index 60f5e4f..38645d7 100644 (file)
Binary files a/lib/editor/atto/plugins/align/yui/build/moodle-atto_align-button/moodle-atto_align-button-debug.js and b/lib/editor/atto/plugins/align/yui/build/moodle-atto_align-button/moodle-atto_align-button-debug.js differ
index 204f472..15107ef 100644 (file)
Binary files a/lib/editor/atto/plugins/align/yui/build/moodle-atto_align-button/moodle-atto_align-button-min.js and b/lib/editor/atto/plugins/align/yui/build/moodle-atto_align-button/moodle-atto_align-button-min.js differ
index 60f5e4f..38645d7 100644 (file)
Binary files a/lib/editor/atto/plugins/align/yui/build/moodle-atto_align-button/moodle-atto_align-button.js and b/lib/editor/atto/plugins/align/yui/build/moodle-atto_align-button/moodle-atto_align-button.js differ
index a1d62df..019699a 100644 (file)
@@ -81,6 +81,15 @@ Y.namespace('M.atto_align').Button = Y.Base.create('button', Y.M.editor_atto.Edi
 
         document.execCommand(justification, false, null);
 
+        // To clean up IE's mess.
+        this.editor.all('*[align]').each(function(node) {
+            var align = node.get('align');
+            if (align) {
+                node.setStyle('text-align', align);
+                node.removeAttribute('align');
+            }
+        }, this);
+
         // Re-disable the CSS styling after making the change.
         host.disableCssStyling();
 
index feefb27..5bac588 100644 (file)
 $string['browserepositories'] = 'Browse repositories...';
 $string['copyrightbutton'] = 'Copyright button';
 $string['downloadbutton'] = 'Allow download';
-$string['either'] = 'Either';
 $string['embedbutton'] = 'Embed button';
-$string['enterurl'] = 'URL or embed code';
 $string['h5p:addembed'] = 'Add embedded H5P';
 $string['h5pfile'] = 'H5P file upload';
+$string['h5pfileorurl'] = 'H5P URL or file upload';
 $string['h5poptions'] = 'H5P options';
-$string['h5pproperties'] = 'H5P properties';
 $string['h5purl'] = 'H5P URL';
 $string['invalidh5purl'] = 'Invalid URL';
-$string['instructions'] = 'You can insert H5P content by <strong>either</strong> entering a URL or embed code from an external H5P site <strong>or</strong> by uploading an H5P file.';
+$string['instructions'] = 'You can insert H5P content by <strong>either</strong> entering a URL <strong>or</strong> by uploading an H5P file.';
 $string['noh5pcontent'] = 'No H5P content added';
 $string['pluginname'] = 'Insert H5P';
 $string['privacy:metadata'] = 'The atto_h5p plugin does not store any personal data.';
-$string['or'] = 'or';
\ No newline at end of file
+
+// Deprecated since Moodle 3.9.
+$string['either'] = 'Either';
+$string['enterurl'] = 'URL or embed code';
+$string['h5pproperties'] = 'H5P properties';
+$string['or'] = 'or';
diff --git a/lib/editor/atto/plugins/h5p/lang/en/deprecated.txt b/lib/editor/atto/plugins/h5p/lang/en/deprecated.txt
new file mode 100644 (file)
index 0000000..baee94f
--- /dev/null
@@ -0,0 +1,4 @@
+either,atto_h5p
+enterurl,atto_h5p
+h5pproperties,atto_h5p
+or,atto_h5p
\ No newline at end of file
index 404f2e1..5aa3457 100644 (file)
@@ -69,16 +69,13 @@ function atto_h5p_strings_for_js() {
         'copyrightbutton',
         'downloadbutton',
         'instructions',
-        'either',
         'embedbutton',
-        'enterurl',
         'h5pfile',
         'h5poptions',
-        'h5pproperties',
         'h5purl',
+        'h5pfileorurl',
         'invalidh5purl',
         'noh5pcontent',
-        'or',
         'pluginname'
     );
 
index fc51cf4..5e224f0 100644 (file)
@@ -17,7 +17,7 @@ Feature: Add h5ps to Atto
       | page     | PageName1  | PageDesc1  | 1           | C1     | H5Ptest  | 1             | 1        |
     And the "displayh5p" filter is "on"
     And the following config values are set as admin:
-      | allowedsources | https://moodle.h5p.com/content/[id]/embed | filter_displayh5p |
+      | allowedsources | https://moodle.h5p.com/content/[id] | filter_displayh5p |
 
   @javascript @external
   Scenario: Insert an embedded h5p
@@ -27,7 +27,7 @@ Feature: Add h5ps to Atto
     And I follow "PageName1"
     And I navigate to "Edit settings" in current page administration
     And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
-    And I set the field with xpath "//textarea[@data-region='h5purl']" to "https://moodle.h5p.com/content/1290772960722742119/embed"
+    And I set the field with xpath "//input[@data-region='h5pfile']" to "https://moodle.h5p.com/content/1290772960722742119"
     And I click on "Insert H5P" "button" in the "Insert H5P" "dialogue"
     And I wait until the page is ready
     When I click on "Save and display" "button"
@@ -64,7 +64,7 @@ Feature: Add h5ps to Atto
     And I navigate to "Edit settings" in current page administration
     And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
 #   This is not a real external URL, so this scenario shouldn't be labeled as external.
-    And I set the field with xpath "//textarea[@data-region='h5purl']" to "ftp://moodle.h5p.com/content/1290772960722742119/embed"
+    And I set the field with xpath "//input[@data-region='h5pfile']" to "ftp://moodle.h5p.com/content/1290772960722742119"
     When I click on "Insert H5P" "button" in the "Insert H5P" "dialogue"
     And I wait until the page is ready
     Then I should see "Invalid URL" in the "Insert H5P" "dialogue"
@@ -91,7 +91,9 @@ Feature: Add h5ps to Atto
     And I follow "PageName1"
     When I navigate to "Edit settings" in current page administration
     And I click on "Insert H5P" "button"
-    Then I should not see "URL or embed code" in the "Insert H5P" "dialogue"
+    Then I should not see "H5P URL" in the "Insert H5P" "dialogue"
+    And I should see "H5P file upload" in the "Insert H5P" "dialogue"
+    And I should see "H5P options" in the "Insert H5P" "dialogue"
 
   @javascript
   Scenario: No upload h5p capabilities
@@ -104,6 +106,8 @@ Feature: Add h5ps to Atto
     When I navigate to "Edit settings" in current page administration
     And I click on "Insert H5P" "button"
     Then I should not see "H5P file upload" in the "Insert H5P" "dialogue"
+    And I should see "H5P URL" in the "Insert H5P" "dialogue"
+    And I should not see "H5P options" in the "Insert H5P" "dialogue"
 
   @javascript @external
   Scenario: Edit H5P content
@@ -132,7 +136,7 @@ Feature: Add h5ps to Atto
     And I click on ".h5p-placeholder" "css_element"
     And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
 #   External URL
-    And I set the field with xpath "//textarea[@data-region='h5purl']" to "https://moodle.h5p.com/content/1290772960722742119/embed"
+    And I set the field with xpath "//input[@data-region='h5pfile']" to "https://moodle.h5p.com/content/1290772960722742119"
     And I click on "Insert H5P" "button" in the "Insert H5P" "dialogue"
     And I wait until the page is ready
     And I click on "Save and display" "button"
@@ -199,6 +203,32 @@ Feature: Add h5ps to Atto
     And I should see "Embed"
     And I should see "Rights of use"
 
+  @javascript @external
+  Scenario: H5P options are ignored for H5P URLs
+    Given I log in as "admin"
+    And I change window size to "large"
+    And I am on "Course 1" course homepage
+    And I follow "PageName1"
+    And I navigate to "Edit settings" in current page administration
+    And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
+    And I set the field with xpath "//input[@data-region='h5pfile']" to "https://moodle.h5p.com/content/1290752078589054689"
+    And I click on "H5P options" "link"
+    And I click on "Embed button" "checkbox"
+    And I click on "Insert H5P" "button" in the "Insert H5P" "dialogue"
+    And I wait until the page is ready
+    When I click on "Save and display" "button"
+    Then ".h5p-placeholder" "css_element" should exist
+    And I wait until the page is ready
+    And I switch to "h5pcontent" iframe
+    And I should see "History of strawberries"
+    And I should not see "Embed"
+    And I switch to the main frame
+    And I navigate to "Edit settings" in current page administration
+    And I click on ".h5p-placeholder" "css_element"
+    And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
+    And I click on "H5P options" "link"
+    And "input[aria-label=\"Embed button\"]:not([checked=checked])" "css_element" should exist
+
   @javascript
   Scenario: Private H5P files are shown to students
     Given the following "users" exist:
index 29ed69b..98d1d42 100644 (file)
Binary files a/lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button-debug.js and b/lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button-debug.js differ
index 41c5f75..7fe48a7 100644 (file)
Binary files a/lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button-min.js and b/lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button-min.js differ
index 29ed69b..98d1d42 100644 (file)
Binary files a/lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button.js and b/lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button.js differ
index 4b753ba..2906734 100644 (file)
@@ -36,7 +36,6 @@ var CSS = {
         H5PBROWSER: 'openh5pbrowser',
         INPUTALT: 'atto_h5p_altentry',
         INPUTH5PFILE: 'atto_h5p_file',
-        INPUTH5PURL: 'atto_h5p_url',
         INPUTSUBMIT: 'atto_h5p_urlentrysubmit',
         OPTION_DOWNLOAD_BUTTON: 'atto_h5p_option_download_button',
         OPTION_COPYRIGHT_BUTTON: 'atto_h5p_option_copyright_button',
@@ -47,7 +46,6 @@ var CSS = {
         CONTENTWARNING: '.' + CSS.CONTENTWARNING,
         H5PBROWSER: '.' + CSS.H5PBROWSER,
         INPUTH5PFILE: '.' + CSS.INPUTH5PFILE,
-        INPUTH5PURL: '.' + CSS.INPUTH5PURL,
         INPUTSUBMIT: '.' + CSS.INPUTSUBMIT,
         OPTION_DOWNLOAD_BUTTON: '.' + CSS.OPTION_DOWNLOAD_BUTTON,
         OPTION_COPYRIGHT_BUTTON: '.' + CSS.OPTION_COPYRIGHT_BUTTON,
@@ -62,65 +60,71 @@ var CSS = {
                 '<div style="display:none" role="alert" class="alert alert-warning mb-1 {{CSS.CONTENTWARNING}}">' +
                     '{{get_string "noh5pcontent" component}}' +
                 '</div>' +
-                '{{#if canUploadAndEmbed}}' +
-                    '<div class="mt-2 attoh5pinstructions">{{{get_string "instructions" component}}}</div>' +
-                    '<div class="my-2"><strong>{{get_string "either" component}}</strong></div>' +
-                '{{/if}}' +
-                '{{#if canEmbed}}' +
-                '<div class="mb-4">' +
-                    '<label for="{{elementid}}_{{CSS.INPUTH5PURL}}">{{get_string "enterurl" component}}</label>' +
-                    '<div style="display:none" role="alert" class="alert alert-warning mb-1 {{CSS.URLWARNING}}">' +
-                        '{{get_string "invalidh5purl" component}}' +
-                    '</div>' +
-                    '<textarea rows="3" data-region="h5purl" class="form-control {{CSS.INPUTH5PURL}}" type="url" ' +
-                    'id="{{elementid}}_{{CSS.INPUTH5PURL}}" />{{embedURL}}</textarea>' +
+                '<div style="display:none" role="alert" class="alert alert-warning mb-1 {{CSS.URLWARNING}}">' +
+                    '{{get_string "invalidh5purl" component}}' +
                 '</div>' +
-                '{{/if}}' +
                 '{{#if canUploadAndEmbed}}' +
-                    '<div class="my-2"><strong>{{get_string "or" component}}</strong></div>' +
+                    '<div class="mt-2 mb-4 attoh5pinstructions">{{{get_string "instructions" component}}}</div>' +
                 '{{/if}}' +
-                '{{#if canUpload}}' +
                 '<div class="mb-4">' +
-                    '<label for="{{elementid}}_{{CSS.H5PBROWSER}}">{{get_string "h5pfile" component}}</label>' +
+                    '<label for="{{elementid}}_{{CSS.H5PBROWSER}}">' +
+                        '{{#if canUploadAndEmbed}}' +
+                            '{{get_string "h5pfileorurl" component}}' +
+                        '{{/if}}' +
+                        '{{^if canUploadAndEmbed}}' +
+                            '{{#if canUpload}}' +
+                                '{{get_string "h5pfile" component}}' +
+                            '{{/if}}' +
+                            '{{#if canEmbed}}' +
+                                '{{get_string "h5purl" component}}' +
+                            '{{/if}}' +
+                        '{{/if}}' +
+                    '</label>' +
                     '<div class="input-group input-append w-100">' +
                         '<input class="form-control {{CSS.INPUTH5PFILE}}" type="url" value="{{fileURL}}" ' +
-                        'id="{{elementid}}_{{CSS.INPUTH5PFILE}}" size="32"/>' +
-                        '<span class="input-group-append">' +
-                            '<button class="btn btn-secondary {{CSS.H5PBROWSER}}" type="button">' +
-                            '{{get_string "browserepositories" component}}</button>' +
-                        '</span>' +
+                        'id="{{elementid}}_{{CSS.INPUTH5PFILE}}" data-region="h5pfile" size="32"/>' +
+                        '{{#if canUpload}}' +
+                            '<span class="input-group-append">' +
+                                '<button class="btn btn-secondary {{CSS.H5PBROWSER}}" type="button">' +
+                                '{{get_string "browserepositories" component}}</button>' +
+                            '</span>' +
+                        '{{/if}}' +
                     '</div>' +
-                    '<fieldset class="collapsible {{#if collapseOptions}}collapsed{{/if}}" id="{{elementid}}_h5poptions">' +
-                        '<legend class="ftoggler">{{get_string "h5poptions" component}}</legend>' +
-                        '<div class="fcontainer">' +
-                            '<div class="form-check">' +
-                                '<input type="checkbox" {{optionDownloadButton}} ' +
-                                'class="form-check-input {{CSS.OPTION_DOWNLOAD_BUTTON}}"' +
-                                'id="{{elementid}}_h5p-option-allow-download"/>' +
-                                '<label class="form-check-label" for="{{elementid}}_h5p-option-allow-download">' +
-                                '{{get_string "downloadbutton" component}}' +
-                                '</label>' +
-                            '</div>' +
-                            '<div class="form-check">' +
-                                '<input type="checkbox" {{optionEmbedButton}} ' +
-                                'class="form-check-input {{CSS.OPTION_EMBED_BUTTON}}" ' +
-                                    'id="{{elementid}}_h5p-option-embed-button"/>' +
-                                '<label class="form-check-label" for="{{elementid}}_h5p-option-embed-button">' +
-                                '{{get_string "embedbutton" component}}' +
-                                '</label>' +
-                            '</div>' +
-                            '<div class="form-check mb-2">' +
-                                '<input type="checkbox" {{optionCopyrightButton}} ' +
-                                'class="form-check-input {{CSS.OPTION_COPYRIGHT_BUTTON}}" ' +
-                                    'id="{{elementid}}_h5p-option-copyright-button"/>' +
-                                '<label class="form-check-label" for="{{elementid}}_h5p-option-copyright-button">' +
-                                '{{get_string "copyrightbutton" component}}' +
-                                '</label>' +
+                    '{{#if canUpload}}' +
+                        '<fieldset class="collapsible {{#if collapseOptions}}collapsed{{/if}}" id="{{elementid}}_h5poptions">' +
+                            '<legend class="ftoggler">{{get_string "h5poptions" component}}</legend>' +
+                            '<div class="fcontainer">' +
+                                '<div class="form-check">' +
+                                    '<input type="checkbox" {{optionDownloadButton}} ' +
+                                    'class="form-check-input {{CSS.OPTION_DOWNLOAD_BUTTON}}"' +
+                                    'aria-label="{{get_string "downloadbutton" component}}" ' +
+                                    'id="{{elementid}}_h5p-option-allow-download"/>' +
+                                    '<label class="form-check-label" for="{{elementid}}_h5p-option-allow-download">' +
+                                    '{{get_string "downloadbutton" component}}' +
+                                    '</label>' +
+                                '</div>' +
+                                '<div class="form-check">' +
+                                    '<input type="checkbox" {{optionEmbedButton}} ' +
+                                    'class="form-check-input {{CSS.OPTION_EMBED_BUTTON}}" ' +
+                                    'aria-label="{{get_string "embedbutton" component}}" ' +
+                                        'id="{{elementid}}_h5p-option-embed-button"/>' +
+                                    '<label class="form-check-label" for="{{elementid}}_h5p-option-embed-button">' +
+                                    '{{get_string "embedbutton" component}}' +
+                                    '</label>' +
+                                '</div>' +
+                                '<div class="form-check mb-2">' +
+                                    '<input type="checkbox" {{optionCopyrightButton}} ' +
+                                    'class="form-check-input {{CSS.OPTION_COPYRIGHT_BUTTON}}" ' +
+                                    'aria-label="{{get_string "copyrightbutton" component}}" ' +
+                                        'id="{{elementid}}_h5p-option-copyright-button"/>' +
+                                    '<label class="form-check-label" for="{{elementid}}_h5p-option-copyright-button">' +
+                                    '{{get_string "copyrightbutton" component}}' +
+                                    '</label>' +
+                                '</div>' +
                             '</div>' +
-                        '</div>' +
-                    '</fieldset>' +
+                        '</fieldset>' +
+                    '{{/if}}' +
                 '</div>' +
-                '{{/if}}' +
                 '<div class="text-center">' +
                 '<button class="btn btn-secondary {{CSS.INPUTSUBMIT}}" type="submit">' + '' +
                     '{{get_string "pluginname" component}}</button>' +
@@ -268,9 +272,9 @@ Y.namespace('M.atto_h5p').Button = Y.Base.create('button', Y.M.editor_atto.Edito
      */
     _getPermissions: function() {
         var permissions = {
+            'canEmbed': false,
             'canUpload': false,
-            'canUploadAndEbmed': false,
-            'canEmbed': false
+            'canUploadAndEmbed': false
         };
 
         if (this.get('host').canShowFilepicker('h5p')) {
@@ -302,7 +306,6 @@ Y.namespace('M.atto_h5p').Button = Y.Base.create('button', Y.M.editor_atto.Edito
         var permissions = this._getPermissions();
 
         var fileURL,
-            embedURL,
             optionDownloadButton,
             optionEmbedButton,
             optionCopyrightButton,
@@ -332,7 +335,7 @@ Y.namespace('M.atto_h5p').Button = Y.Base.create('button', Y.M.editor_atto.Edito
                     }
                 }
             } else {
-                embedURL = H5PURL;
+                fileURL = H5PURL;
             }
         }
 
@@ -343,10 +346,9 @@ Y.namespace('M.atto_h5p').Button = Y.Base.create('button', Y.M.editor_atto.Edito
                 component: COMPONENTNAME,
                 canUpload: permissions.canUpload,
                 canEmbed: permissions.canEmbed,
-                fileURL: fileURL,
-                embedURL: embedURL,
                 canUploadAndEmbed: permissions.canUploadAndEmbed,
                 collapseOptions: collapseOptions,
+                fileURL: fileURL,
                 optionDownloadButton: optionDownloadButton,
                 optionEmbedButton: optionEmbedButton,
                 optionCopyrightButton: optionCopyrightButton
@@ -372,7 +374,6 @@ Y.namespace('M.atto_h5p').Button = Y.Base.create('button', Y.M.editor_atto.Edito
         if (params.url !== '') {
             var input = this._form.one(SELECTORS.INPUTH5PFILE);
             input.set('value', params.url);
-            this._form.one(SELECTORS.INPUTH5PURL).set('value', '');
             this._removeWarnings();
         }
     },
@@ -397,11 +398,6 @@ Y.namespace('M.atto_h5p').Button = Y.Base.create('button', Y.M.editor_atto.Edito
 
         if (permissions.canUploadAndEmbed) {
             form.one(SELECTORS.INPUTH5PFILE).on('change', function() {
-                form.one(SELECTORS.INPUTH5PURL).set('value', '');
-                this._removeWarnings();
-            }, this);
-            form.one(SELECTORS.INPUTH5PURL).on('change', function() {
-                form.one(SELECTORS.INPUTH5PFILE).set('value', '');
                 this._removeWarnings();
             }, this);
         }
@@ -421,7 +417,6 @@ Y.namespace('M.atto_h5p').Button = Y.Base.create('button', Y.M.editor_atto.Edito
 
     /**
      * Update the h5p in the contenteditable.
-
      *
      * @method _setH5P
      * @param {EventFacade} e
@@ -429,20 +424,11 @@ Y.namespace('M.atto_h5p').Button = Y.Base.create('button', Y.M.editor_atto.Edito
      */
     _setH5P: function(e) {
         var form = this._form,
-            url = form.one(SELECTORS.INPUTH5PURL).get('value'),
             h5phtml,
             host = this.get('host'),
-            h5pfile,
+            h5pfile = form.one(SELECTORS.INPUTH5PFILE).get('value'),
             permissions = this._getPermissions();
 
-        if (permissions.canEmbed) {
-            url = form.one(SELECTORS.INPUTH5PURL).get('value');
-        }
-
-        if (permissions.canUpload) {
-            h5pfile = form.one(SELECTORS.INPUTH5PFILE).get('value');
-        }
-
         e.preventDefault();
 
         // Check if there are any issues.
@@ -462,68 +448,49 @@ Y.namespace('M.atto_h5p').Button = Y.Base.create('button', Y.M.editor_atto.Edito
             addParagraphs = false;
         }
 
-        if (url !== '') {
-
+        if (h5pfile !== '') {
             host.setSelection(this._currentSelection);
 
-            if (this._validEmbed(url)) {
-                var embedtemplate = Y.Handlebars.compile(H5PTEMPLATE);
-                var regex = /<iframe.*?src="(.*?)".*<\/iframe>/;
-                var src = url.match(regex)[1];
+            if (h5pfile.startsWith(M.cfg.wwwroot)) {
+                // It's a local file.
+                var params = '';
+                if (permissions.canUpload) {
+                    var options = {};
+                    if (form.one(SELECTORS.OPTION_DOWNLOAD_BUTTON).get('checked')) {
+                        options['export'] = '1';
+                    }
+                    if (form.one(SELECTORS.OPTION_EMBED_BUTTON).get('checked')) {
+                        options.embed = '1';
+                    }
+                    if (form.one(SELECTORS.OPTION_COPYRIGHT_BUTTON).get('checked')) {
+                        options.copyright = '1';
+                    }
 
-                // In case a local H5P embed code is used we need get the url
-                // param form the src and decode it.
-                if (src.startsWith(M.cfg.wwwroot + '/h5p/embed.php')) {
-                    src = decodeURIComponent(src.split("url=")[1]);
+                    for (var opt in options) {
+                        if (params === "" && (h5pfile.indexOf("?") === -1)) {
+                            params += "?";
+                        } else {
+                            params += "&amp;";
+                        }
+                        params += opt + "=" + options[opt];
+                    }
                 }
 
-                h5phtml = embedtemplate({
-                    url: src
+                var h5ptemplate = Y.Handlebars.compile(H5PTEMPLATE);
+
+                h5phtml = h5ptemplate({
+                    url: h5pfile + params,
+                    addParagraphs: addParagraphs
                 });
             } else {
+                // It's a URL.
                 var urltemplate = Y.Handlebars.compile(H5PTEMPLATE);
                 h5phtml = urltemplate({
-                    url: url
+                    url: h5pfile
                 });
             }
 
-            this.get('host').insertContentAtFocusPoint(h5phtml);
-
-            this.markUpdated();
-        } else if (h5pfile !== '') {
-
-            host.setSelection(this._currentSelection);
-
-            var options = {};
-
-            if (form.one(SELECTORS.OPTION_DOWNLOAD_BUTTON).get('checked')) {
-                options['export'] = '1';
-            }
-            if (form.one(SELECTORS.OPTION_EMBED_BUTTON).get('checked')) {
-                options.embed = '1';
-            }
-            if (form.one(SELECTORS.OPTION_COPYRIGHT_BUTTON).get('checked')) {
-                options.copyright = '1';
-            }
-
-            var params = "";
-            for (var opt in options) {
-                if (params === "" && (h5pfile.indexOf("?") === -1)) {
-                    params += "?";
-                } else {
-                    params += "&amp;";
-                }
-                params += opt + "=" + options[opt];
-            }
-
-            var h5ptemplate = Y.Handlebars.compile(H5PTEMPLATE);
-
-            h5phtml = h5ptemplate({
-                url: h5pfile + params,
-                addParagraphs: addParagraphs
-            });
-
-            this.get('host').insertContentAtFocusPoint(h5phtml);
+            host.insertContentAtFocusPoint(h5phtml);
 
             this.markUpdated();
         }
@@ -572,30 +539,21 @@ Y.namespace('M.atto_h5p').Button = Y.Base.create('button', Y.M.editor_atto.Edito
     _updateWarning: function() {
         var form = this._form,
             state = true,
-            url,
             h5pfile,
             permissions = this._getPermissions();
 
-
-        if (permissions.canEmbed) {
-            url = form.one(SELECTORS.INPUTH5PURL).get('value');
-            if (url !== '') {
-                if (this._validURL(url) || this._validEmbed(url)) {
+        if (permissions.canUpload || permissions.canEmbed) {
+            h5pfile = form.one(SELECTORS.INPUTH5PFILE).get('value');
+            if (h5pfile !== '') {
+                form.one(SELECTORS.CONTENTWARNING).setStyle('display', 'none');
+                if (h5pfile.startsWith(M.cfg.wwwroot) || this._validURL(h5pfile)) {
+                    // Only external URLs have to be validated.
                     form.one(SELECTORS.URLWARNING).setStyle('display', 'none');
                     state = false;
                 } else {
                     form.one(SELECTORS.URLWARNING).setStyle('display', 'block');
                     state = true;
                 }
-                return state;
-            }
-        }
-
-        if (permissions.canUpload) {
-            h5pfile = form.one(SELECTORS.INPUTH5PFILE).get('value');
-            if (h5pfile !== '') {
-                form.one(SELECTORS.CONTENTWARNING).setStyle('display', 'none');
-                state = false;
             } else {
                 form.one(SELECTORS.CONTENTWARNING).setStyle('display', 'block');
                 state = true;
index 10f02f5..1a7e8f1 100644 (file)
Binary files a/lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button-debug.js and b/lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button-debug.js differ
index 687aea1..b74d110 100644 (file)
Binary files a/lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button-min.js and b/lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button-min.js differ
index 10f02f5..1a7e8f1 100644 (file)
Binary files a/lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button.js and b/lib/editor/atto/plugins/rtl/yui/build/moodle-atto_rtl-button/moodle-atto_rtl-button.js differ
index 724c21d..7cca17c 100644 (file)
@@ -70,6 +70,10 @@ Y.namespace('M.atto_rtl').Button = Y.Base.create('button', Y.M.editor_atto.Edito
             newDirection = {
                 rtl: 'ltr',
                 ltr: 'rtl'
+            },
+            directionAlignment = {
+                rtl: 'right',
+                ltr: 'left'
             };
         if (selection) {
             // Format the selection to be sure it has a tag parent (not the contenteditable).
@@ -79,8 +83,10 @@ Y.namespace('M.atto_rtl').Button = Y.Base.create('button', Y.M.editor_atto.Edito
             var currentDirection = parentDOMNode.getAttribute('dir');
             if (currentDirection === direction) {
                 parentDOMNode.setAttribute("dir", newDirection[direction]);
+                parentDOMNode.style.textAlign = directionAlignment[newDirection[direction]];
             } else {
                 parentDOMNode.setAttribute("dir", direction);
+                parentDOMNode.style.textAlign = directionAlignment[direction];
             }
 
             // Change selection from the containing paragraph to the original one.
index 91723d8..94c9d5a 100644 (file)
@@ -25,6 +25,8 @@
 
 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
 
+require_once(__DIR__ . '/../../../../behat/behat_base.php');
+
 /**
  * Steps definitions to deal with the atto text editor
  *
diff --git a/lib/editor/atto/tests/behat/direction.feature b/lib/editor/atto/tests/behat/direction.feature
new file mode 100644 (file)
index 0000000..6009727
--- /dev/null
@@ -0,0 +1,49 @@
+@editor @editor_atto @atto
+Feature: Add text direction and alignment
+  In order to generate a content that can be displayed in the proper direction to everyone
+  As a user
+  I should see the Atto editor with explicit direction and alignment being set
+
+  Background:
+    Given the following "user preferences" exist:
+      | user  | preference  | value |
+      | admin | htmleditor  | atto  |
+    And I log in as "admin"
+    And I navigate to "Plugins > Text editors > Atto HTML editor > Atto toolbar settings" in site administration
+    And I set the field "Toolbar config" to multiline:
+    """
+    collapse = collapse
+    style1 = title, bold, italic
+    list = unorderedlist, orderedlist
+    links = link
+    files = image, media, recordrtc, managefiles, h5p
+    style2 = underline, strike, subscript, superscript
+    align = align,rtl
+    indent = indent
+    insert = equation, charmap, table, clear
+    undo = undo
+    accessibility = accessibilitychecker, accessibilityhelper
+    other = html
+    """
+    And I press "Save changes"
+    And I log out
+
+  @javascript
+  Scenario Outline: Atto should apply user's direction and alignment by default
+    Given the following "courses" exist:
+      | fullname  | shortname | summary | summaryformat |
+      | Course 1  | C1        |         | 1             |
+    And the following "language customisations" exist:
+      | component   | stringid    | value         |
+      | <component> | <stringid>  | <localstring> |
+    And I log in as "admin"
+    And I am on "Course 1" course homepage
+    When I navigate to "Edit settings" in current page administration
+    And I press "Show more buttons"
+    And I press "HTML"
+    Then I should see "<partialtext>"
+
+    Examples:
+      | component       | stringid      | localstring | partialtext                               |
+      | core_langconfig | thisdirection | ltr         | dir=\"ltr\" style=\"text-align: left;\"   |
+      | core_langconfig | thisdirection | rtl         | dir=\"rtl\" style=\"text-align: right;\"  |
index f24e521..46360b7 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js differ
index 7b7b5be..7c68071 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js differ
index c373535..eb50741 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js differ
index aed2fb8..60b95cb 100644 (file)
@@ -143,14 +143,24 @@ EditorAutosave.prototype = {
                 }
 
                 // Revert untouched editor contents to an empty string.
-                // Check for FF and Chrome.
-                if (response.result === '<p></p>' || response.result === '<p><br></p>' ||
-                    response.result === '<br>') {
-                    response.result = '';
-                }
-
-                // Check for IE 9 and 10.
-                if (response.result === '<p>&nbsp;</p>' || response.result === '<p><br>&nbsp;</p>') {
+                var emptyContents = [
+                    // For FF and Chrome.
+                    '<p></p>',
+                    '<p><br></p>',
+                    '<br>',
+                    '<p dir="rtl" style="text-align: right;"></p>',
+                    '<p dir="rtl" style="text-align: right;"><br></p>',
+                    '<p dir="ltr" style="text-align: left;"></p>',
+                    '<p dir="ltr" style="text-align: left;"><br></p>',
+                    // For IE 9 and 10.
+                    '<p>&nbsp;</p>',
+                    '<p><br>&nbsp;</p>',
+                    '<p dir="rtl" style="text-align: right;">&nbsp;</p>',
+                    '<p dir="rtl" style="text-align: right;"><br>&nbsp;</p>',
+                    '<p dir="ltr" style="text-align: left;">&nbsp;</p>',
+                    '<p dir="ltr" style="text-align: left;"><br>&nbsp;</p>'
+                ];
+                if (emptyContents.includes(response.result)) {
                     response.result = '';
                 }
 
index cfb35e7..a3310f0 100644 (file)
@@ -55,12 +55,29 @@ EditorClean.prototype = {
         html = editorClone.get('innerHTML');
 
         // Revert untouched editor contents to an empty string.
-        if (html === '<p></p>' || html === '<p><br></p>') {
+        var emptyContents = [
+            // For FF and Chrome.
+            '<p></p>',
+            '<p><br></p>',
+            '<br>',
+            '<p dir="rtl" style="text-align: right;"></p>',
+            '<p dir="rtl" style="text-align: right;"><br></p>',
+            '<p dir="ltr" style="text-align: left;"></p>',
+            '<p dir="ltr" style="text-align: left;"><br></p>',
+            // For IE 9 and 10.
+            '<p>&nbsp;</p>',
+            '<p><br>&nbsp;</p>',
+            '<p dir="rtl" style="text-align: right;">&nbsp;</p>',
+            '<p dir="rtl" style="text-align: right;"><br>&nbsp;</p>',
+            '<p dir="ltr" style="text-align: left;">&nbsp;</p>',
+            '<p dir="ltr" style="text-align: left;"><br>&nbsp;</p>'
+        ];
+        if (emptyContents.includes(html)) {
             return '';
         }
 
         // Remove any and all nasties from source.
-       return this._cleanHTML(html);
+        return this._cleanHTML(html);
     },
 
     /**
index b846bcd..d085283 100644 (file)
@@ -148,6 +148,14 @@ Y.extend(Editor, Y.Base, {
      */
     plugins: null,
 
+    /**
+     * An indicator of the current input direction.
+     *
+     * @property coreDirection
+     * @type string
+     */
+    coreDirection: null,
+
     /**
      * Event Handles to clear on editor destruction.
      *
@@ -194,6 +202,9 @@ Y.extend(Editor, Y.Base, {
             this.editor.setAttribute('aria-labelledby', this.textareaLabel.get("id"));
         }
 
+        // Set diretcion according to current page language.
+        this.coreDirection = Y.one('body').hasClass('dir-rtl') ? 'rtl' : 'ltr';
+
         // Add everything to the wrapper.
         this.setupToolbar();
 
index dcf6fb8..66c91a9 100644 (file)
@@ -175,8 +175,14 @@ EditorStyling.prototype = {
 
         // No valid block element - make one.
         if (!nearestblock) {
+            var alignment;
+            if (this.coreDirection === 'rtl') {
+                alignment = 'style="text-align: right;"';
+            } else {
+                alignment = 'style="text-align: left;"';
+            }
             // There is no block node in the content, wrap the content in a p and use that.
-            newcontent = Y.Node.create('<p></p>');
+            newcontent = Y.Node.create('<p dir="' + this.coreDirection + '" ' + alignment + '></p>');
             boundary.get('childNodes').each(function(child) {
                 newcontent.append(child.remove());
             });
index 1e24287..0670ad9 100644 (file)
@@ -45,10 +45,16 @@ EditorTextArea.prototype = {
      * @private
      */
     _getEmptyContent: function() {
+        var alignment;
+        if (this.coreDirection === 'rtl') {
+            alignment = 'style="text-align: right;"';
+        } else {
+            alignment = 'style="text-align: left;"';
+        }
         if (Y.UA.ie && Y.UA.ie < 10) {
-            return '<p></p>';
+            return '<p dir="' + this.coreDirection + '" ' + alignment + '></p>';
         } else {
-            return '<p><br></p>';
+            return '<p dir="' + this.coreDirection + '" ' + alignment + '><br></p>';
         }
     },
 
index 3daebae..64a3a4f 100644 (file)
@@ -140,6 +140,7 @@ function environment_get_errors($environment_results) {
         $type = $environment_result->getPart();
         $info = $environment_result->getInfo();
         $status = $environment_result->getStatus();
+        $plugin = $environment_result->getPluginName();
         $error_code = $environment_result->getErrorCode();
 
         $a = new stdClass();
@@ -209,7 +210,13 @@ function environment_get_errors($environment_results) {
         // Append the restrict if there is some
         $feedbacktext .= $environment_result->strToReport($environment_result->getRestrictStr(), 'error');
 
-        $report .= html_to_text($feedbacktext);
+        if ($plugin === '') {
+            $report = '[' . get_string('coresystem') . '] ' . $report;
+        } else {
+            $report = '[' . $plugin . '] ' . $report;
+        }
+
+        $report .= ' - ' . html_to_text($feedbacktext);
 
         if ($environment_result->getPart() == 'custom_check'){
             $errors[] = array($info, $report);
index 7a53d37..4664a09 100644 (file)
@@ -7,7 +7,7 @@
         {{/element.hiddenlabel}}
     {{/label}}
     {{$element}}
-        <fieldset class="m-0 p-0 border-0">
+        <fieldset class="w-100 m-0 p-0 border-0">
             <legend class="sr-only">{{label}}</legend>
             <div class="d-flex flex-wrap">
             {{#element.elements}}
index a9a70a0..a66dfe0 100644 (file)
@@ -636,7 +636,35 @@ class CSS extends Minify
 
             return $placeholder;
         };
+        // Moodle-specific change MDL-68191 starts.
+        /* This was the old code:
         $this->registerPattern('/\n?\/\*(!|.*?@license|.*?@preserve).*?\*\/\n?/s', $callback);
+        */
+        // This is the new, more accurate and faster regex.
+        $this->registerPattern('/
+            # optional newline
+            \n?
+
+            # start comment
+            \/\*
+
+            # comment content
+            (?:
+                # either starts with an !
+                !
+            |
+                # or, after some number of characters which do not end the comment
+                (?:(?!\*\/).)*?
+
+                # there is either a @license or @preserve tag
+                @(?:license|preserve)
+            )
+
+            # then match to the end of the comment
+            .*?\*\/\n?
+
+            /ixs', $callback);
+        // Moodle-specific change MDL-68191.
 
         $this->registerPattern('/\/\*.*?\*\//s', '');
     }
index 9593ad0..d03fa3d 100644 (file)
@@ -14,3 +14,10 @@ Local changes applied:
 MDL-67115: php 74 compliance - implode() params order. Note this has been fixed upstream
   by https://github.com/matthiasmullie/minify/pull/300 so, whenever this library is updated
   check if the fix is included and remove this note.
+
+MDL-68191: https://github.com/matthiasmullie/minify/issues/317 is a bug that stops
+  large sections of the CSS from being minimised, and also is a huge performance drain.
+  We have applied the fix sent upstream because the performance win is so big.
+  (E.g. one case I measured, with the bug was 40 seconds to minify