Merge branch 'MDL-67910-master' of git://github.com/sarjona/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Fri, 10 Apr 2020 09:10:16 +0000 (11:10 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Fri, 10 Apr 2020 09:10:16 +0000 (11:10 +0200)
307 files changed:
.eslintignore
.stylelintignore
admin/cli/checks.php [new file with mode: 0644]
admin/index.php
admin/renderer.php
admin/settings/badges.php
admin/tasklogs.php
admin/templates/tasklogs.mustache
admin/tool/analytics/classes/output/renderer.php
admin/tool/task/classes/check/adhocqueue.php [new file with mode: 0644]
admin/tool/task/classes/check/cronrunning.php [new file with mode: 0644]
admin/tool/task/classes/check/maxfaildelay.php [new file with mode: 0644]
admin/tool/task/classes/edit_scheduled_task_form.php
admin/tool/task/clear_fail_delay.php
admin/tool/task/lang/en/tool_task.php
admin/tool/task/lib.php [new file with mode: 0644]
admin/tool/task/renderer.php
admin/tool/task/schedule_task.php
admin/tool/task/scheduledtasks.php
admin/tool/task/tests/behat/clear_fail_delay.feature
admin/tool/task/tests/behat/manage_tasks.feature
auth/none/classes/check/noauth.php [new file with mode: 0644]
auth/none/lang/en/auth_none.php
auth/none/lib.php [new file with mode: 0644]
auth/none/version.php
backup/util/ui/renderer.php
badges/classes/assertion.php
badges/classes/badge.php
badges/issuer_json.php
badges/renderer.php
badges/tests/badgeslib_test.php
badges/tests/behat/add_badge.feature
badges/tests/behat/award_badge.feature
badges/tests/behat/award_badge_groups.feature
badges/tests/behat/criteria_activity.feature
badges/tests/behat/criteria_cohort.feature
badges/tests/behat/criteria_competency.feature
badges/tests/behat/criteria_profile.feature
badges/tests/behat/role_visibility.feature
badges/upgrade.txt
badges/upgradelib.php
blocks/admin_bookmarks/tests/behat/bookmark_admin_pages.feature
blocks/badges/block_badges.php
blocks/badges/tests/behat/block_badges_course.feature
blocks/badges/tests/behat/block_badges_dashboard.feature
blocks/badges/tests/behat/block_badges_frontpage.feature
blocks/comments/block_comments.php
blocks/private_files/block_private_files.php
blocks/rss_client/block_rss_client.php
blocks/settings/block_settings.php
config-dist.php
course/amd/build/activitychooser.min.js
course/amd/build/activitychooser.min.js.map
course/amd/build/local/activitychooser/dialogue.min.js
course/amd/build/local/activitychooser/dialogue.min.js.map
course/amd/build/local/activitychooser/selectors.min.js
course/amd/build/local/activitychooser/selectors.min.js.map
course/amd/src/activitychooser.js
course/amd/src/local/activitychooser/dialogue.js
course/amd/src/local/activitychooser/selectors.js
course/classes/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/format.js
course/format/topics/renderer.php
course/format/upgrade.txt
course/format/weeks/format.js
course/templates/activity_list.mustache
course/templates/activitychooser.mustache
course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-debug.js
course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-min.js
course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop.js
course/yui/src/dragdrop/js/resource.js
course/yui/src/dragdrop/js/section.js
enrol/manual/lib.php
grade/grading/form/guide/renderer.php
grade/report/user/renderer.php
h5p/classes/core.php
install/lang/sd_ap/langconfig.php [new file with mode: 0644]
install/lang/se/error.php [new file with mode: 0644]
install/lang/se/install.php
lang/en/admin.php
lang/en/moodle.php
lang/en/xapi.php [new file with mode: 0644]
lib/adminlib.php
lib/badgeslib.php
lib/classes/check/access/defaultuserrole.php [new file with mode: 0644]
lib/classes/check/access/frontpagerole.php [new file with mode: 0644]
lib/classes/check/access/guestrole.php [new file with mode: 0644]
lib/classes/check/access/riskadmin.php [new file with mode: 0644]
lib/classes/check/access/riskbackup.php [new file with mode: 0644]
lib/classes/check/access/riskbackup_result.php [new file with mode: 0644]
lib/classes/check/access/riskxss.php [new file with mode: 0644]
lib/classes/check/access/riskxss_result.php [new file with mode: 0644]
lib/classes/check/check.php [new file with mode: 0644]
lib/classes/check/environment/configrw.php [new file with mode: 0644]
lib/classes/check/environment/displayerrors.php [new file with mode: 0644]
lib/classes/check/environment/environment.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/upgradecheck.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/performance/backups.php [new file with mode: 0644]
lib/classes/check/performance/cachejs.php [new file with mode: 0644]
lib/classes/check/performance/debugging.php [new file with mode: 0644]
lib/classes/check/performance/designermode.php [new file with mode: 0644]
lib/classes/check/performance/stats.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/check/table.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/clilib.php
lib/completionlib.php
lib/components.json
lib/db/services.php
lib/db/upgrade.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/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/mdn-polyfills/readme_moodle.txt [deleted file]
lib/minify/matthiasmullie-minify/src/CSS.php
lib/minify/readme_moodle.txt
lib/moodlelib.php
lib/navigationlib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/polyfills/polyfill.js [moved from lib/mdn-polyfills/polyfill.js with 64% similarity]
lib/polyfills/readme_moodle.txt [new file with mode: 0644]
lib/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/templates/initials_bar.mustache
lib/tests/authlib_test.php
lib/tests/check_test.php [new file with mode: 0644]
lib/tests/completionlib_test.php
lib/tests/component_test.php
lib/tests/events_test.php
lib/tests/tablelib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
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]
lib/yui/build/moodle-core-blocks/moodle-core-blocks-debug.js
lib/yui/build/moodle-core-blocks/moodle-core-blocks-min.js
lib/yui/build/moodle-core-blocks/moodle-core-blocks.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop.js
lib/yui/src/blocks/js/manager.js
lib/yui/src/dragdrop/js/dragdrop.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/book/tool/print/classes/output/renderer.php
mod/book/view.php
mod/choice/renderer.php
mod/data/field/multimenu/field.class.php
mod/folder/renderer.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/wiki/renderer.php
mod/workshop/renderer.php
phpunit.xml.dist
privacy/export_files/general.js
question/import.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
report/eventlist/classes/renderer.php
report/insights/classes/output/renderer.php
report/performance/index.php
report/performance/locallib.php [deleted file]
report/performance/version.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
report/status/classes/privacy/provider.php [new file with mode: 0644]
report/status/db/access.php [new file with mode: 0644]
report/status/index.php [new file with mode: 0644]
report/status/lang/en/report_status.php [new file with mode: 0644]
report/status/settings.php [new file with mode: 0644]
report/status/version.php [new file with mode: 0644]
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 c111d11..a13efd1 100644 (file)
@@ -62,7 +62,7 @@ lib/amd/src/popper.js
 lib/geopattern-php/
 lib/php-jwt/
 lib/babel-polyfill/
-lib/mdn-polyfills/
+lib/polyfills/
 lib/emoji-data/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
index a828212..ac09a33 100644 (file)
@@ -63,7 +63,7 @@ lib/amd/src/popper.js
 lib/geopattern-php/
 lib/php-jwt/
 lib/babel-polyfill/
-lib/mdn-polyfills/
+lib/polyfills/
 lib/emoji-data/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
diff --git a/admin/cli/checks.php b/admin/cli/checks.php
new file mode 100644 (file)
index 0000000..40deb4d
--- /dev/null
@@ -0,0 +1,171 @@
+<?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/>.
+
+/**
+ * CLI tool for system 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
+ */
+
+define('CLI_SCRIPT', true);
+
+require(__DIR__ . '/../../config.php');
+require_once($CFG->libdir.'/clilib.php');
+
+use core\check\result;
+
+list($options, $unrecognized) = cli_get_params([
+    'help'    => false,
+    'filter'  => '',
+    'type'    => 'status',
+    'verbose' => false,
+], [
+    'h' => 'help',
+    'f' => 'filter',
+    'v' => 'verbose',
+    't' => 'type',
+]);
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+$checks = \core\check\manager::get_checks($options['type']);
+$types = join(', ', \core\check\manager::TYPES);
+
+$help = "Run Moodle system checks
+
+Options:
+ -h, --help      Print out this help
+ -f, --filter    Filter to a subset of checks
+ -t, --type      Which set of checks? Defaults to 'status'
+                 One of $types
+ -v, --verbose   Show details of all checks, not just failed checks
+
+Example:
+
+  sudo -u www-data php admin/cli/checks.php
+  sudo -u www-data php admin/cli/checks.php -v
+  sudo -u www-data php admin/cli/checks.php -v --filter=environment
+
+";
+
+if ($options['help']) {
+    echo $help;
+    die();
+}
+
+$filter = $options['filter'];
+if ($filter) {
+    $checks = array_filter($checks, function($check, $key) use ($filter) {
+        $ref = $check->get_ref();
+        return (strpos($ref, $filter) !== false);
+    }, 1);
+}
+
+// These shell exit codes and labels align with the NRPE standard.
+$exitcodes = [
+    result::NA        => 0,
+    result::OK        => 0,
+    result::INFO      => 0,
+    result::UNKNOWN   => 3,
+    result::WARNING   => 1,
+    result::ERROR     => 2,
+    result::CRITICAL  => 2,
+];
+$exitlabel = [
+    result::NA        => 'OK',
+    result::OK        => 'OK',
+    result::INFO      => 'OK',
+    result::UNKNOWN   => 'UNKNOWN',
+    result::WARNING   => 'WARNING',
+    result::ERROR     => 'CRITICAL',
+    result::CRITICAL  => 'CRITICAL',
+];
+
+$format = "%      10s| % -60s\n";
+$spacer = "----------+--------------------------------------------------------------------\n";
+$prefix = '          |';
+
+$output = '';
+$header = $exitlabel[result::OK] . ': ' . get_string('checksok', '', $options['type']) . "\n";
+$exitcode = $exitcodes[result::OK];
+
+foreach ($checks as $check) {
+    $ref = $check->get_ref();
+    $result = $check->get_result();
+
+    $status = $result->get_status();
+    $checkexitcode = $exitcodes[$status];
+
+    // Summary is treated as html.
+    $summary = $result->get_summary();
+    $summary = html_to_text($summary, 60, false);
+
+    if ($checkexitcode > $exitcode) {
+        $exitcode = $checkexitcode;
+        $header = $exitlabel[$status] . ': ' . $check->get_name() . " (" . $check->get_ref() . ")\n";
+    }
+
+    if (empty($messages[$status])) {
+        $messages[$status] = $result;
+    }
+
+    $len = strlen(get_string('status' . $status));
+
+    if ($options['verbose'] ||
+        $status == result::WARNING ||
+        $status == result::CRITICAL ||
+        $status == result::ERROR) {
+
+        $output .= sprintf(
+            $format,
+            $OUTPUT->check_result($result),
+            sprintf('%s (%s)', $check->get_name(), $ref)
+        );
+
+        $summary = str_replace("\n", "\n" . $prefix . '     ', $summary);
+        $output .= sprintf( $format, '', '    ' . $summary);
+
+        if ($options['verbose']) {
+            $actionlink = $check->get_action_link();
+            if ($actionlink) {
+                $output .= sprintf( $format, '', '    ' . $actionlink->url);
+            }
+            $output .= sprintf( $format, '', '');
+        }
+    }
+}
+
+// Print NRPE header.
+print $header;
+
+// Only show the table header if there is anything to show.
+if ($output) {
+    print sprintf($format,
+        get_string('status'). ' ',
+        get_string('check')
+    ) .  $spacer;
+    print $output;
+}
+
+// NRPE shell exit code.
+exit($exitcode);
+
index c95c73c..ee3b2e3 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');
 }
 
@@ -826,8 +843,9 @@ $errorsdisplayed = defined('WARN_DISPLAY_ERRORS_ENABLED');
 $lastcron = get_config('tool_task', 'lastcronstart');
 $cronoverdue = ($lastcron < time() - 3600 * 24);
 $lastcroninterval = get_config('tool_task', 'lastcroninterval');
-$expectedfrequency = $CFG->expectedcronfrequency ?? 200;
-$croninfrequent = !$cronoverdue && ($lastcroninterval > $expectedfrequency || $lastcron < time() - $expectedfrequency);
+
+$expectedfrequency = $CFG->expectedcronfrequency ?? MINSECS;
+$croninfrequent = !$cronoverdue && ($lastcroninterval > ($expectedfrequency + MINSECS) || $lastcron < time() - $expectedfrequency);
 $dbproblems = $DB->diagnose();
 $maintenancemode = !empty($CFG->maintenance_enabled);
 
index 4ab7754..f904352 100644 (file)
@@ -601,19 +601,9 @@ class core_admin_renderer extends plugin_renderer_base {
             return '';
         }
 
-        if (empty($CFG->cronclionly)) {
-            $url = new moodle_url('/admin/cron.php');
-            if (!empty($CFG->cronremotepassword)) {
-                $url = new moodle_url('/admin/cron.php', array('password' => $CFG->cronremotepassword));
-            }
-
-            return $this->warning(get_string('cronwarning', 'admin', $url->out()) . '&nbsp;' .
-                    $this->help_icon('cron', 'admin'));
-        }
-
-        // $CFG->cronclionly is not empty: cron can run only from CLI.
-        return $this->warning(get_string('cronwarningcli', 'admin') . '&nbsp;' .
-                $this->help_icon('cron', 'admin'));
+        $check = new \tool_task\check\cronrunning();
+        $result = $check->get_result();
+        return $this->warning($result->get_summary() . '&nbsp;' . $this->help_icon('cron', 'admin'));
     }
 
     /**
@@ -629,9 +619,9 @@ class core_admin_renderer extends plugin_renderer_base {
             return '';
         }
 
-        $expectedfrequency = $CFG->expectedcronfrequency ?? 200;
-        return $this->warning(get_string('croninfrequent', 'admin', $expectedfrequency) . '&nbsp;' .
-                $this->help_icon('cron', 'admin'));
+        $check = new \tool_task\check\cronrunning();
+        $result = $check->get_result();
+        return $this->warning($result->get_summary() . '&nbsp;' . $this->help_icon('cron', 'admin'));
     }
 
     /**
index 0ea5193..2451390 100644 (file)
@@ -100,10 +100,11 @@ if (($hassiteconfig || has_any_capability(array(
             new lang_string('allowexternalbackpack', 'badges'),
             new lang_string('allowexternalbackpack_desc', 'badges'), 1));
 
+    $bp = $DB->get_record('badge_external_backpack', ['backpackweburl' => BADGRIO_BACKPACKWEBURL]);
     $backpacksettings->add(new admin_setting_configselect('badges_site_backpack',
             new lang_string('sitebackpack', 'badges'),
             new lang_string('sitebackpack_help', 'badges'),
-            1, $choices));
+            $bp->id, $choices));
 
     $warning = badges_verify_site_backpack();
     if (!empty($warning)) {
index 0fb5206..796f323 100644 (file)
@@ -62,6 +62,8 @@ if (null !== $logid) {
 $renderer = $PAGE->get_renderer('tool_task');
 
 echo $OUTPUT->header();
+
+// Output the search form.
 echo $OUTPUT->render_from_template('core_admin/tasklogs', (object) [
     'action' => $pageurl->out(),
     'filter' => $filter,
@@ -84,6 +86,7 @@ echo $OUTPUT->render_from_template('core_admin/tasklogs', (object) [
     ],
 ]);
 
+// Output any matching logs.
 $table = new \core_admin\task_log_table($filter, $result);
 $table->baseurl = $pageurl;
 $table->out(100, false);
index c3180a2..bb180b6 100644 (file)
@@ -17,7 +17,7 @@
 {{!
     @template core_admin/tasklogs
 
-    Task Logs template.
+    This is the template for the search form which appears above the task logs report.
 }}
 <form class="form-inline" method="GET" action="{{{action}}}">
     <label class="sr-only" for="tasklog-filter">{{#str}}filter{{/str}}</label>
index edd165d..0855ed1 100644 (file)
@@ -27,8 +27,7 @@ namespace tool_analytics\output;
 defined('MOODLE_INTERNAL') || die();
 
 use plugin_renderer_base;
-use templatable;
-use renderable;
+
 
 /**
  * Renderer class.
@@ -74,14 +73,12 @@ class renderer extends plugin_renderer_base {
      * @return string HTML
      */
     public function render_evaluate_results($results, $logs = array()) {
-        global $OUTPUT;
-
         $output = '';
 
         foreach ($results as $timesplittingid => $result) {
 
             if (!CLI_SCRIPT) {
-                $output .= $OUTPUT->box_start('generalbox mb-3');
+                $output .= $this->output->box_start('generalbox mb-3');
             }
 
             // Check that the array key is a string, not all results depend on time splitting methods (e.g. general errors).
@@ -90,47 +87,48 @@ class renderer extends plugin_renderer_base {
                 $langstrdata = (object)array('name' => $timesplitting->get_name(), 'id' => $timesplittingid);
 
                 if (CLI_SCRIPT) {
-                    $output .= $OUTPUT->heading(get_string('scheduledanalysisresultscli', 'tool_analytics', $langstrdata), 3);
+                    $output .= $this->output->heading(get_string('scheduledanalysisresultscli', 'tool_analytics', $langstrdata), 3);
                 } else {
-                    $output .= $OUTPUT->heading(get_string('scheduledanalysisresults', 'tool_analytics', $langstrdata), 3);
+                    $output .= $this->output->heading(get_string('scheduledanalysisresults', 'tool_analytics', $langstrdata), 3);
                 }
             }
 
             if ($result->status == 0) {
-                $output .= $OUTPUT->notification(get_string('goodmodel', 'tool_analytics'),
+                $output .= $this->output->notification(get_string('goodmodel', 'tool_analytics'),
                     \core\output\notification::NOTIFY_SUCCESS);
             } else if ($result->status === \core_analytics\model::NO_DATASET) {
-                $output .= $OUTPUT->notification(get_string('nodatatoevaluate', 'tool_analytics'),
+                $output .= $this->output->notification(get_string('nodatatoevaluate', 'tool_analytics'),
                     \core\output\notification::NOTIFY_WARNING);
             }
 
             if (isset($result->score)) {
                 // Score.
-                $output .= $OUTPUT->heading(get_string('accuracy', 'tool_analytics') . ': ' .
+                $output .= $this->output->heading(get_string('accuracy', 'tool_analytics') . ': ' .
                     round(floatval($result->score), 4) * 100  . '%', 4);
             }
 
             if (!empty($result->info)) {
                 foreach ($result->info as $message) {
-                    $output .= $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING);
+                    $output .= $this->output->notification($message, \core\output\notification::NOTIFY_WARNING);
                 }
             }
 
             if (!CLI_SCRIPT) {
-                $output .= $OUTPUT->box_end();
+                $output .= $this->output->box_end();
             }
         }
 
         // Info logged during evaluation.
         if (!empty($logs) && debugging()) {
-            $output .= $OUTPUT->heading(get_string('extrainfo', 'tool_analytics'), 3);
+            $output .= $this->output->heading(get_string('extrainfo', 'tool_analytics'), 3);
             foreach ($logs as $log) {
-                $output .= $OUTPUT->notification($log, \core\output\notification::NOTIFY_WARNING);
+                $output .= $this->output->notification($log, \core\output\notification::NOTIFY_WARNING);
             }
         }
 
         if (!CLI_SCRIPT) {
-            $output .= $OUTPUT->single_button(new \moodle_url('/admin/tool/analytics/index.php'), get_string('continue'), 'get');
+            $output .= $this->output->single_button(new \moodle_url('/admin/tool/analytics/index.php'),
+                    get_string('continue'), 'get');
         }
 
         return $output;
@@ -147,62 +145,68 @@ class renderer extends plugin_renderer_base {
      * @return string HTML
      */
     public function render_get_predictions_results($trainresults = false, $trainlogs = array(), $predictresults = false, $predictlogs = array()) {
-        global $OUTPUT;
-
         $output = '';
 
         if ($trainresults || (!empty($trainlogs) && debugging())) {
-            $output .= $OUTPUT->heading(get_string('trainingresults', 'tool_analytics'), 3);
+            $output .= $this->output->heading(get_string('trainingresults', 'tool_analytics'), 3);
         }
 
         if ($trainresults) {
             if ($trainresults->status == 0) {
-                $output .= $OUTPUT->notification(get_string('trainingprocessfinished', 'tool_analytics'),
+                $output .= $this->output->notification(
+                        get_string('trainingprocessfinished', 'tool_analytics'),
                     \core\output\notification::NOTIFY_SUCCESS);
             } else if ($trainresults->status === \core_analytics\model::NO_DATASET ||
                     $trainresults->status === \core_analytics\model::NOT_ENOUGH_DATA) {
-                $output .= $OUTPUT->notification(get_string('nodatatotrain', 'tool_analytics'),
+                $output .= $this->output->notification(
+                        get_string('nodatatotrain', 'tool_analytics'),
                     \core\output\notification::NOTIFY_WARNING);
             } else {
-                $output .= $OUTPUT->notification(get_string('generalerror', 'tool_analytics', $trainresults->status),
+                $output .= $this->output->notification(
+                        get_string('generalerror', 'tool_analytics', $trainresults->status),
                     \core\output\notification::NOTIFY_ERROR);
             }
         }
 
         if (!empty($trainlogs) && debugging()) {
-            $output .= $OUTPUT->heading(get_string('extrainfo', 'tool_analytics'), 4);
+            $output .= $this->output->heading(get_string('extrainfo', 'tool_analytics'), 4);
             foreach ($trainlogs as $log) {
-                $output .= $OUTPUT->notification($log, \core\output\notification::NOTIFY_WARNING);
+                $output .= $this->output->notification($log, \core\output\notification::NOTIFY_WARNING);
             }
         }
 
         if ($predictresults || (!empty($predictlogs) && debugging())) {
-            $output .= $OUTPUT->heading(get_string('predictionresults', 'tool_analytics'), 3, 'main mt-3');
+            $output .= $this->output->heading(
+                    get_string('predictionresults', 'tool_analytics'), 3, 'main mt-3');
         }
 
         if ($predictresults) {
             if ($predictresults->status == 0) {
-                $output .= $OUTPUT->notification(get_string('predictionprocessfinished', 'tool_analytics'),
+                $output .= $this->output->notification(
+                        get_string('predictionprocessfinished', 'tool_analytics'),
                     \core\output\notification::NOTIFY_SUCCESS);
             } else if ($predictresults->status === \core_analytics\model::NO_DATASET ||
                     $predictresults->status === \core_analytics\model::NOT_ENOUGH_DATA) {
-                $output .= $OUTPUT->notification(get_string('nodatatopredict', 'tool_analytics'),
+                $output .= $this->output->notification(
+                        get_string('nodatatopredict', 'tool_analytics'),
                     \core\output\notification::NOTIFY_WARNING);
             } else {
-                $output .= $OUTPUT->notification(get_string('generalerror', 'tool_analytics', $predictresults->status),
+                $output .= $this->output->notification(
+                        get_string('generalerror', 'tool_analytics', $predictresults->status),
                     \core\output\notification::NOTIFY_ERROR);
             }
         }
 
         if (!empty($predictlogs) && debugging()) {
-            $output .= $OUTPUT->heading(get_string('extrainfo', 'tool_analytics'), 4);
+            $output .= $this->output->heading(get_string('extrainfo', 'tool_analytics'), 4);
             foreach ($predictlogs as $log) {
-                $output .= $OUTPUT->notification($log, \core\output\notification::NOTIFY_WARNING);
+                $output .= $this->output->notification($log, \core\output\notification::NOTIFY_WARNING);
             }
         }
 
         if (!CLI_SCRIPT) {
-            $output .= $OUTPUT->single_button(new \moodle_url('/admin/tool/analytics/index.php'), get_string('continue'), 'get');
+            $output .= $this->output->single_button(new \moodle_url('/admin/tool/analytics/index.php'),
+                    get_string('continue'), 'get');
         }
 
         return $output;
@@ -236,17 +240,18 @@ class renderer extends plugin_renderer_base {
      * @return string HTML
      */
     public function render_analytics_disabled() {
-        global $OUTPUT, $PAGE, $FULLME;
+        global $FULLME;
 
-        $PAGE->set_url($FULLME);
-        $PAGE->set_title(get_string('pluginname', 'tool_analytics'));
-        $PAGE->set_heading(get_string('pluginname', 'tool_analytics'));
+        $this->page->set_url($FULLME);
+        $this->page->set_title(get_string('pluginname', 'tool_analytics'));
+        $this->page->set_heading(get_string('pluginname', 'tool_analytics'));
 
-        $output = $OUTPUT->header();
-        $output .= $OUTPUT->notification(get_string('analyticsdisabled', 'analytics'), \core\output\notification::NOTIFY_INFO);
+        $output = $this->output->header();
+        $output .= $this->output->notification(get_string('analyticsdisabled', 'analytics'),
+                \core\output\notification::NOTIFY_INFO);
         $output .= \html_writer::tag('a', get_string('continue'), ['class' => 'btn btn-primary',
             'href' => (new \moodle_url('/'))->out()]);
-        $output .= $OUTPUT->footer();
+        $output .= $this->output->footer();
 
         return $output;
     }
diff --git a/admin/tool/task/classes/check/adhocqueue.php b/admin/tool/task/classes/check/adhocqueue.php
new file mode 100644 (file)
index 0000000..85f98b8
--- /dev/null
@@ -0,0 +1,93 @@
+<?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/>.
+
+/**
+ * Ad hoc queue checks
+ *
+ * @package    tool_task
+ * @copyright  2020 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_task\check;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Ad hoc queue checks
+ *
+ * @package    tool_task
+ * @copyright  2020 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class adhocqueue extends check {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+        global $CFG;
+        $this->id = 'adhocqueue';
+        $this->name = get_string('checkadhocqueue', 'tool_task');
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result() : result {
+        global $DB, $CFG;
+
+        $stats = $DB->get_record_sql('
+            SELECT count(*) cnt,
+                   MAX(? - nextruntime) age
+              FROM {task_adhoc}', [time()]);
+
+        $status = result::OK;
+        $summary = get_string('adhocempty', 'tool_task');
+        $details = '';
+
+        if ($stats->cnt > 0) {
+            // A large queue size by itself is not an issue, only when tasks
+            // are not being processed in a timely fashion is it an issue.
+            $status = result::INFO;
+            $summary = get_string('adhocqueuesize', 'tool_task', $stats->cnt);
+        }
+
+        $max = $CFG->adhoctaskagewarn ?? 10 * MINSECS;
+        if ($stats->age > $max) {
+            $status = result::WARNING;
+            $summary = get_string('adhocqueueold', 'tool_task', [
+                'age' => format_time($stats->age),
+                'max' => format_time($max),
+            ]);
+        }
+
+        $max = $CFG->adhoctaskageerror ?? 4 * HOURSECS;
+        if ($stats->age > $max) {
+            $status = result::ERROR;
+            $summary = get_string('adhocqueueold', 'tool_task', [
+                'age' => format_time($stats->age),
+                'max' => format_time($max),
+            ]);
+        }
+
+        return new result($status, $summary, $details);
+    }
+}
diff --git a/admin/tool/task/classes/check/cronrunning.php b/admin/tool/task/classes/check/cronrunning.php
new file mode 100644 (file)
index 0000000..46aa690
--- /dev/null
@@ -0,0 +1,118 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Cron running check
+ *
+ * @package    tool_task
+ * @copyright  2020 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_task\check;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+/**
+ * Cron running check
+ *
+ * @package    tool_task
+ * @copyright  2020 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cronrunning extends check {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+        global $CFG;
+        $this->id = 'cronrunning';
+        $this->name = get_string('checkcronrunning', 'tool_task');
+        if (empty($CFG->cronclionly)) {
+            $this->actionlink = new \action_link(
+                new \moodle_url('/admin/cron.php'),
+                get_string('cron', 'admin'));
+        }
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result() : result {
+        global $CFG;
+
+        // Eventually this should replace cron_overdue_warning and
+        // cron_infrequent_warning.
+        $lastcron = get_config('tool_task', 'lastcronstart');
+        $expectedfrequency = $CFG->expectedcronfrequency ?? MINSECS;
+
+        $delta = time() - $lastcron;
+
+        $lastcroninterval = get_config('tool_task', 'lastcroninterval');
+
+        $formatdelta    = format_time($delta);
+        $formatexpected = format_time($expectedfrequency);
+        $formatinterval = format_time($lastcroninterval);
+
+        $details = format_time($delta);
+
+        if ($delta > $expectedfrequency + MINSECS) {
+            $status = result::WARNING;
+
+            if ($delta > DAYSECS) {
+                $status = result::CRITICAL;
+            }
+
+            if (empty($lastcron)) {
+                $summary = get_string('cronwarningnever', 'admin', [
+                    'expected' => $formatexpected,
+                ]);
+            } else if (empty($CFG->cronclionly)) {
+                $url = new \moodle_url('/admin/cron.php');
+                $summary = get_string('cronwarning', 'admin', [
+                    'url' => $url->out(),
+                    'actual'   => $formatdelta,
+                    'expected' => $formatexpected,
+                ]);
+            } else {
+                $summary = get_string('cronwarningcli', 'admin', [
+                    'actual'   => $formatdelta,
+                    'expected' => $formatexpected,
+                ]);
+            }
+            return new result($status, $summary, $details);
+        }
+
+        if ($lastcroninterval > $expectedfrequency) {
+            $status = result::WARNING;
+            $summary = get_string('croninfrequent', 'admin', [
+                'actual'   => $formatinterval,
+                'expected' => $formatexpected,
+            ]);
+            return new result($status, $summary, $details);
+        }
+
+        $status = result::OK;
+        $summary = get_string('cronok', 'tool_task');
+
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/admin/tool/task/classes/check/maxfaildelay.php b/admin/tool/task/classes/check/maxfaildelay.php
new file mode 100644 (file)
index 0000000..019aa75
--- /dev/null
@@ -0,0 +1,96 @@
+<?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/>.
+
+/**
+ * Task fail delay check
+ *
+ * @package    tool_task
+ * @copyright  2020 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_task\check;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Task fail delay check
+ *
+ * @package    tool_task
+ * @copyright  2020 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class maxfaildelay extends check {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+        global $CFG;
+        $this->id = 'cronfaildelay';
+        $this->name = get_string('checkmaxfaildelay', 'tool_task');
+        $this->actionlink = new \action_link(
+            new \moodle_url('/admin/tool/task/scheduledtasks.php'),
+            get_string('scheduledtasks', 'tool_task'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result() : result {
+        global $CFG;
+
+        $status = result::OK;
+        $summary = get_string('tasknofailures', 'tool_task');
+        $details = '';
+        $failures = 0;
+        $maxdelay = 0;
+
+        $tasks = \core\task\manager::get_all_scheduled_tasks();
+        foreach ($tasks as $task) {
+            if ($task->get_disabled()) {
+                continue;
+            }
+            $faildelay = $task->get_fail_delay();
+            if ($faildelay > $maxdelay) {
+                $maxdelay = $faildelay;
+            }
+            if ($faildelay > 0) {
+                $failures++;
+                $details .= get_string('faildelay', 'tool_task') . ': ' . format_time($faildelay);
+                $details .= ' - ' . $task->get_name() . ' (' .get_class($task) . ")<br>";
+            }
+        }
+
+        if ($failures > 0) {
+            // Intermittent failures are not yet a warning.
+            $status = result::INFO;
+            $summary = get_string('taskfailures', 'tool_task', $failures);
+        }
+        if ($maxdelay > 5 * MINSECS) {
+            $status = result::WARNING;
+        }
+        if ($maxdelay > 4 * HOURSECS) {
+            $status = result::ERROR;
+        }
+
+        return new result($status, $summary, $details);
+    }
+}
index 145b59d..1a76a3b 100644 (file)
@@ -34,46 +34,59 @@ require_once($CFG->libdir.'/formslib.php');
  */
 class tool_task_edit_scheduled_task_form extends moodleform {
     public function definition() {
+        global $PAGE;
+
         $mform = $this->_form;
         /** @var \core\task\scheduled_task $task */
         $task = $this->_customdata;
+        $defaulttask = \core\task\manager::get_default_scheduled_task(get_class($task), false);
+        $renderer = $PAGE->get_renderer('tool_task');
 
-        $plugininfo = core_plugin_manager::instance()->get_plugin_info($task->get_component());
-        $plugindisabled = $plugininfo && $plugininfo->is_enabled() === false && !$task->get_run_if_component_disabled();
-
-        $lastrun = $task->get_last_run_time() ? userdate($task->get_last_run_time()) : get_string('never');
-        $nextrun = $task->get_next_run_time();
-        if ($plugindisabled) {
-            $nextrun = get_string('plugindisabled', 'tool_task');
-        } else if ($task->get_disabled()) {
-            $nextrun = get_string('taskdisabled', 'tool_task');
-        } else if ($nextrun > time()) {
-            $nextrun = userdate($nextrun);
-        } else {
-            $nextrun = get_string('asap', 'tool_task');
-        }
-        $mform->addElement('static', 'lastrun', get_string('lastruntime', 'tool_task'), $lastrun);
-        $mform->addElement('static', 'nextrun', get_string('nextruntime', 'tool_task'), $nextrun);
+        $mform->addElement('static', 'lastrun', get_string('lastruntime', 'tool_task'),
+                $renderer->last_run_time($task));
+
+        $mform->addElement('static', 'nextrun', get_string('nextruntime', 'tool_task'),
+                $renderer->next_run_time($task));
 
-        $mform->addElement('text', 'minute', get_string('taskscheduleminute', 'tool_task'));
+        $mform->addGroup([
+                $mform->createElement('text', 'minute'),
+                $mform->createElement('static', 'minutedefault', '',
+                        get_string('defaultx', 'tool_task', $defaulttask->get_minute())),
+            ], 'minutegroup', get_string('taskscheduleminute', 'tool_task'), null, false);
         $mform->setType('minute', PARAM_RAW);
-        $mform->addHelpButton('minute', 'taskscheduleminute', 'tool_task');
+        $mform->addHelpButton('minutegroup', 'taskscheduleminute', 'tool_task');
 
-        $mform->addElement('text', 'hour', get_string('taskschedulehour', 'tool_task'));
+        $mform->addGroup([
+                $mform->createElement('text', 'hour'),
+                $mform->createElement('static', 'hourdefault', '',
+                        get_string('defaultx', 'tool_task', $defaulttask->get_hour())),
+        ], 'hourgroup', get_string('taskschedulehour', 'tool_task'), null, false);
         $mform->setType('hour', PARAM_RAW);
-        $mform->addHelpButton('hour', 'taskschedulehour', 'tool_task');
+        $mform->addHelpButton('hourgroup', 'taskschedulehour', 'tool_task');
 
-        $mform->addElement('text', 'day', get_string('taskscheduleday', 'tool_task'));
+        $mform->addGroup([
+                $mform->createElement('text', 'day'),
+                $mform->createElement('static', 'daydefault', '',
+                        get_string('defaultx', 'tool_task', $defaulttask->get_day())),
+        ], 'daygroup', get_string('taskscheduleday', 'tool_task'), null, false);
         $mform->setType('day', PARAM_RAW);
-        $mform->addHelpButton('day', 'taskscheduleday', 'tool_task');
+        $mform->addHelpButton('daygroup', 'taskscheduleday', 'tool_task');
 
-        $mform->addElement('text', 'month', get_string('taskschedulemonth', 'tool_task'));
+        $mform->addGroup([
+                $mform->createElement('text', 'month'),
+                $mform->createElement('static', 'monthdefault', '',
+                        get_string('defaultx', 'tool_task', $defaulttask->get_month())),
+        ], 'monthgroup', get_string('taskschedulemonth', 'tool_task'), null, false);
         $mform->setType('month', PARAM_RAW);
-        $mform->addHelpButton('month', 'taskschedulemonth', 'tool_task');
+        $mform->addHelpButton('monthgroup', 'taskschedulemonth', 'tool_task');
 
-        $mform->addElement('text', 'dayofweek', get_string('taskscheduledayofweek', 'tool_task'));
+        $mform->addGroup([
+                $mform->createElement('text', 'dayofweek'),
+                $mform->createElement('static', 'dayofweekdefault', '',
+                        get_string('defaultx', 'tool_task', $defaulttask->get_day_of_week())),
+        ], 'dayofweekgroup', get_string('taskscheduledayofweek', 'tool_task'), null, false);
         $mform->setType('dayofweek', PARAM_RAW);
-        $mform->addHelpButton('dayofweek', 'taskscheduledayofweek', 'tool_task');
+        $mform->addHelpButton('dayofweekgroup', 'taskscheduledayofweek', 'tool_task');
 
         $mform->addElement('advcheckbox', 'disabled', get_string('disabled', 'tool_task'));
         $mform->addHelpButton('disabled', 'disabled', 'tool_task');
@@ -111,7 +124,7 @@ class tool_task_edit_scheduled_task_form extends moodleform {
         $fields = array('minute', 'hour', 'day', 'month', 'dayofweek');
         foreach ($fields as $field) {
             if (!self::validate_fields($field, $data[$field])) {
-                $error[$field] = get_string('invaliddata', 'core_error');
+                $error[$field . 'group'] = get_string('invaliddata', 'core_error');
             }
         }
         return $error;
index 8f41b45..8f357b8 100644 (file)
@@ -39,13 +39,16 @@ if (!$task) {
     print_error('cannotfindinfo', 'error', $taskname);
 }
 
+$returnurl = new moodle_url('/admin/tool/task/scheduledtasks.php',
+        ['lastchanged' => get_class($task)]);
+
 // If actually doing the clear, then carry out the task and redirect to the scheduled task page.
 if (optional_param('confirm', 0, PARAM_INT)) {
     require_sesskey();
 
     \core\task\manager::clear_fail_delay($task);
 
-    redirect(new moodle_url('/admin/tool/task/scheduledtasks.php'));
+    redirect($returnurl);
 }
 
 // Start output.
@@ -60,9 +63,8 @@ echo $OUTPUT->header();
 // they confirm.
 echo $OUTPUT->confirm(get_string('clearfaildelay_confirm', 'tool_task', $task->get_name()),
         new single_button(new moodle_url('/admin/tool/task/clear_fail_delay.php',
-                array('task' => $taskname, 'confirm' => 1, 'sesskey' => sesskey())),
+                ['task' => $taskname, 'confirm' => 1, 'sesskey' => sesskey()]),
                 get_string('clear')),
-        new single_button(new moodle_url('/admin/tool/task/scheduledtasks.php'),
-                get_string('cancel'), false));
+        new single_button($returnurl, get_string('cancel'), false));
 
 echo $OUTPUT->footer();
index 76ab5da..9ccd292 100644 (file)
  */
 
 $string['asap'] = 'ASAP';
+$string['adhocempty'] = 'Adhoc task queue is empty';
+$string['adhocqueuesize'] = 'Adhoc task queue has {$a} tasks';
+$string['adhocqueueold'] = 'Oldest task is {$a->age} which is more than {$a->max}';
 $string['backtoscheduledtasks'] = 'Back to scheduled tasks';
 $string['blocking'] = 'Blocking';
 $string['cannotfindthepathtothecli'] = 'Cannot find the path to the PHP CLI executable so task execution aborted. Set the \'Path to PHP CLI\' setting in Site administration / Server / System paths.';
+$string['checkadhocqueue'] = 'Adhoc task queue';
+$string['checkcronrunning'] = 'Cron running';
+$string['checkmaxfaildelay'] = 'Tasks max fail delay';
 $string['clearfaildelay_confirm'] = 'Are you sure you want to clear the fail delay for task \'{$a}\'? After clearing the delay, the task will run according to its normal schedule.';
 $string['component'] = 'Component';
 $string['corecomponent'] = 'Core';
+$string['cronok'] = 'Cron is running frequently';
 $string['default'] = 'Default';
+$string['defaultx'] = 'Default: {$a}';
 $string['disabled'] = 'Disabled';
 $string['disabled_help'] = 'Disabled scheduled tasks are not executed from cron, however they can still be executed manually via the CLI tool.';
 $string['edittaskschedule'] = 'Edit task schedule: {$a}';
 $string['enablerunnow'] = 'Allow \'Run now\' for scheduled tasks';
 $string['enablerunnow_desc'] = 'Allows administrators to run a single scheduled task immediately, rather than waiting for it to run as scheduled. The feature requires \'Path to PHP CLI\' (pathtophp) to be set in System paths. The task runs on the web server, so you may wish to disable this feature to avoid potential performance issues.';
 $string['faildelay'] = 'Fail delay';
+$string['fromcomponent'] = 'From component: {$a}';
 $string['lastruntime'] = 'Last run';
 $string['nextruntime'] = 'Next run';
 $string['plugindisabled'] = 'Plugin disabled';
@@ -49,7 +58,9 @@ $string['runpattern'] = 'Run pattern';
 $string['scheduledtasks'] = 'Scheduled tasks';
 $string['scheduledtaskchangesdisabled'] = 'Modifications to the list of scheduled tasks have been prevented in Moodle configuration';
 $string['taskdisabled'] = 'Task disabled';
+$string['taskfailures'] = 'There are {$a} task(s) failing';
 $string['tasklogs'] = 'Task logs';
+$string['tasknofailures'] = 'There are no tasks failing';
 $string['taskscheduleday'] = 'Day';
 $string['taskscheduleday_help'] = 'Day of month field for task schedule. The field uses the same format as unix cron. Some examples are:
 
diff --git a/admin/tool/task/lib.php b/admin/tool/task/lib.php
new file mode 100644 (file)
index 0000000..ae23257
--- /dev/null
@@ -0,0 +1,39 @@
+<?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/>.
+
+/**
+ * Task API status checks
+ *
+ * @package    tool_task
+ * @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 cron related service status checks
+ *
+ * @return array of check objects
+ */
+function tool_task_status_checks() : array {
+    return [
+        new \tool_task\check\cronrunning(),
+        new \tool_task\check\maxfaildelay(),
+        new \tool_task\check\adhocqueue(),
+    ];
+}
+
index 3afd20c..0a01d0a 100644 (file)
@@ -25,6 +25,9 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+use core\task\scheduled_task;
+
+
 /**
  * Implements the plugin renderer
  *
@@ -36,9 +39,10 @@ class tool_task_renderer extends plugin_renderer_base {
      * This function will render one beautiful table with all the scheduled tasks.
      *
      * @param \core\task\scheduled_task[] $tasks - list of all scheduled tasks.
+     * @param string $lastchanged (optional) the last task edited. Gets highlighted in teh table.
      * @return string HTML to output.
      */
-    public function scheduled_tasks_table($tasks) {
+    public function scheduled_tasks_table($tasks, $lastchanged = '') {
         global $CFG;
 
         $showloglink = \core\task\logmanager::has_log_report();
@@ -68,118 +72,205 @@ class tool_task_renderer extends plugin_renderer_base {
             $table->colclasses['3'] = 'hidden';
         }
 
-        $data = array();
+        $data = [];
         $yes = get_string('yes');
         $no = get_string('no');
-        $never = get_string('never');
-        $asap = get_string('asap', 'tool_task');
-        $disabledstr = get_string('taskdisabled', 'tool_task');
-        $plugindisabledstr = get_string('plugindisabled', 'tool_task');
-        $runnabletasks = tool_task\run_from_cli::is_runnable();
+        $canruntasks = tool_task\run_from_cli::is_runnable();
         foreach ($tasks as $task) {
+            $classname = get_class($task);
+            $defaulttask = \core\task\manager::get_default_scheduled_task($classname, false);
+
             $customised = $task->is_customised() ? $no : $yes;
             if (empty($CFG->preventscheduledtaskchanges)) {
-                $configureurl = new moodle_url('/admin/tool/task/scheduledtasks.php', array('action'=>'edit', 'task' => get_class($task)));
-                $editlink = $this->action_icon($configureurl, new pix_icon('t/edit', get_string('edittaskschedule', 'tool_task', $task->get_name())));
+                $configureurl = new moodle_url('/admin/tool/task/scheduledtasks.php',
+                        ['action' => 'edit', 'task' => $classname]);
+                $editlink = $this->output->action_icon($configureurl, new pix_icon('t/edit',
+                        get_string('edittaskschedule', 'tool_task', $task->get_name())));
             } else {
-                $editlink = $this->render(new pix_icon('t/locked', get_string('scheduledtaskchangesdisabled', 'tool_task')));
+                $editlink = $this->render(new pix_icon('t/locked',
+                        get_string('scheduledtaskchangesdisabled', 'tool_task')));
             }
 
             $loglink = '';
             if ($showloglink) {
-                $loglink = $this->action_icon(
-                    \core\task\logmanager::get_url_for_task_class(get_class($task)),
+                $loglink = $this->output->action_icon(
+                    \core\task\logmanager::get_url_for_task_class($classname),
                     new pix_icon('e/file-text', get_string('viewlogs', 'tool_task', $task->get_name())
                 ));
             }
 
-            $namecell = new html_table_cell($task->get_name() . "\n" . html_writer::tag('span', '\\'.get_class($task),
-                array('class' => 'task-class text-ltr')));
+            $namecell = new html_table_cell($task->get_name() . "\n" .
+                    html_writer::span('\\' . $classname, 'task-class text-ltr'));
             $namecell->header = true;
 
-            $component = $task->get_component();
-            $plugininfo = null;
-            list($type, $plugin) = core_component::normalize_component($component);
-            if ($type === 'core') {
-                $componentcell = new html_table_cell(get_string('corecomponent', 'tool_task'));
-            } else {
-                if ($plugininfo = core_plugin_manager::instance()->get_plugin_info($component)) {
-                    $plugininfo->init_display_name();
-                    $componentcell = new html_table_cell($plugininfo->displayname);
-                } else {
-                    $componentcell = new html_table_cell($component);
-                }
-            }
-
-            $lastrun = $task->get_last_run_time() ? userdate($task->get_last_run_time()) : $never;
-            $nextrun = $task->get_next_run_time();
-            $disabled = false;
-            if ($plugininfo && $plugininfo->is_enabled() === false && !$task->get_run_if_component_disabled()) {
-                $disabled = true;
-                $nextrun = $plugindisabledstr;
-            } else if ($task->get_disabled()) {
-                $disabled = true;
-                $nextrun = $disabledstr;
-            } else if ($nextrun > time()) {
-                $nextrun = userdate($nextrun);
-            } else {
-                $nextrun = $asap;
-            }
+            $plugininfo = core_plugin_manager::instance()->get_plugin_info($task->get_component());
+            $plugindisabled = $plugininfo && $plugininfo->is_enabled() === false &&
+                    !$task->get_run_if_component_disabled();
+            $disabled = $plugindisabled || $task->get_disabled();
 
             $runnow = '';
-            if ( ! $disabled && get_config('tool_task', 'enablerunnow') && $runnabletasks ) {
+            if (!$disabled && get_config('tool_task', 'enablerunnow') && $canruntasks ) {
                 $runnow = html_writer::div(html_writer::link(
                         new moodle_url('/admin/tool/task/schedule_task.php',
-                            array('task' => get_class($task))),
+                            ['task' => $classname]),
                         get_string('runnow', 'tool_task')), 'task-runnow');
             }
 
-            $clearfail = '';
+            $faildelaycell = new html_table_cell($task->get_fail_delay());
             if ($task->get_fail_delay()) {
-                $clearfail = html_writer::div(html_writer::link(
+                $faildelaycell->text .= html_writer::div(html_writer::link(
                         new moodle_url('/admin/tool/task/clear_fail_delay.php',
-                                array('task' => get_class($task), 'sesskey' => sesskey())),
+                                ['task' => $classname, 'sesskey' => sesskey()]),
                         get_string('clear')), 'task-clearfaildelay');
+                $faildelaycell->attributes['class'] = 'table-danger';
             }
 
-            $row = new html_table_row(array(
+            $row = new html_table_row([
                         $namecell,
-                        $componentcell,
+                        new html_table_cell($this->component_name($task->get_component())),
                         new html_table_cell($editlink),
                         new html_table_cell($loglink),
-                        new html_table_cell($lastrun . $runnow),
-                        new html_table_cell($nextrun),
-                        new html_table_cell($task->get_minute()),
-                        new html_table_cell($task->get_hour()),
-                        new html_table_cell($task->get_day()),
-                        new html_table_cell($task->get_day_of_week()),
-                        new html_table_cell($task->get_month()),
-                        new html_table_cell($task->get_fail_delay() . $clearfail),
-                        new html_table_cell($customised)));
-
-            // Cron-style values must always be LTR.
-            $row->cells[6]->attributes['class'] = 'text-ltr';
-            $row->cells[7]->attributes['class'] = 'text-ltr';
-            $row->cells[8]->attributes['class'] = 'text-ltr';
-            $row->cells[9]->attributes['class'] = 'text-ltr';
-            $row->cells[10]->attributes['class'] = 'text-ltr';
+                        new html_table_cell($this->last_run_time($task) . $runnow),
+                        new html_table_cell($this->next_run_time($task)),
+                        $this->time_cell($task->get_minute(), $defaulttask->get_minute()),
+                        $this->time_cell($task->get_hour(), $defaulttask->get_hour()),
+                        $this->time_cell($task->get_day(), $defaulttask->get_day()),
+                        $this->time_cell($task->get_day_of_week(), $defaulttask->get_day_of_week()),
+                        $this->time_cell($task->get_month(), $defaulttask->get_month()),
+                        $faildelaycell,
+                        new html_table_cell($customised)]);
 
+            $classes = [];
             if ($disabled) {
-                $row->attributes['class'] = 'disabled';
+                $classes[] = 'disabled';
+            }
+            if (get_class($task) == $lastchanged) {
+                $classes[] = 'table-primary';
             }
+            $row->attributes['class'] = implode(' ', $classes);
             $data[] = $row;
         }
         $table->data = $data;
+        if ($lastchanged) {
+            // IE does not support this, and the ancient version of Firefox we use for Behat
+            // has the method, but then errors on 'centre'. So, just try to scroll, and if it fails, don't care.
+            $this->page->requires->js_init_code(
+                    'try{document.querySelector("tr.table-primary").scrollIntoView({block: "center"});}catch(e){}');
+        }
         return html_writer::table($table);
     }
 
+    /**
+     * Nicely display the name of a component, with its disabled status and internal name.
+     *
+     * @param string $component component name, e.g. 'core' or 'mod_forum'.
+     * @return string HTML.
+     */
+    public function component_name(string $component): string {
+        list($type) = core_component::normalize_component($component);
+        if ($type === 'core') {
+            return get_string('corecomponent', 'tool_task');
+        }
+
+        $plugininfo = core_plugin_manager::instance()->get_plugin_info($component);
+        if (!$plugininfo) {
+            return $component;
+        }
+
+        $plugininfo->init_display_name();
+
+        $componentname = $plugininfo->displayname;
+        if (!$plugininfo->is_enabled()) {
+            $componentname .= ' ' . html_writer::span(
+                            get_string('disabled', 'tool_task'), 'badge badge-secondary');
+        }
+        $componentname .= "\n" . html_writer::span($plugininfo->component, 'task-class text-ltr');
+
+        return $componentname;
+    }
+
+    /**
+     * Standard display of a tasks last run time.
+     *
+     * @param scheduled_task $task
+     * @return string HTML.
+     */
+    public function last_run_time(scheduled_task $task): string {
+        if ($task->get_last_run_time()) {
+            return userdate($task->get_last_run_time());
+        } else {
+            return get_string('never');
+        }
+    }
+
+    /**
+     * Standard display of a tasks next run time.
+     *
+     * @param scheduled_task $task
+     * @return string HTML.
+     */
+    public function next_run_time(scheduled_task $task): string {
+        $plugininfo = core_plugin_manager::instance()->get_plugin_info($task->get_component());
+
+        $nextrun = $task->get_next_run_time();
+        if ($plugininfo && $plugininfo->is_enabled() === false && !$task->get_run_if_component_disabled()) {
+            $nextrun = get_string('plugindisabled', 'tool_task');
+        } else if ($task->get_disabled()) {
+            $nextrun = get_string('taskdisabled', 'tool_task');
+        } else if ($nextrun > time()) {
+            $nextrun = userdate($nextrun);
+        } else {
+            $nextrun = get_string('asap', 'tool_task');
+        }
+
+        return $nextrun;
+    }
+
+    /**
+     * Get a table cell to show one time, comparing it to the default.
+     *
+     * @param string $current the current setting.
+     * @param string $default the default setting from the db/tasks.php file.
+     * @return html_table_cell for use in the table.
+     */
+    protected function time_cell(string $current, string $default): html_table_cell {
+        $cell = new html_table_cell($current);
+        // Cron-style values must always be LTR.
+        $cell->attributes['class'] = 'text-ltr';
+
+        // If the current value is default, that is all we want to do.
+        if ($default === '*') {
+            if ($current === '*') {
+                return $cell;
+            }
+        } else if ($default === 'R' ) {
+            if (is_numeric($current)) {
+                return $cell;
+            }
+        } else {
+            if ($default === $current) {
+                return $cell;
+            }
+        }
+
+        // Otherwise, highlight and show the default.
+        $cell->attributes['class'] .= ' table-warning';
+        $cell->text .= ' ' . html_writer::span(
+                get_string('defaultx', 'tool_task', $default), 'task-class');
+        return $cell;
+    }
+
     /**
      * Renders a link back to the scheduled tasks page (used from the 'run now' screen).
      *
+     * @param string $taskclassname if specified, the list of tasks will scroll to show this task.
      * @return string HTML code
      */
-    public function link_back() {
-        return $this->render_from_template('tool_task/link_back',
-                array('url' => new moodle_url('/admin/tool/task/scheduledtasks.php')));
+    public function link_back($taskclassname = '') {
+        $url = new moodle_url('/admin/tool/task/scheduledtasks.php');
+        if ($taskclassname) {
+            $url->param('lastchanged', $taskclassname);
+        }
+        return $this->render_from_template('tool_task/link_back', ['url' => $url]);
     }
 }
index fd7bc30..b404a82 100644 (file)
@@ -71,9 +71,10 @@ echo $OUTPUT->heading($task->get_name());
 if (!optional_param('confirm', 0, PARAM_INT)) {
     echo $OUTPUT->confirm(get_string('runnow_confirm', 'tool_task', $task->get_name()),
             new single_button(new moodle_url('/admin/tool/task/schedule_task.php',
-            array('task' => $taskname, 'confirm' => 1, 'sesskey' => sesskey())),
+                    ['task' => $taskname, 'confirm' => 1, 'sesskey' => sesskey()]),
             get_string('runnow', 'tool_task')),
-            new single_button(new moodle_url('/admin/tool/task/scheduledtasks.php'),
+            new single_button(new moodle_url('/admin/tool/task/scheduledtasks.php',
+                    ['lastchanged' => get_class($task)]),
             get_string('cancel'), false));
     echo $OUTPUT->footer();
     exit;
@@ -97,6 +98,6 @@ $output = $PAGE->get_renderer('tool_task');
 echo $OUTPUT->single_button(new moodle_url('/admin/tool/task/schedule_task.php',
         array('task' => $taskname, 'confirm' => 1, 'sesskey' => sesskey())),
         get_string('runagain', 'tool_task'));
-echo $output->link_back();
+echo $output->link_back(get_class($task));
 
 echo $OUTPUT->footer();
index 90d8b8d..d256f3d 100644 (file)
@@ -26,19 +26,12 @@ require_once(__DIR__ . '/../../../config.php');
 require_once($CFG->libdir.'/adminlib.php');
 require_once($CFG->libdir.'/tablelib.php');
 
-$PAGE->set_url('/admin/tool/task/scheduledtasks.php');
-$PAGE->set_context(context_system::instance());
-$PAGE->set_pagelayout('admin');
-$strheading = get_string('scheduledtasks', 'tool_task');
-$PAGE->set_title($strheading);
-$PAGE->set_heading($strheading);
-
-require_admin();
-
-$renderer = $PAGE->get_renderer('tool_task');
+admin_externalpage_setup('scheduledtasks');
 
 $action = optional_param('action', '', PARAM_ALPHAEXT);
 $taskname = optional_param('task', '', PARAM_RAW);
+$lastchanged = optional_param('lastchanged', '', PARAM_RAW);
+
 $task = null;
 $mform = null;
 
@@ -55,15 +48,16 @@ if ($action == 'edit') {
 
 if ($task) {
     $mform = new tool_task_edit_scheduled_task_form(null, $task);
+    $nexturl = new moodle_url($PAGE->url, ['lastchanged' => $taskname]);
 }
 
+$renderer = $PAGE->get_renderer('tool_task');
+
 if ($mform && ($mform->is_cancelled() || !empty($CFG->preventscheduledtaskchanges))) {
-    redirect(new moodle_url('/admin/tool/task/scheduledtasks.php'));
+    redirect($nexturl);
 } else if ($action == 'edit' && empty($CFG->preventscheduledtaskchanges)) {
 
     if ($data = $mform->get_data()) {
-
-
         if ($data->resettodefaults) {
             $defaulttask = \core\task\manager::get_default_scheduled_task($taskname);
             $task->set_minute($defaulttask->get_minute());
@@ -85,13 +79,16 @@ if ($mform && ($mform->is_cancelled() || !empty($CFG->preventscheduledtaskchange
 
         try {
             \core\task\manager::configure_scheduled_task($task);
-            redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS);
+            redirect($nexturl, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS);
         } catch (Exception $e) {
-            redirect($PAGE->url, $e->getMessage(), null, \core\output\notification::NOTIFY_ERROR);
+            redirect($nexturl, $e->getMessage(), null, \core\output\notification::NOTIFY_ERROR);
         }
     } else {
         echo $OUTPUT->header();
         echo $OUTPUT->heading(get_string('edittaskschedule', 'tool_task', $task->get_name()));
+        echo html_writer::div('\\' . get_class($task), 'task-class text-ltr');
+        echo html_writer::div(get_string('fromcomponent', 'tool_task',
+                $renderer->component_name($task->get_component())));
         $mform->display();
         echo $OUTPUT->footer();
     }
@@ -99,6 +96,6 @@ if ($mform && ($mform->is_cancelled() || !empty($CFG->preventscheduledtaskchange
 } else {
     echo $OUTPUT->header();
     $tasks = core\task\manager::get_all_scheduled_tasks();
-    echo $renderer->scheduled_tasks_table($tasks);
+    echo $renderer->scheduled_tasks_table($tasks, $lastchanged);
     echo $OUTPUT->footer();
 }
index aaa36d9..643750b 100644 (file)
@@ -9,6 +9,11 @@ Feature: Clear scheduled task fail delay
     And I log in as "admin"
     And I navigate to "Server > Tasks > Scheduled tasks" in site administration
 
+  Scenario: Any fail delay is highlighted
+    Then I should see "60" in the "Send new user passwords" "table_row"
+    And I should see "Clear" in the "Send new user passwords" "table_row"
+    And I should see "60" in the "td.table-danger" "css_element"
+
   Scenario: Clear fail delay
     When I click on "Clear" "text" in the "Send new user passwords" "table_row"
     And I should see "Are you sure you want to clear the fail delay"
@@ -16,6 +21,8 @@ Feature: Clear scheduled task fail delay
 
     Then I should not see "60" in the "Send new user passwords" "table_row"
     And I should not see "Clear" in the "Send new user passwords" "table_row"
+    And I should see "Send new user passwords" in the "tr.table-primary" "css_element"
+
 
   Scenario: Cancel clearing the fail delay
     When I click on "Clear" "text" in the "Send new user passwords" "table_row"
@@ -23,3 +30,4 @@ Feature: Clear scheduled task fail delay
 
     Then I should see "60" in the "Send new user passwords" "table_row"
     And I should see "Clear" in the "Send new user passwords" "table_row"
+    And I should see "Send new user passwords" in the "tr.table-primary" "css_element"
index 4da19e5..160451d 100644 (file)
@@ -16,6 +16,7 @@ Feature: Manage scheduled tasks
     And I press "Save changes"
     Then I should see "Changes saved"
     And I should see "Task disabled" in the "Log table cleanup" "table_row"
+    And I should see "Log table cleanup" in the "tr.table-primary" "css_element"
 
   Scenario: Enable scheduled task
     When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row"
@@ -25,10 +26,20 @@ Feature: Manage scheduled tasks
     And I press "Save changes"
     Then I should see "Changes saved"
     And I should not see "Task disabled" in the "Log table cleanup" "table_row"
+    And I should see "Log table cleanup" in the "tr.table-primary" "css_element"
 
   Scenario: Edit scheduled task
     When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row"
     Then I should see "Edit task schedule: Log table cleanup"
+    And I should see "\logstore_standard\task\cleanup_task"
+    And I should see "From component: Standard log"
+    And I should see "logstore_standard"
+    And I should see "Default: R" in the "Minute" "fieldset"
+    And I should see "Default: *" in the "Day" "fieldset"
+    And I set the following fields to these values:
+      | minute               | frog |
+    And I press "Save changes"
+    And I should see "Data submitted is invalid"
     And I set the following fields to these values:
       | minute               | */5 |
       | hour                 | 1   |
@@ -36,10 +47,12 @@ Feature: Manage scheduled tasks
       | month                | 3   |
       | dayofweek            | 4   |
     And I press "Save changes"
-    Then I should see "Changes saved"
+    And I should see "Changes saved"
     And the following should exist in the "admintable" table:
-      | Component    | Minute | Hour | Day | Day of week | Month |
-      | Standard log | */5    | 1    | 2   | 4           | 3     |
+      | Component                      | Minute         | Hour         | Day          | Day of week  | Month        |
+      | Standard log logstore_standard | */5 Default: R | 1 Default: 4 | 2 Default: * | 4 Default: * | 3 Default: * |
+    And I should see "Log table cleanup" in the "tr.table-primary" "css_element"
+    And I should see "*/5 Default: R" in the "td.table-warning" "css_element"
 
   Scenario: Reset scheduled task to default
     When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row"
@@ -50,4 +63,5 @@ Feature: Manage scheduled tasks
     Then I should see "Changes saved"
     And the following should not exist in the "admintable" table:
       | Name               | Component    | Minute | Hour | Day | Day of week | Month |
-      | Log table cleanup  | Standard log | */5    | 1    | 2   | 4           | 3     |
\ No newline at end of file
+      | Log table cleanup  | Standard log | */5    | 1    | 2   | 4           | 3     |
+    And I should see "Log table cleanup" in the "tr.table-primary" "css_element"
diff --git a/auth/none/classes/check/noauth.php b/auth/none/classes/check/noauth.php
new file mode 100644 (file)
index 0000000..7e57907
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Verifies unsupported noauth setting
+ *
+ * @package    auth_none
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace auth_none\check;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\result;
+
+/**
+ * Verifies unsupported noauth setting
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class noauth extends \core\check\check {
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/settings.php?section=manageauths'),
+            get_string('authsettings', 'admin'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        if (is_enabled_auth('none')) {
+            $status = result::ERROR;
+            $summary = get_string('checknoautherror', 'auth_none');
+        } else {
+            $status = result::OK;
+            $summary = get_string('checknoauthok', 'auth_none');
+        }
+        $details = get_string('checknoauthdetails', 'auth_none');
+
+        return new result($status, $summary, $details);
+    }
+}
+
index a4ba89d..4ae1975 100644 (file)
@@ -25,3 +25,7 @@
 $string['auth_nonedescription'] = 'Users can sign in and create valid accounts immediately, with no authentication against an external server and no confirmation via email.  Be careful using this option - think of the security and administration problems this could cause.';
 $string['pluginname'] = 'No authentication';
 $string['privacy:metadata'] = 'The No authentication plugin does not store any personal data.';
+$string['checknoauthdetails'] = '<p>The <em>No authentication</em> plugin is not intended for production sites. Please disable it unless this is a development test site.</p>';
+$string['checknoautherror'] = 'The No authentication plugin cannot be used on production sites.';
+$string['checknoauth'] = 'No authentication';
+$string['checknoauthok'] = 'The no authentication plugin is disabled.';
diff --git a/auth/none/lib.php b/auth/none/lib.php
new file mode 100644 (file)
index 0000000..7c6b551
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Anybody can login with any password.
+ *
+ * @package    auth_none
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Add security check to make sure this isn't on in production.
+ *
+ * @return array check
+ */
+function auth_none_security_checks() {
+    return [new auth_none\check\noauth()];
+}
+
index b09772e..4dea016 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2019111800;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2019111801;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2019111200;        // Requires this Moodle version
 $plugin->component = 'auth_none';       // Full name of the plugin (used for diagnostics)
index c531260..d9bdbe1 100644 (file)
@@ -277,7 +277,7 @@ class core_backup_renderer extends plugin_renderer_base {
      */
     public function course_selector(moodle_url $nextstageurl, $wholecourse = true, restore_category_search $categories = null,
                                     restore_course_search $courses = null, $currentcourse = null) {
-        global $CFG, $PAGE;
+        global $CFG;
         require_once($CFG->dirroot.'/course/lib.php');
 
         // These variables are used to check if the form using this function was submitted.
index 8920393..e1c471f 100644 (file)
@@ -49,7 +49,7 @@ class core_badges_assertion {
     private $_url;
 
     /** @var int $obversion to control version JSON-LD. */
-    private $_obversion = OPEN_BADGES_V1;
+    private $_obversion = OPEN_BADGES_V2;
 
     /**
      * Constructs with issued badge unique hash.
@@ -57,7 +57,7 @@ class core_badges_assertion {
      * @param string $hash Badge unique hash from badge_issued table.
      * @param int $obversion to control version JSON-LD.
      */
-    public function __construct($hash, $obversion = OPEN_BADGES_V1) {
+    public function __construct($hash, $obversion = OPEN_BADGES_V2) {
         global $DB;
 
         $this->_data = $DB->get_record_sql('
@@ -198,11 +198,8 @@ class core_badges_assertion {
             $class['image'] = 'data:image/png;base64,' . $imagedata;
             $class['criteria'] = $this->_url->out(false); // Currently issued badge URL.
             if ($issued) {
-                if ($this->_obversion == OPEN_BADGES_V2) {
-                    $issuerurl = new moodle_url('/badges/issuer_json.php', array('id' => $this->get_badge_id()));
-                } else {
-                    $issuerurl = new moodle_url('/badges/assertion.php', array('b' => $this->_data->uniquehash, 'action' => 0));
-                }
+                $params = ['id' => $this->get_badge_id(), 'obversion' => $this->_obversion];
+                $issuerurl = new moodle_url('/badges/issuer_json.php', $params);
                 $class['issuer'] = $issuerurl->out(false);
             }
             $this->embed_data_badge_version2($class, OPEN_BADGES_V2_TYPE_BADGE);
@@ -223,7 +220,7 @@ class core_badges_assertion {
         $issuer = array();
         if ($this->_data) {
             // Required.
-            if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) {
+            if ($this->_obversion == OPEN_BADGES_V1) {
                 $issuer['name'] = $this->_data->issuername;
                 $issuer['url'] = $this->_data->issuerurl;
                 // Optional.
index a3dfdba..9bd9d19 100644 (file)
@@ -925,17 +925,28 @@ class badge {
     /**
      * Define issuer information by format Open Badges specification version 2.
      *
+     * @param int $obversion OB version to use.
      * @return array Issuer informations of the badge.
      */
-    public function get_badge_issuer() {
-        $issuer = array();
-        $issuer['name'] = $this->issuername;
-        $issuer['url'] = $this->issuerurl;
-        $issuer['email'] = $this->issuercontact;
-        $issuer['@context'] = OPEN_BADGES_V2_CONTEXT;
-        $issueridurl = new moodle_url('/badges/issuer_json.php', array('id' => $this->id));
-        $issuer['id'] = $issueridurl->out(false);
-        $issuer['type'] = OPEN_BADGES_V2_TYPE_ISSUER;
+    public function get_badge_issuer(?int $obversion = null) {
+        global $DB;
+
+        $issuer = [];
+        if ($obversion == OPEN_BADGES_V1) {
+            $data = $DB->get_record('badge', ['id' => $this->id]);
+            $issuer['name'] = $data->issuername;
+            $issuer['url'] = $data->issuerurl;
+            $issuer['email'] = $data->issuercontact;
+        } else {
+            $issuer['name'] = $this->issuername;
+            $issuer['url'] = $this->issuerurl;
+            $issuer['email'] = $this->issuercontact;
+            $issuer['@context'] = OPEN_BADGES_V2_CONTEXT;
+            $issueridurl = new moodle_url('/badges/issuer_json.php', array('id' => $this->id));
+            $issuer['id'] = $issueridurl->out(false);
+            $issuer['type'] = OPEN_BADGES_V2_TYPE_ISSUER;
+        }
+
         return $issuer;
     }
 }
index ebbf74f..87fe6bd 100644 (file)
@@ -30,6 +30,8 @@ require_once($CFG->libdir . '/badgeslib.php');
 
 
 $id = optional_param('id', null, PARAM_INT);
+// OB specification version. If it's not defined, the site will be used as default.
+$obversion = optional_param('obversion', badges_open_badges_backpack_api(), PARAM_INT);
 
 if (empty($id)) {
     // Get the default issuer for this site.
@@ -38,7 +40,7 @@ if (empty($id)) {
     // Get the issuer for this badge.
     $badge = new badge($id);
     if ($badge->status != BADGE_STATUS_INACTIVE) {
-        $json = $badge->get_badge_issuer();
+        $json = $badge->get_badge_issuer($obversion);
     } else {
         // The badge doen't exist or not accessible for the users.
         header("HTTP/1.0 410 Gone");
index a95f9cc..d3c2a6b 100644 (file)
@@ -599,7 +599,7 @@ class core_badges_renderer extends plugin_renderer_base {
      * @return string
      */
     protected function render_badge_user_collection(\core_badges\output\badge_user_collection $badges) {
-        global $CFG, $USER, $SITE, $OUTPUT;
+        global $CFG, $USER, $SITE;
         $backpack = $badges->backpack;
         $mybackpack = new moodle_url('/badges/mybackpack.php');
 
@@ -645,7 +645,7 @@ class core_badges_renderer extends plugin_renderer_base {
             $externalhtml .= $this->output->heading_with_help(get_string('externalbadges', 'badges'), 'externalbadges', 'badges');
             if (!is_null($backpack)) {
                 if ($backpack->backpackid != $CFG->badges_site_backpack) {
-                    $externalhtml .= $OUTPUT->notification(get_string('backpackneedsupdate', 'badges'), 'warning');
+                    $externalhtml .= $this->output->notification(get_string('backpackneedsupdate', 'badges'), 'warning');
 
                 }
                 if ($backpack->totalcollections == 0) {
index 1e379e8..d3d3541 100644 (file)
@@ -75,6 +75,10 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
 
         $this->badgeid = $DB->insert_record('badge', $fordb, true);
 
+        // Set the default Issuer (because OBv2 needs them).
+        set_config('badges_defaultissuername', $fordb->issuername);
+        set_config('badges_defaultissuercontact', $fordb->issuercontact);
+
         // Create a course with activity and auto completion tracking.
         $this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
         $this->user = $this->getDataGenerator()->create_user();
@@ -670,7 +674,7 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
 
         // Get assertion.
         $award = reset($awards);
-        $assertion = new core_badges_assertion($award->uniquehash);
+        $assertion = new core_badges_assertion($award->uniquehash, OPEN_BADGES_V1);
         $testassertion = $this->assertion;
 
         // Make sure JSON strings have the same structure.
index c2a4c00..81ecdb7 100644 (file)
@@ -16,8 +16,9 @@ Feature: Add badges to the system
     And I press "Save changes"
     And I follow "Badges"
     When I follow "Add a new badge"
-    Then the field "issuercontact" matches value "testuser@example.com"
-    And the field "issuername" matches value "Test Badge Site"
+    And I press "Issuer details"
+    Then I should see "testuser@example.com"
+    And I should see "Test Badge Site"
 
   @javascript
   Scenario: Accessing the badges
@@ -38,8 +39,6 @@ Feature: Add badges to the system
       | Description | Test badge description |
       | Image author | http://author.example.com |
       | Image caption | Test caption image |
-      | issuername | Test Badge Site |
-      | issuercontact | testuser@example.com |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     When I press "Create badge"
     Then I should see "Edit details"
@@ -62,8 +61,6 @@ Feature: Add badges to the system
       | Description | Test badge related description |
       | Image author | http://author.example.com |
       | Image caption | Test caption image |
-      | issuername | Test Badge Site |
-      | issuercontact | testuser@example.com |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I wait until the page is ready
@@ -77,8 +74,6 @@ Feature: Add badges to the system
       | Description | Test badge description |
       | Image author | http://author.example.com |
       | Image caption | Test caption image |
-      | issuername | Test Badge Site |
-      | issuercontact | testuser@example.com |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I follow "Related badges (0)"
@@ -101,8 +96,6 @@ Feature: Add badges to the system
       | Description | Test badge description |
       | Image author | http://author.example.com |
       | Image caption | Test caption image |
-      | issuername | Test Badge Site |
-      | issuercontact | testuser@example.com |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     When I press "Create badge"
     Then I should see "Edit details"
@@ -127,8 +120,6 @@ Feature: Add badges to the system
       | Description | Test badge description |
       | Image author | http://author.example.com |
       | Image caption | Test caption image |
-      | issuername | Test Badge Site |
-      | issuercontact | testuser@example.com |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     When I press "Create badge"
     Then I should see "Test Badge"
@@ -161,8 +152,6 @@ Feature: Add badges to the system
       | Description | Test badge description |
       | Image author | http://author.example.com |
       | Image caption | Test caption image |
-      | issuername | Test Badge Site |
-      | issuercontact | testuser@example.com |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     Then I should see "Edit details"
index 079b2c6..313a9a5 100644 (file)
@@ -25,7 +25,6 @@ Feature: Award badges
     And I set the following fields to these values:
       | Name | Course Badge 1 |
       | Description | Course badge 1 description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Manual issue by role"
@@ -43,7 +42,6 @@ Feature: Award badges
     And I set the following fields to these values:
       | Name | Course Badge 2 |
       | Description | Course badge 2 description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     # Set "course badge 1" as criteria
@@ -102,8 +100,6 @@ Feature: Award badges
     And I set the following fields to these values:
       | Name | Profile Badge |
       | Description | Test badge description |
-      | issuername | Test Badge Site |
-      | issuercontact | testuser@example.com |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Profile completion"
@@ -140,7 +136,6 @@ Feature: Award badges
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Manual issue by role"
@@ -183,7 +178,6 @@ Feature: Award badges
     And I set the following fields to these values:
       | Name | Course Badge |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Manual issue by role"
@@ -235,7 +229,6 @@ Feature: Award badges
     And I set the following fields to these values:
       | Name | Course Badge |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Activity completion"
@@ -290,7 +283,6 @@ Feature: Award badges
     And I set the following fields to these values:
       | Name | Course Badge |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Course completion"
@@ -340,7 +332,6 @@ Feature: Award badges
     And I set the following fields to these values:
       | Name | Course Badge 1 |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Manual issue by role"
@@ -366,7 +357,6 @@ Feature: Award badges
     And I set the following fields to these values:
       | Name | Course Badge 2 |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Manual issue by role"
@@ -423,7 +413,6 @@ Feature: Award badges
     And I set the following fields to these values:
       | Name | Course Badge |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Manual issue by role"
index 90683b3..67c5b6e 100644 (file)
@@ -41,7 +41,6 @@ Feature: Award badges with separate groups
     And I set the following fields to these values:
       | Name | Course Badge |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Manual issue by role"
index b71fcea..55f9254 100644 (file)
@@ -40,7 +40,6 @@ Feature: Award badges based on activity completion
     And I set the following fields to these values:
       | Name | Course Badge |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Activity completion"
index e497144..e0578de 100644 (file)
@@ -23,7 +23,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
@@ -59,7 +58,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
@@ -100,7 +98,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
@@ -137,7 +134,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
@@ -188,7 +184,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
@@ -245,7 +240,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
@@ -302,7 +296,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
@@ -360,7 +353,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge 1 |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
@@ -373,7 +365,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge 2 |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
@@ -415,7 +406,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge 1 |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
@@ -430,7 +420,6 @@ Feature: Award badges based on cohort
     And I set the following fields to these values:
       | Name | Site Badge 2 |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Cohort membership"
index 88bc30a..f84cf82 100644 (file)
@@ -44,7 +44,6 @@ Feature: Award badges based on competency completion
     And I set the following fields to these values:
       | Name | Course Badge |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     # Set the competency as a criteria for the badge
@@ -89,7 +88,6 @@ Feature: Award badges based on competency completion
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     # Set the competency as a criteria for the badge
@@ -142,7 +140,6 @@ Feature: Award badges based on competency completion
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     # Set the competency as a criteria for the badge
index 14cc5b5..93be0ed 100644 (file)
@@ -14,7 +14,6 @@ Feature: Award badges based on user profile field
     And I set the following fields to these values:
       | Name | Site Badge |
       | Description | Site badge description |
-      | issuername | Tester of site badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Profile completion"
index e89489b..036ff5d 100644 (file)
@@ -26,7 +26,6 @@ Feature: Test role visibility for the badge administration page
     And I set the following fields to these values:
       | Name | Course Badge |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the field "type" to "Manual issue by role"
@@ -42,7 +41,6 @@ Feature: Test role visibility for the badge administration page
     And I set the following fields to these values:
       | Name | Course Badge |
       | Description | Course badge description |
-      | issuername | Tester of course badge |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I set the following fields to these values:
index ef6f892..faca603 100644 (file)
@@ -1,5 +1,10 @@
 This files describes API changes in /badges/*,
 information provided here is intended especially for developers.
+
+=== 3.9 ===
+* BADGE_BACKPACKAPIURL and BADGE_BACKPACKWEBURL are deprecated and should not be used.
+* OBv2 has been set to the default value when the obversion is not defined.
+
 === 3.7 ===
 * BADGE_BACKPACKURL is deprecated and should not be used.
 * Incorrect term "badge competencies" has been refactored to "alignments" everywhere.
index d1acd8b..7ce5b00 100644 (file)
@@ -33,23 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function badges_install_default_backpacks() {
     global $DB;
 
-    $record = new stdClass();
-    $record->backpackweburl = 'https://backpack.openbadges.org';
-    $record->backpackapiurl = 'https://backpack.openbadges.org';
-    $record->apiversion = 1;
-    $record->sortorder = 0;
-    $record->password = '';
-
-    if (!($bp = $DB->get_record('badge_external_backpack', array('backpackapiurl' => $record->backpackapiurl)))) {
-        $bpid = $DB->insert_record('badge_external_backpack', $record);
-    } else {
-        $bpid = $bp->id;
-    }
-    set_config('badges_site_backpack', $bpid);
-
-    // All existing backpacks default to V1.
-    $DB->set_field('badge_backpack', 'externalbackpackid', $bpid);
-
     $record = new stdClass();
     $record->backpackapiurl = 'https://api.badgr.io/v2';
     $record->backpackweburl = 'https://badgr.io';
@@ -57,9 +40,16 @@ function badges_install_default_backpacks() {
     $record->sortorder = 1;
     $record->password = '';
 
-    if (!$DB->record_exists('badge_external_backpack', array('backpackapiurl' => $record->backpackapiurl))) {
-        $DB->insert_record('badge_external_backpack', $record);
+    $bp = $DB->get_record('badge_external_backpack', ['backpackapiurl' => $record->backpackapiurl]);
+    if ($bp) {
+        $bpid = $bp->id;
+    } else {
+        $bpid = $DB->insert_record('badge_external_backpack', $record);
     }
 
+    set_config('badges_site_backpack', $bpid);
+
+    // Set external backpack to v2.
+    $DB->set_field('badge_backpack', 'externalbackpackid', $bpid);
 }
 
index 5d6f205..6ecdbcd 100644 (file)
@@ -24,7 +24,7 @@ Feature: Add a bookmarks to an admin pages
     And I navigate to "Notifications" in site administration
     And I click on "Scheduled tasks" "link" in the "Admin bookmarks" "block"
     # Verify that we are on the right page.
-    Then I should see "Scheduled tasks" in the "h1" "css_element"
+    Then I should see "Day of week" in the "admintable" "table"
 
   Scenario: Admin page can be removed from bookmarks
     Given I log in as "admin"
index 8fb51b6..96eefc6 100644 (file)
@@ -67,7 +67,7 @@ class block_badges extends block_base {
     }
 
     public function get_content() {
-        global $USER, $PAGE, $CFG;
+        global $USER, $CFG;
 
         if ($this->content !== null) {
             return $this->content;
@@ -105,4 +105,4 @@ class block_badges extends block_base {
 
         return $this->content;
     }
-}
\ No newline at end of file
+}
index cc739af..32d1626 100644 (file)
@@ -21,7 +21,6 @@ Feature: Enable Block Badges in a course
     And I set the following fields to these values:
       | id_name | Badge 1 |
       | id_description | Badge 1 |
-      | id_issuername | Teacher 1 |
     And I upload "blocks/badges/tests/fixtures/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I select "Manual issue by role" from the "Add badge criteria" singleselect
@@ -39,7 +38,6 @@ Feature: Enable Block Badges in a course
     And I set the following fields to these values:
       | id_name | Badge 2 |
       | id_description | Badge 2 |
-      | id_issuername | Teacher 1 |
     And I upload "blocks/badges/tests/fixtures/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I select "Manual issue by role" from the "Add badge criteria" singleselect
index 45d10c7..fe21e46 100644 (file)
@@ -21,7 +21,6 @@ Feature: Enable Block Badges on the dashboard and view awarded badges
     And I set the following fields to these values:
       | id_name | Badge 1 |
       | id_description | Badge 1 |
-      | id_issuername | Teacher 1 |
     And I upload "blocks/badges/tests/fixtures/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I select "Manual issue by role" from the "Add badge criteria" singleselect
index 89a99de..388f4a3 100644 (file)
@@ -26,7 +26,6 @@ Feature: Enable Block Badges on the frontpage and view awarded badges
     And I set the following fields to these values:
       | id_name | Badge 1 |
       | id_description | Badge 1 |
-      | id_issuername | Teacher 1 |
     And I upload "blocks/badges/tests/fixtures/badge.png" file to "Image" filemanager
     And I press "Create badge"
     And I select "Manual issue by role" from the "Add badge criteria" singleselect
index b36facc..a54d5cc 100644 (file)
@@ -46,7 +46,7 @@ class block_comments extends block_base {
     }
 
     function get_content() {
-        global $CFG, $PAGE;
+        global $CFG;
         if ($this->content !== NULL) {
             return $this->content;
         }
@@ -64,10 +64,10 @@ class block_comments extends block_base {
         if (empty($this->instance)) {
             return $this->content;
         }
-        list($context, $course, $cm) = get_context_info_array($PAGE->context->id);
+        list($context, $course, $cm) = get_context_info_array($this->page->context->id);
 
         $args = new stdClass;
-        $args->context   = $PAGE->context;
+        $args->context   = $this->page->context;
         $args->course    = $course;
         $args->area      = 'page_comments';
         $args->itemid    = 0;
index fba8113..dcebb01 100644 (file)
@@ -41,7 +41,6 @@ class block_private_files extends block_base {
     }
 
     function get_content() {
-        global $CFG, $USER, $PAGE, $OUTPUT;
 
         if ($this->content !== NULL) {
             return $this->content;
@@ -62,7 +61,7 @@ class block_private_files extends block_base {
             $this->content->text = $renderer->private_files_tree();
             if (has_capability('moodle/user:manageownfiles', $this->context)) {
                 $this->content->footer = html_writer::link(
-                    new moodle_url('/user/files.php', array('returnurl' => $PAGE->url->out())),
+                    new moodle_url('/user/files.php', array('returnurl' => $this->page->url->out())),
                     get_string('privatefilesmanage') . '...');
             }
 
index 06052c7..3f3e292 100644 (file)
@@ -59,7 +59,6 @@
      * @return block_rss_client\output\footer|null The renderable footer or null if none should be displayed.
      */
     protected function get_footer($feedrecords) {
-        global $PAGE;
         $footer = null;
 
         if ($this->config->block_rss_client_show_channel_link) {
@@ -80,7 +79,8 @@
                 if ($footer === null) {
                     $footer = new block_rss_client\output\footer();
                 }
-                $manageurl = new moodle_url('/blocks/rss_client/managefeeds.php', ['courseid' => $PAGE->course->id]);
+                $manageurl = new moodle_url('/blocks/rss_client/managefeeds.php',
+                        ['courseid' => $this->page->course->id]);
                 $footer->set_failed($manageurl);
             }
         }
index 96f79f9..e6d9c65 100644 (file)
@@ -90,8 +90,7 @@ class block_settings extends block_base {
     }
 
     function get_required_javascript() {
-        global $PAGE;
-        $adminnode = $PAGE->settingsnav->find('siteadministration', navigation_node::TYPE_SITE_ADMIN);
+        $adminnode = $this->page->settingsnav->find('siteadministration', navigation_node::TYPE_SITE_ADMIN);
         parent::get_required_javascript();
         $arguments = array(
             'instanceid' => $this->instance->id,
index 5da7105..51f7e55 100644 (file)
@@ -616,6 +616,12 @@ $CFG->admin = 'admin';
 //
 //      $CFG->expectedcronfrequency = 200;
 //
+// Moodle 3.9+ checks how old tasks are in the ad hoc queue and warns at 10 minutes
+// and errors at 4 hours. Set these to override these limits:
+//
+//      $CFG->adhoctaskagewarn = 10 * 60;
+//      $CFG->adhoctaskageerror = 4 * 60 * 60;
+//
 // Session lock warning threshold. Long running pages should release the session using \core\session\manager::write_close().
 // Set this threshold to any value greater than 0 to add developer warnings when a page locks the session for too long.
 // The session should rarely be locked for more than 1 second. The input should be in seconds and may be a float.
index 56f6374..ea7a72e 100644 (file)
Binary files a/course/amd/build/activitychooser.min.js and b/course/amd/build/activitychooser.min.js differ
index 9201790..f410d70 100644 (file)
Binary files a/course/amd/build/activitychooser.min.js.map and b/course/amd/build/activitychooser.min.js.map differ
index 65c611f..92a8164 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js and b/course/amd/build/local/activitychooser/dialogue.min.js differ
index 8ea4836..6a85405 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js.map and b/course/amd/build/local/activitychooser/dialogue.min.js.map differ
index 37df273..bc9cee0 100644 (file)
Binary files a/course/amd/build/local/activitychooser/selectors.min.js and b/course/amd/build/local/activitychooser/selectors.min.js differ
index 75707c6..f6b3784 100644 (file)
Binary files a/course/amd/build/local/activitychooser/selectors.min.js.map and b/course/amd/build/local/activitychooser/selectors.min.js.map differ
index cb58e9f..0207281 100644 (file)
@@ -176,20 +176,28 @@ const buildModal = data => {
  * @param {HTMLElement} modalBody Our current modals' body
  */
 const nullFavouriteDomManager = (favouriteTabNav, modalBody) => {
+    favouriteTabNav.tabIndex = -1;
     favouriteTabNav.classList.add('d-none');
     // Need to set active to an available tab.
     if (favouriteTabNav.classList.contains('active')) {
         favouriteTabNav.classList.remove('active');
+        favouriteTabNav.setAttribute('aria-selected', 'false');
         const favouriteTab = modalBody.querySelector(selectors.regions.favouriteTab);
         favouriteTab.classList.remove('active');
         const recommendedTabNav = modalBody.querySelector(selectors.regions.recommendedTabNav);
         const defaultTabNav = modalBody.querySelector(selectors.regions.defaultTabNav);
         if (recommendedTabNav.classList.contains('d-none') === false) {
             recommendedTabNav.classList.add('active');
+            recommendedTabNav.setAttribute('aria-selected', 'true');
+            recommendedTabNav.tabIndex = 0;
+            recommendedTabNav.focus();
             const recommendedTab = modalBody.querySelector(selectors.regions.recommendedTab);
             recommendedTab.classList.add('active');
         } else {
             defaultTabNav.classList.add('active');
+            defaultTabNav.setAttribute('aria-selected', 'true');
+            defaultTabNav.tabIndex = 0;
+            defaultTabNav.focus();
             const defaultTab = modalBody.querySelector(selectors.regions.defaultTab);
             defaultTab.classList.add('active');
         }
index 9f803c3..6a476cf 100644 (file)
@@ -108,7 +108,7 @@ const manageFavouriteState = async(modalBody, caller, partialFavourite) => {
  * @param {Function} partialFavourite Partially applied function we need to manage favourite status
  */
 const registerListenerEvents = (modal, mappedModules, partialFavourite) => {
-    const bodyClickListener = e => {
+    const bodyClickListener = async(e) => {
         if (e.target.closest(selectors.actions.optionActions.showSummary)) {
             const carousel = $(modal.getBody()[0].querySelector(selectors.regions.carousel));
 
@@ -120,7 +120,14 @@ const registerListenerEvents = (modal, mappedModules, partialFavourite) => {
 
         if (e.target.closest(selectors.actions.optionActions.manageFavourite)) {
             const caller = e.target.closest(selectors.actions.optionActions.manageFavourite);
-            manageFavouriteState(modal.getBody()[0], caller, partialFavourite);
+            await manageFavouriteState(modal.getBody()[0], caller, partialFavourite);
+            const activeSectionId = modal.getBody()[0].querySelector(selectors.elements.activetab).getAttribute("href");
+            const sectionChooserOptions = modal.getBody()[0]
+                .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));
+            const firstChooserOption = sectionChooserOptions
+                .querySelector(selectors.regions.chooserOption.container);
+            toggleFocusableChooserOption(firstChooserOption, true);
+            initChooserOptionsKeyboardNavigation(modal.getBody()[0], mappedModules, sectionChooserOptions);
         }
 
         // From the help screen go back to the module overview.
@@ -208,49 +215,51 @@ const initTabsKeyboardNavigation = (body) => {
     const defaultTabNav = body.querySelector(selectors.regions.defaultTabNav);
     const tabNavArray = [favTabNav, recommendedTabNav, defaultTabNav];
     tabNavArray.forEach((element) => {
-        return element.addEventListener('keyup', (e) => {
-            const firstLink = e.target.parentElement.parentElement.firstElementChild.firstElementChild;
-            const lastLink = e.target.parentElement.parentElement.lastElementChild.firstElementChild;
+        return element.addEventListener('keydown', (e) => {
+            // The first visible navigation tab link.
+            const firstLink = e.target.parentElement.querySelector(selectors.elements.visibletabs);
+            // The last navigation tab link. It would always be the default activities tab link.
+            const lastLink = e.target.parentElement.lastElementChild;
 
             if (e.keyCode === arrowRight) {
-                const nextLink = e.target.parentElement.nextElementSibling;
+                const nextLink = e.target.nextElementSibling;
                 if (nextLink === null) {
-                    e.srcElement.tabIndex = -1;
+                    e.target.tabIndex = -1;
                     firstLink.tabIndex = 0;
                     firstLink.focus();
-                } else if (nextLink.firstElementChild.classList.contains('d-none')) {
-                    e.srcElement.tabIndex = -1;
+                } else if (nextLink.classList.contains('d-none')) {
+                    e.target.tabIndex = -1;
                     lastLink.tabIndex = 0;
                     lastLink.focus();
                 } else {
-                    e.srcElement.tabIndex = -1;
-                    nextLink.firstElementChild.tabIndex = 0;
-                    nextLink.firstElementChild.focus();
+                    e.target.tabIndex = -1;
+                    nextLink.tabIndex = 0;
+                    nextLink.focus();
                 }
             }
             if (e.keyCode === arrowLeft) {
-                const previousLink = e.target.parentElement.previousElementSibling;
+                const previousLink = e.target.previousElementSibling;
                 if (previousLink === null) {
-                    e.srcElement.tabIndex = -1;
+                    e.target.tabIndex = -1;
                     lastLink.tabIndex = 0;
                     lastLink.focus();
-                } else if (previousLink.firstElementChild.classList.contains('d-none')) {
-                    e.srcElement.tabIndex = -1;
+                } else if (previousLink.classList.contains('d-none')) {
+                    e.target.tabIndex = -1;
                     firstLink.tabIndex = 0;
                     firstLink.focus();
                 } else {
-                    e.srcElement.tabIndex = -1;
-                    previousLink.firstElementChild.tabIndex = 0;
-                    previousLink.firstElementChild.focus();
+                    e.target.tabIndex = -1;
+                    previousLink.tabIndex = 0;
+                    previousLink.focus();
                 }
             }
             if (e.keyCode === home) {
-                e.srcElement.tabIndex = -1;
+                e.target.tabIndex = -1;
                 firstLink.tabIndex = 0;
                 firstLink.focus();
             }
             if (e.keyCode === end) {
-                e.srcElement.tabIndex = -1;
+                e.target.tabIndex = -1;
                 lastLink.tabIndex = 0;
                 lastLink.focus();
             }
@@ -274,7 +283,7 @@ const initChooserOptionsKeyboardNavigation = (body, mappedModules, chooserOption
     const chooserOptions = chooserOptionsContainer.querySelectorAll(selectors.regions.chooserOption.container);
 
     Array.from(chooserOptions).forEach((element) => {
-        return element.addEventListener('keyup', (e) => {
+        return element.addEventListener('keydown', (e) => {
 
             // Check for enter/ space triggers for showing the help.
             if (e.keyCode === enter || e.keyCode === space) {
index a072a1e..b8361e6 100644 (file)
@@ -82,6 +82,7 @@ export default {
         sitetopic: 'div.sitetopic',
         tab: 'a[data-toggle="tab"]',
         activetab: 'a[data-toggle="tab"][aria-selected="true"]',
+        visibletabs: 'a[data-toggle="tab"]:not(.d-none)',
         searchicon: '.searchbar-append .search-icon',
         clearsearch: '.searchbar-append .clear'
     },
index 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 c7e6941..8d56504 100644 (file)
@@ -40,14 +40,17 @@ M.course.format.swap_sections = function(Y, node1, node2) {
     };
 
     var sectionlist = Y.Node.all('.'+CSS.COURSECONTENT+' '+M.course.format.get_section_selector(Y));
-    // Swap menus.
-    sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS));
+    // Swap the non-ajax menus, noting these are not always present (depends on theme and user prefs).
+    if (sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS)) {
+        sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS));
+    }
 }
 
 /**
  * Process sections after ajax response
  *
  * @param {YUI} Y YUI3 instance
+ * @param {NodeList} sectionlist of sections
  * @param {array} response ajax response
  * @param {string} sectionfrom first affected section
  * @param {string} sectionto last affected section
@@ -76,13 +79,14 @@ M.course.format.process_sections = function(Y, sectionlist, response, sectionfro
             // Update section title.
             var content = Y.Node.create('<span>' + response.sectiontitles[i] + '</span>');
             sectionlist.item(i).all('.'+CSS.SECTIONNAME).setHTML(content);
-            // Update move icon.
-            ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE);
-            str = ele.getAttribute('alt');
+            // Update the drag handle.
+            ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE).ancestor('.section-handle');
+            str = ele.getAttribute('title');
             stridx = str.lastIndexOf(' ');
             newstr = str.substr(0, stridx +1) + i;
-            ele.setAttribute('alt', newstr);
-            ele.setAttribute('title', newstr); // For FireFox as 'alt' is not refreshed.
+            ele.setAttribute('title', newstr);
+            // Update the aria-label for the section.
+            sectionlist.item(i).setAttribute('aria-label', content.get('innerText').trim());
         }
     }
 }
index 67551c5..5d11be9 100644 (file)
@@ -104,9 +104,7 @@ class format_topics_renderer extends format_section_renderer_base {
      * @return array of edit control items
      */
     protected function section_edit_control_items($course, $section, $onsectionpage = false) {
-        global $PAGE;
-
-        if (!$PAGE->user_is_editing()) {
+        if (!$this->page->user_is_editing()) {
             return array();
         }
 
index bac0e59..382287d 100644 (file)
@@ -11,6 +11,9 @@ Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
     - format_weeks_upgrade_remove_numsections()
     - format_weeks_upgrade_hide_extra_sections()
     - format_weeks_upgrade_add_empty_sections()
+* The non-ajax controls to add resources and activities are now rendered only when needed, such as when the user
+  preference is set, or when the theme sets $THEME->enablecourseajaxtheme to false. Formats which directly access
+  the '.section_add_menus' element or its children should be updated accordingly.
 
 === 3.8 ===
 
index 1fb0c8e..0ad69b8 100644 (file)
@@ -40,14 +40,17 @@ M.course.format.swap_sections = function(Y, node1, node2) {
     };
 
     var sectionlist = Y.Node.all('.'+CSS.COURSECONTENT+' '+M.course.format.get_section_selector(Y));
-    // Swap menus.
-    sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS));
+    // Swap the non-ajax menus, noting these are not always present (depends on theme and user prefs).
+    if (sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS)) {
+        sectionlist.item(node1).one('.' + CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.' + CSS.SECTIONADDMENUS));
+    }
 }
 
 /**
  * Process sections after ajax response
  *
  * @param {YUI} Y YUI3 instance
+ * @param {NodeList} sectionlist of sections
  * @param {array} response ajax response
  * @param {string} sectionfrom first affected section
  * @param {string} sectionto last affected section
@@ -77,13 +80,14 @@ M.course.format.process_sections = function(Y, sectionlist, response, sectionfro
             var content = Y.Node.create('<span>' + response.sectiontitles[i] + '</span>');
             sectionlist.item(i).all('.'+CSS.SECTIONNAME).setHTML(content);
 
-            // Update move icon.
-            ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE);
-            str = ele.getAttribute('alt');
+            // Update the drag handle.
+            ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE).ancestor('.section-handle');
+            str = ele.getAttribute('title');
             stridx = str.lastIndexOf(' ');
             newstr = str.substr(0, stridx +1) + i;
-            ele.setAttribute('alt', newstr);
-            ele.setAttribute('title', newstr); // For FireFox as 'alt' is not refreshed.
+            ele.setAttribute('title', newstr);
+            // Update the aria-label for the section.
+            sectionlist.item(i).setAttribute('aria-label', content.get('innerText').trim());
 
             // Remove the current class as section has been moved.
             sectionlist.item(i).removeClass('current');
index 8c0965c..43efcd4 100644 (file)
@@ -33,7 +33,7 @@
     <tbody>
         {{#categorydata}}
         <tr class="d-flex">
-            <td class="font-weight-bold col-7 c0"><span>{{{icon}}}</span>{{name}}</td>
+            <td class="col-7 c0"><span>{{{icon}}}</span>{{name}}</td>
             {{#id}}
             <td class="col-5 c1 colselect">
             <input class="activity-recommend-checkbox" type="checkbox" aria-label="{{#str}}recommendcheckbox, course, {{name}}{{/str}}" data-area="{{componentname}}" data-id="{{id}}" {{#recommended}}checked="checked"{{/recommended}}  />
index 7bd4b3d..45d9a25 100644 (file)
                     {{>core_course/local/activitychooser/search}}
                 </div>
                 <div data-region="chooser-container">
-                    <ul class="nav nav-tabs mb-3" id="activities-{{uniqid}}" role="tablist">
-                        <li class="nav-item">
-                            <a class="nav-link {{#favouritesFirst}}active{{/favouritesFirst}} {{^favourites}}d-none{{/favourites}}"
-                               id="starred-tab-{{uniqid}}"
-                               data-toggle="tab"
-                               data-region="favourite-tab-nav"
-                               href="#starred-{{uniqid}}"
-                               role="tab"
-                               aria-label="{{#str}} aria:favouritestab, core_course {{/str}}"
-                               aria-controls="starred-{{uniqid}}"
-                               aria-selected="{{#favouritesFirst}}true{{/favouritesFirst}}{{^favouritesFirst}}false{{/favouritesFirst}}"
-                               tabindex="{{#favouritesFirst}}0{{/favouritesFirst}}{{^favouritesFirst}}-1{{/favouritesFirst}}"
-                            >
-                                {{#str}} favourites, core {{/str}}
-                            </a>
-                        </li>
-                        <li class="nav-item">
-                            <a class="nav-link {{#recommendedFirst}}active{{/recommendedFirst}} {{^recommended}}d-none{{/recommended}}"
-                               id="recommended-tab-{{uniqid}}"
-                               data-region="recommended-tab-nav"
-                               data-toggle="tab"
-                               href="#recommended-{{uniqid}}"
-                               role="tab"
-                               aria-label="{{#str}} aria:recommendedtab, core_course {{/str}}"
-                               aria-controls="recommended-{{uniqid}}"
-                               aria-selected="{{#recommendedFirst}}true{{/recommendedFirst}}{{^recommendedFirst}}false{{/recommendedFirst}}"
-                               tabindex="{{#recommendedFirst}}0{{/recommendedFirst}}{{^recommendedFirst}}-1{{/recommendedFirst}}">
-                                {{#str}} recommended, core {{/str}}
-                            </a>
-                        </li>
-                        <li class="nav-item">
-                            <a class="nav-link {{#fallback}}active{{/fallback}}"
-                               id="all-tab-{{uniqid}}"
-                               data-toggle="tab"
-                               data-region="default-tab-nav"
-                               href="#all-{{uniqid}}"
-                               role="tab"
-                               aria-label="{{#str}} aria:defaulttab, core_course {{/str}}"
-                               aria-controls="all-{{uniqid}}"
-                               aria-selected="{{#fallback}}true{{/fallback}}{{^fallback}}false{{/fallback}}"
-                               tabindex="{{#fallback}}0{{/fallback}}{{^fallback}}-1{{/fallback}}"
-                            >
-                                {{#str}} activities, core {{/str}}
-                            </a>
-                        </li>
-                    </ul>
+                    <div class="nav nav-tabs mb-3" id="activities-{{uniqid}}" role="tablist">
+                        <a class="nav-item nav-link {{#favouritesFirst}}active{{/favouritesFirst}} {{^favourites}}d-none{{/favourites}}"
+                           id="starred-tab-{{uniqid}}"
+                           data-toggle="tab"
+                           data-region="favourite-tab-nav"
+                           href="#starred-{{uniqid}}"
+                           role="tab"
+                           aria-label="{{#str}} aria:favouritestab, core_course {{/str}}"
+                           aria-controls="starred-{{uniqid}}"
+                           aria-selected="{{#favouritesFirst}}true{{/favouritesFirst}}{{^favouritesFirst}}false{{/favouritesFirst}}"
+                           tabindex="{{#favouritesFirst}}0{{/favouritesFirst}}{{^favouritesFirst}}-1{{/favouritesFirst}}"
+                        >
+                            {{#str}} favourites, core {{/str}}
+                        </a>
+                        <a class="nav-item nav-link {{#recommendedFirst}}active{{/recommendedFirst}} {{^recommended}}d-none{{/recommended}}"
+                           id="recommended-tab-{{uniqid}}"
+                           data-region="recommended-tab-nav"
+                           data-toggle="tab"
+                           href="#recommended-{{uniqid}}"
+                           role="tab"
+                           aria-label="{{#str}} aria:recommendedtab, core_course {{/str}}"
+                           aria-controls="recommended-{{uniqid}}"
+                           aria-selected="{{#recommendedFirst}}true{{/recommendedFirst}}{{^recommendedFirst}}false{{/recommendedFirst}}"
+                           tabindex="{{#recommendedFirst}}0{{/recommendedFirst}}{{^recommendedFirst}}-1{{/recommendedFirst}}"
+                        >
+                            {{#str}} recommended, core {{/str}}
+                        </a>
+                        <a class="nav-item nav-link {{#fallback}}active{{/fallback}}"
+                           id="all-tab-{{uniqid}}"
+                           data-toggle="tab"
+                           data-region="default-tab-nav"
+                           href="#all-{{uniqid}}"
+                           role="tab"
+                           aria-label="{{#str}} aria:defaulttab, core_course {{/str}}"
+                           aria-controls="all-{{uniqid}}"
+                           aria-selected="{{#fallback}}true{{/fallback}}{{^fallback}}false{{/fallback}}"
+                           tabindex="{{#fallback}}0{{/fallback}}{{^fallback}}-1{{/fallback}}"
+                        >
+                            {{#str}} activities, core {{/str}}
+                        </a>
+                    </div>
                     <div class="tab-content" id="tabbed-activities-{{uniqid}}">
                         <div class="tab-pane {{#favouritesFirst}}active{{/favouritesFirst}}" id="starred-{{uniqid}}" data-region="favourites" role="tabpanel" aria-labelledby="starred-tab-{{uniqid}}">
                             <div class="optionscontainer d-flex flex-wrap p-1 mw-100 position-relative" role="menubar" data-region="chooser-options-container" data-render="favourites-area">
index 88a9706..dc8e8f1 100644 (file)
Binary files a/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-debug.js and b/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-debug.js differ
index 6ea5d1b..5782dd8 100644 (file)
Binary files a/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-min.js and b/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop-min.js differ
index 6c016e6..5d985c2 100644 (file)
Binary files a/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop.js and b/course/yui/build/moodle-course-dragdrop/moodle-course-dragdrop.js differ
index fc32f51..5864ec1 100644 (file)
@@ -116,6 +116,11 @@ Y.extend(DRAGRESOURCE, M.core.dragdrop, {
     drag_start: function(e) {
         // Get our drag object
         var drag = e.target;
+        if (drag.get('dragNode') === drag.get('node')) {
+            // We do not want to modify the contents of the real node.
+            // They will be the same during a keyboard drag and drop.
+            return;
+        }
         drag.get('dragNode').setContent(drag.get('node').get('innerHTML'));
         drag.get('dragNode').all('img.iconsmall').setStyle('vertical-align', 'baseline');
     },
index efbb73e..ac80e63 100644 (file)
@@ -16,6 +16,8 @@ Y.extend(DRAGSECTION, M.core.dragdrop, {
         this.groups = [CSS.SECTIONDRAGGABLE];
         this.samenodeclass = M.course.format.get_sectionwrapperclass();
         this.parentnodeclass = M.course.format.get_containerclass();
+        // Detect the direction of travel.
+        this.detectkeyboarddirection = true;
 
         // Check if we are in single section mode
         if (Y.Node.one('.' + CSS.JUMPMENU)) {
@@ -115,6 +117,14 @@ Y.extend(DRAGSECTION, M.core.dragdrop, {
     drag_start: function(e) {
         // Get our drag object
         var drag = e.target;
+        // This is the node that the user started to drag.
+        var node = drag.get('node');
+        // This is the container node that will follow the mouse around,
+        // or during a keyboard drag and drop the original node.
+        var dragnode = drag.get('dragNode');
+        if (node === dragnode) {
+            return;
+        }
         // Creat a dummy structure of the outer elemnents for clean styles application
         var containernode = Y.Node.create('<' + M.course.format.get_containernode() +
                 '></' + M.course.format.get_containernode() + '>');
@@ -123,10 +133,10 @@ Y.extend(DRAGSECTION, M.core.dragdrop, {
                 '></' + M.course.format.get_sectionwrappernode() + '>');
         sectionnode.addClass(M.course.format.get_sectionwrapperclass());
         sectionnode.setStyle('margin', 0);
-        sectionnode.setContent(drag.get('node').get('innerHTML'));
+        sectionnode.setContent(node.get('innerHTML'));
         containernode.appendChild(sectionnode);
-        drag.get('dragNode').setContent(containernode);
-        drag.get('dragNode').addClass(CSS.COURSECONTENT);
+        dragnode.setContent(containernode);
+        dragnode.addClass(CSS.COURSECONTENT);
     },
 
     drag_dropmiss: function(e) {
index 24e3122..2e05276 100644 (file)
@@ -197,14 +197,14 @@ class enrol_manual_plugin extends enrol_plugin {
         global $CFG, $PAGE;
         require_once($CFG->dirroot.'/cohort/lib.php');
 
+        static $called = false;
+
         $instance = null;
-        $instances = array();
         foreach ($manager->get_enrolment_instances() as $tempinstance) {
             if ($tempinstance->enrol == 'manual') {
                 if ($instance === null) {
                     $instance = $tempinstance;
                 }
-                $instances[] = array('id' => $tempinstance->id, 'name' => $this->get_instance_name($tempinstance));
             }
         }
         if (empty($instance)) {
@@ -222,7 +222,11 @@ class enrol_manual_plugin extends enrol_plugin {
         $context = context_course::instance($instance->courseid);
         $arguments = array('contextid' => $context->id);
 
-        $PAGE->requires->js_call_amd('enrol_manual/quickenrolment', 'init', array($arguments));
+        if (!$called) {
+            $called = true;
+            // Calling the following more than once will cause unexpected results.
+            $PAGE->requires->js_call_amd('enrol_manual/quickenrolment', 'init', array($arguments));
+        }
 
         return $button;
     }
index 1292a9e..ef85ebc 100644 (file)
@@ -60,7 +60,6 @@ class gradingform_guide_renderer extends plugin_renderer_base {
      */
     public function criterion_template($mode, $options, $elementname = '{NAME}', $criterion = null, $value = null,
                                        $validationerrors = null, $comments = null) {
-        global $PAGE;
 
         if ($criterion === null || !is_array($criterion) || !array_key_exists('id', $criterion)) {
             $criterion = array('id' => '{CRITERION-id}',
@@ -254,9 +253,9 @@ class gradingform_guide_renderer extends plugin_renderer_base {
                 }
 
                 // Include string for JS for the comment chooser title.
-                $PAGE->requires->string_for_js('insertcomment', 'gradingform_guide');
+                $this->page->requires->string_for_js('insertcomment', 'gradingform_guide');
                 // Include comment_chooser module.
-                $PAGE->requires->js_call_amd('gradingform_guide/comment_chooser', 'initialise',
+                $this->page->requires->js_call_amd('gradingform_guide/comment_chooser', 'initialise',
                     array($criterion['id'], $chooserbuttonid, $remarkid, $commentoptions));
             }
 
index 7062bdd..6149d5d 100644 (file)
@@ -51,8 +51,8 @@ class gradereport_user_renderer extends plugin_renderer_base {
      * @return string
      */
     public function view_user_selector($userid, $userview) {
-        global $PAGE, $USER;
-        $url = $PAGE->url;
+        global $USER;
+        $url = $this->page->url;
         if ($userid != $USER->id) {
             $url->param('userid', $userid);
         }
index 3c15d85..6b0c3a4 100644 (file)
@@ -146,7 +146,6 @@ class core extends \H5PCore {
     public static function get_scripts(): array {
         global $PAGE;
 
-        $factory = new factory();
         $jsrev = $PAGE->requires->get_jsrev();
         $urls = [];
         foreach (self::$scripts as $script) {
@@ -224,10 +223,6 @@ class core extends \H5PCore {
     public function fetch_content_type(array $library): ?int {
         $factory = new factory();
 
-        // Get a temp path to download the content type.
-        $temppath = make_request_directory();
-        $tempfile = "{$temppath}/" . $library['machineName'] . ".h5p";
-
         // Download the latest content type from the H5P official repository.
         $fs = get_file_storage();
         $file = $fs->create_file_from_url(
@@ -287,6 +282,8 @@ class core extends \H5PCore {
      *     - array contentTypes: an object for each H5P content type with its information
      */
     public function get_latest_content_types(): \stdClass {
+        global $CFG;
+
         $siteuuid = $this->get_site_uuid() ?? md5($CFG->wwwroot);
         $postdata = ['uuid' => $siteuuid];
 
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'] = 'سنڌي‎';
diff --git a/install/lang/se/error.php b/install/lang/se/error.php
new file mode 100644 (file)
index 0000000..69fc065
--- /dev/null
@@ -0,0 +1,35 @@
+<?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['cannotsavemd5file'] = 'Ii sáhte vurket md5-fiilla.';
+$string['cannotsavezipfile'] = 'Ii sáhte vurket ZIP-fiilla.';
+$string['wrongzipfilename'] = 'Boasttu ZIP-fiilanamma.';
index 4c072b2..a70df1c 100644 (file)
@@ -35,5 +35,6 @@ $string['chooselanguagehead'] = 'Vállje giela';
 $string['chooselanguagesub'] = 'Vállje giela (dušše installašuvdnjii). Don sáhtát válljet neahttabáikki ja geavaheaddji giela šearbmagovas maŋŋelaš.';
 $string['clialreadyconfigured'] = 'Fiila config.php gávdno jo. Geavat admin/cli/install_database.php jus áiggut installeret dán portála.';
 $string['clialreadyinstalled'] = 'Fiila config.php gávdno jo. Geavat admin/cli/install_database.php jus háliidat ođasmahttit Moodle dán portálas.';
+$string['databasename'] = 'Diehtovuođđonamma';
 $string['langdownloaderror'] = 'Dađibahábut ii installerejuvvon giellapáhkka "{$a}". Installašuvdnaproseassa joatká eaŋgalsgillii.';
 $string['phpextension'] = '{$a} PHP gilkor';
index d4a8f67..5cc4192 100644 (file)
@@ -117,6 +117,7 @@ $string['cannotuninstall'] = '{$a} can not be uninstalled.';
 $string['categoryemail'] = 'Email';
 $string['cfgwwwrootslashwarning'] = '$CFG->wwwroot is defined incorrectly in the config.php file. It includes a \'/\' character at the end which must be removed.';
 $string['cfgwwwrootwarning'] = '$CFG->wwwroot is defined incorrectly in the config.php file. It should match the URL you are using to access this page.';
+$string['checkupgradepending'] = 'Upgrade';
 $string['cleanup'] = 'Cleanup';
 $string['clianswerno'] = 'n';
 $string['cliansweryes'] = 'y';
@@ -428,10 +429,11 @@ $string['cron_link'] = 'admin/cron';
 $string['cronclionly'] = 'Cron execution via command line only';
 $string['cronerrorclionly'] = 'Sorry, internet access to this page has been disabled by the administrator.';
 $string['cronerrorpassword'] = 'Sorry, you have not provided a valid password to access this page';
-$string['croninfrequent'] = 'The time between the last two runs of the cron maintenance script was over {$a} seconds. We recommend configuring it to run more frequently.';
+$string['croninfrequent'] = 'There was {$a->actual} between the last two runs of the cron maintenance script and it should run every {$a->expected}. We recommend configuring it to run more frequently.';
 $string['cronremotepassword'] = 'Cron password for remote access';
-$string['cronwarning'] = 'The <a href="{$a}">cron.php maintenance script</a> has not been run for at least 24 hours.';
-$string['cronwarningcli'] = 'The cli/cron.php maintenance script has not been run for at least 24 hours.';
+$string['cronwarning'] = 'The <a href="{$a->url}">admin/cron.php script</a> has not been run for {$a->actual} and should run every {$a->expected}.';
+$string['cronwarningcli'] = 'The <code>admin/cli/cron.php</code> script has not been run for {$a->actual} and should run every {$a->expected}.';
+$string['cronwarningnever'] = 'The <code>admin/cli/cron.php</code> script has never been run and should run every {$a->expected}.';
 $string['ctyperequired'] = 'The ctype PHP extension is now required by Moodle, in order to improve site performance and to offer multilingual compatibility.';
 $string['curlsecurityallowedport'] = 'cURL allowed ports list';
 $string['curlsecurityallowedportsyntax'] = 'List of port numbers that cURL can connect to. Valid entries are integer numbers only. Put each entry on a new line. If left empty, then all ports are allowed. If set, in almost all cases, both 443 and 80 should be specified for cURL to connect to standard HTTPS and HTTP ports.';
index bec67cc..89c386b 100644 (file)
@@ -509,6 +509,7 @@ $string['description'] = 'Description';
 $string['descriptiona'] = 'Description: {$a}';
 $string['deselectall'] = 'Deselect all';
 $string['deselectnos'] = 'Deselect all \'No\'';
+$string['details'] = 'Details';
 $string['detailedless'] = 'Less detailed';
 $string['detailedmore'] = 'More detailed';
 $string['digitalminor'] = 'Digital minor';
@@ -790,6 +791,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 +1000,8 @@ $string['changedpassword'] = 'Changed password';
 $string['changepassword'] = 'Change password';
 $string['changessaved'] = 'Changes saved';
 $string['check'] = 'Check';
+$string['checks'] = 'Checks';
+$string['checksok'] = 'All \'{$a}\' checks ok';
 $string['checkall'] = 'Check all';
 $string['checkingbackup'] = 'Checking backup';
 $string['checkingcourse'] = 'Checking course';
@@ -1986,6 +1990,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 6d58efa..4fc5e65 100644 (file)
@@ -96,8 +96,6 @@ define('BADGE_MESSAGE_MONTHLY', 4);
 /*
  * URL of backpack. Custom ones can be added.
  */
-define('BADGE_BACKPACKAPIURL', 'https://backpack.openbadges.org');
-define('BADGE_BACKPACKWEBURL', 'https://backpack.openbadges.org');
 define('BADGRIO_BACKPACKAPIURL', 'https://api.badgr.io/v2');
 define('BADGRIO_BACKPACKWEBURL', 'https://badgr.io');
 
@@ -106,6 +104,12 @@ define('BADGRIO_BACKPACKWEBURL', 'https://badgr.io');
  */
 define('BADGE_BACKPACKURL', 'https://backpack.openbadges.org');
 
+/*
+ * @deprecated since 3.9 (MDL-66357).
+ */
+define('BADGE_BACKPACKAPIURL', 'https://backpack.openbadges.org');
+define('BADGE_BACKPACKWEBURL', 'https://backpack.openbadges.org');
+
 /*
  * Open Badges specifications.
  */
diff --git a/lib/classes/check/access/defaultuserrole.php b/lib/classes/check/access/defaultuserrole.php
new file mode 100644 (file)
index 0000000..da15a57
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Verifies sanity of default user role.
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\access;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Verifies sanity of default user role.
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class defaultuserrole extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_defaultuserrole_name', 'report_security');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        global $CFG;
+        return new \action_link(
+            new \moodle_url('/admin/roles/define.php?action=view&roleid=' . $CFG->defaultuserroleid),
+            get_string('userpolicies', 'admin'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $DB, $CFG;
+        $details = '';
+
+        if (!$defaultrole = $DB->get_record('role', ['id' => $CFG->defaultuserroleid])) {
+            $status  = result::WARNING;
+            $summary = get_string('check_defaultuserrole_notset', 'report_security');
+            return new result($status, $summary, $details);
+        }
+
+        // Risky caps - usually very dangerous.
+        $sql = "SELECT COUNT(DISTINCT rc.contextid)
+                  FROM {role_capabilities} rc
+                  JOIN {capabilities} cap ON cap.name = rc.capability
+                 WHERE " . $DB->sql_bitand('cap.riskbitmask', (RISK_XSS | RISK_CONFIG | RISK_DATALOSS)) . " <> 0
+                   AND rc.permission = :capallow
+                   AND rc.roleid = :roleid";
+
+        $riskycount = $DB->count_records_sql($sql, [
+            'capallow' => CAP_ALLOW,
+            'roleid' => $defaultrole->id,
+        ]);
+
+        // It may have either none or 'user' archetype - nothing else, or else it would break during upgrades badly.
+        if ($defaultrole->archetype === '' or $defaultrole->archetype === 'user') {
+            $legacyok = true;
+        } else {
+            $legacyok = false;
+        }
+
+        if ($riskycount or !$legacyok) {
+            $status = result::CRITICAL;
+            $summary = get_string('check_defaultuserrole_error', 'report_security', role_get_name($defaultrole));
+
+        } else {
+            $status = result::OK;
+            $summary = get_string('check_defaultuserrole_ok', 'report_security');
+        }
+
+        $details = get_string('check_defaultuserrole_details', 'report_security');
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/access/frontpagerole.php b/lib/classes/check/access/frontpagerole.php
new file mode 100644 (file)
index 0000000..5fcd3d8
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Verifies sanity of frontpage role
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\access;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Verifies sanity of frontpage role
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class frontpagerole extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_frontpagerole_name', 'report_security');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/settings.php?section=frontpagesettings#admin-defaultfrontpageroleid'),
+            get_string('frontpagesettings', 'admin'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $DB, $CFG;
+
+        if (!$frontpagerole = $DB->get_record('role', array('id' => $CFG->defaultfrontpageroleid))) {
+            $status  = result::INFO;
+            $summary = get_string('check_frontpagerole_notset', 'report_security');
+            $details = get_string('check_frontpagerole_details', 'report_security');
+            return new result($status, $summary, $details);
+        }
+
+        // Risky caps - usually very dangerous.
+        $sql = "SELECT COUNT(DISTINCT rc.contextid)
+                  FROM {role_capabilities} rc
+                  JOIN {capabilities} cap ON cap.name = rc.capability
+                 WHERE " . $DB->sql_bitand('cap.riskbitmask', (RISK_XSS | RISK_CONFIG | RISK_DATALOSS)) . " <> 0
+                   AND rc.permission = :capallow
+                   AND rc.roleid = :roleid";
+
+        $riskycount = $DB->count_records_sql($sql, [
+            'capallow' => CAP_ALLOW,
+            'roleid' => $frontpagerole->id,
+        ]);
+
+        // There is no legacy role type for frontpage yet - anyway we can not allow teachers or admins there!
+        if ($frontpagerole->archetype === 'teacher' or $frontpagerole->archetype === 'editingteacher'
+          or $frontpagerole->archetype === 'coursecreator' or $frontpagerole->archetype === 'manager') {
+            $legacyok = false;
+        } else {
+            $legacyok = true;
+        }
+
+        if ($riskycount or !$legacyok) {
+            $status  = result::CRITICAL;
+            $summary = get_string('check_frontpagerole_error', 'report_security', role_get_name($frontpagerole));
+
+        } else {
+            $status  = result::OK;
+            $summary = get_string('check_frontpagerole_ok', 'report_security');
+        }
+
+        $details = get_string('check_frontpagerole_details', 'report_security');
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/access/guestrole.php b/lib/classes/check/access/guestrole.php
new file mode 100644 (file)
index 0000000..99d1359
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Verifies sanity of guest role
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\access;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Verifies sanity of guest role
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class guestrole extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_guestrole_name', 'report_security');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/settings.php?section=userpolicies'),
+            get_string('userpolicies', 'admin'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $DB, $CFG;
+
+        if (!$guestrole = $DB->get_record('role', ['id' => $CFG->guestroleid])) {
+            $status  = result::WARNING;
+            $summary = get_string('check_guestrole_notset', 'report_security');
+            return new result($status, $summary);
+        }
+
+        // Risky caps - usually very dangerous.
+        $sql = "SELECT COUNT(DISTINCT rc.contextid)
+                  FROM {role_capabilities} rc
+                  JOIN {capabilities} cap ON cap.name = rc.capability
+                 WHERE " . $DB->sql_bitand('cap.riskbitmask', (RISK_XSS | RISK_CONFIG | RISK_DATALOSS)) . " <> 0
+                   AND rc.permission = :capallow
+                   AND rc.roleid = :roleid";
+
+        $riskycount = $DB->count_records_sql($sql, [
+            'capallow' => CAP_ALLOW,
+            'roleid' => $guestrole->id,
+        ]);
+
+        // It may have either no or 'guest' archetype - nothing else, or else it would break during upgrades badly.
+        if ($guestrole->archetype === '' or $guestrole->archetype === 'guest') {
+            $legacyok = true;
+        } else {
+            $legacyok = false;
+        }
+
+        if ($riskycount or !$legacyok) {
+            $status  = result::CRITICAL;
+            $summary = get_string('check_guestrole_error', 'report_security', format_string($guestrole->name));
+
+        } else {
+            $status  = result::OK;
+            $summary = get_string('check_guestrole_ok', 'report_security');
+        }
+
+        $details = get_string('check_guestrole_details', 'report_security');
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/access/riskadmin.php b/lib/classes/check/access/riskadmin.php
new file mode 100644 (file)
index 0000000..7c0729c
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Lists all admins.
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\access;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Lists all admins.
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class riskadmin extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_riskadmin_name', 'report_security');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/roles/admins.php'),
+            get_string('siteadministrators', 'role'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $DB, $CFG;
+        $userfields = \user_picture::fields('u');
+        $sql = "SELECT $userfields
+                  FROM {user} u
+                 WHERE u.id IN ($CFG->siteadmins)";
+
+        $admins = $DB->get_records_sql($sql);
+        $admincount = count($admins);
+
+        foreach ($admins as $uid => $user) {
+            $url = "$CFG->wwwroot/user/view.php?id=$user->id";
+            $link = \html_writer::link($url, fullname($user, true) . ' (' . s($user->email) . ')');
+            $admins[$uid] = \html_writer::tag('li' , $link);
+        }
+        $admins = \html_writer::tag('ul', implode('', $admins));
+        $status  = result::INFO;
+        $summary = get_string('check_riskadmin_ok', 'report_security', $admincount);
+        $details = get_string('check_riskadmin_detailsok', 'report_security', $admins);
+
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/access/riskbackup.php b/lib/classes/check/access/riskbackup.php
new file mode 100644 (file)
index 0000000..630efc3
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Abstract class for common properties of scheduled_task and adhoc_task.
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\access;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Lists all roles that have the ability to backup user data, as well as users
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class riskbackup extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_riskbackup_name', 'report_security');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/roles/manage.php'),
+            get_string('manageroles', 'role'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        return new riskbackup_result();
+    }
+}
+
diff --git a/lib/classes/check/access/riskbackup_result.php b/lib/classes/check/access/riskbackup_result.php
new file mode 100644 (file)
index 0000000..a234072
--- /dev/null
@@ -0,0 +1,196 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Lists all users with XSS risk
+ *
+ * It would be great to combine this with risk trusts in user table,
+ * unfortunately nobody implemented user trust UI yet :-(
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\access;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\result;
+
+/**
+ * Lists all users with XSS risk
+ *
+ * It would be great to combine this with risk trusts in user table,
+ * unfortunately nobody implemented user trust UI yet :-(
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class riskbackup_result extends \core\check\result {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+        global $DB;
+
+        $syscontext = \context_system::instance();
+
+        $params = array('capability' => 'moodle/backup:userinfo', 'permission' => CAP_ALLOW, 'contextid' => $syscontext->id);
+        $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, r.archetype
+                  FROM {role} r
+                  JOIN {role_capabilities} rc ON rc.roleid = r.id
+                 WHERE rc.capability = :capability
+                   AND rc.contextid  = :contextid
+                   AND rc.permission = :permission";
+        $this->systemroles = $DB->get_records_sql($sql, $params);
+
+        $params = array('capability' => 'moodle/backup:userinfo', 'permission' => CAP_ALLOW, 'contextid' => $syscontext->id);
+        $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, r.archetype, rc.contextid
+                  FROM {role} r
+                  JOIN {role_capabilities} rc ON rc.roleid = r.id
+                 WHERE rc.capability = :capability
+                   AND rc.contextid <> :contextid
+                   AND rc.permission = :permission";
+        $this->overriddenroles = $DB->get_records_sql($sql, $params);
+
+        // List of users that are able to backup personal info
+        // note:
+        // "sc" is context where is role assigned,
+        // "c" is context where is role overridden or system context if in role definition.
+        $params = [
+            'capability' => 'moodle/backup:userinfo',
+            'permission' => CAP_ALLOW,
+            'context1' => CONTEXT_COURSE,
+            'context2' => CONTEXT_COURSE,
+        ];
+
+        $this->sqluserinfo = "
+            FROM (SELECT DISTINCT rcx.contextid,
+                         rcx.roleid
+                    FROM {role_capabilities} rcx
+                   WHERE rcx.permission = :permission
+                     AND rcx.capability = :capability) rc
+            JOIN {context} c ON c.id = rc.contextid
+            JOIN {context} sc ON sc.contextlevel <= :context1
+            JOIN {role_assignments} ra ON ra.contextid = sc.id AND ra.roleid = rc.roleid
+            JOIN {user} u ON u.id = ra.userid AND u.deleted = 0
+           WHERE (sc.path = c.path OR
+                  sc.path LIKE " . $DB->sql_concat('c.path', "'/%'") . " OR
+                   c.path LIKE " . $DB->sql_concat('sc.path', "'/%'") . ")
+             AND c.contextlevel <= :context2";
+
+        $usercount = $DB->count_records_sql("SELECT COUNT('x') FROM (SELECT DISTINCT u.id $this->sqluserinfo) userinfo", $params);
+        $systemrolecount = empty($this->systemroles) ? 0 : count($this->systemroles);
+        $overriddenrolecount = empty($this->overriddenroles) ? 0 : count($this->overriddenroles);
+
+        if (max($usercount, $systemrolecount, $overriddenrolecount) > 0) {
+            $this->status = result::WARNING;
+        } else {
+            $this->status = result::OK;
+        }
+
+        $a = (object)array(
+            'rolecount' => $systemrolecount,
+            'overridecount' => $overriddenrolecount,
+            'usercount' => $usercount,
+        );
+        $this->summary = get_string('check_riskbackup_warning', 'report_security', $a);
+    }
+
+    /**
+     * Showing the full list of roles may be slow so defer it
+     *
+     * @return string
+     */
+    public function get_details(): string {
+
+        global $CFG, $DB;
+
+        $details = '';
+
+        // Make a list of roles.
+        if ($this->systemroles) {
+            $links = array();
+            foreach ($this->systemroles as $role) {
+                $role->name = role_get_name($role);
+                $role->url = (new \moodle_url('/admin/roles/manage.php', ['action' => 'edit', 'roleid' => $role->id]))->out();
+                $links[] = \html_writer::tag('li', get_string('check_riskbackup_editrole', 'report_security', $role));
+            }
+            $links = \html_writer::tag('ul', implode('', $links));
+            $details .= get_string('check_riskbackup_details_systemroles', 'report_security', $links);
+        }
+
+        // Make a list of overrides to roles.
+        $rolelinks2 = array();
+        if ($this->overriddenroles) {
+            $links = array();
+            foreach ($this->overriddenroles as $role) {
+                $role->name = $role->localname;
+                $context = context::instance_by_id($role->contextid);
+                $role->name = role_get_name($role, $context, ROLENAME_BOTH);
+                $role->contextname = $context->get_context_name();
+                $role->url = (new \moodle_url('/admin/roles/override.php',
+                    ['contextid' => $role->contextid, 'roleid' => $role->id]))->out();
+                $links[] = \html_writer::tag('li', get_string('check_riskbackup_editoverride', 'report_security', $role));
+            }
+            $links = \html_writer::tag('ul', implode('', $links));
+            $details .= get_string('check_riskbackup_details_overriddenroles', 'report_security', $links);
+        }
+
+        // Get a list of affected users as well.
+        $users = array();
+
+        list($sort, $sortparams) = users_order_by_sql('u');
+        $params = [
+            'capability' => 'moodle/backup:userinfo',
+            'permission' => CAP_ALLOW,
+            'context1' => CONTEXT_COURSE,
+            'context2' => CONTEXT_COURSE,
+        ];
+        $userfields = \user_picture::fields('u');
+        $rs = $DB->get_recordset_sql("
+            SELECT DISTINCT $userfields,
+                            ra.contextid,
+                            ra.roleid
+                            $this->sqluserinfo
+                   ORDER BY $sort", array_merge($params, $sortparams));
+
+        foreach ($rs as $user) {
+            $context = \context::instance_by_id($user->contextid);
+            $url = new \moodle_url('/admin/roles/assign.php', ['contextid' => $user->contextid, 'roleid' => $user->roleid]);
+            $a = (object)array(
+                'fullname' => fullname($user),
+                'url' => $url->out(),
+                'email' => s($user->email),
+                'contextname' => $context->get_context_name(),
+            );
+            $users[] = \html_writer::tag('li', get_string('check_riskbackup_unassign', 'report_security', $a));
+        }
+        $rs->close();
+        if (!empty($users)) {
+            $users = \html_writer::tag('ul', implode('', $users));
+            $details .= get_string('check_riskbackup_details_users', 'report_security', $users);
+        }
+
+        return $details;
+    }
+}
+
diff --git a/lib/classes/check/access/riskxss.php b/lib/classes/check/access/riskxss.php
new file mode 100644 (file)
index 0000000..3ce984a
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Lists all users with XSS risk
+ *
+ * It would be great to combine this with risk trusts in user table,
+ * unfortunately nobody implemented user trust UI yet :-(
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\access;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\result;
+
+/**
+ * Lists all users with XSS risk
+ *
+ * It would be great to combine this with risk trusts in user table,
+ * unfortunately nobody implemented user trust UI yet :-(
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class riskxss extends \core\check\check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_riskxss_name', 'report_security');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/roles/manage.php'),
+            get_string('manageroles', 'role'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        return new riskxss_result();
+    }
+}
+
diff --git a/lib/classes/check/access/riskxss_result.php b/lib/classes/check/access/riskxss_result.php
new file mode 100644 (file)
index 0000000..7097db6
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Lists all users with XSS risk
+ *
+ * It would be great to combine this with risk trusts in user table,
+ * unfortunately nobody implemented user trust UI yet :-(
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\access;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\result;
+
+/**
+ * Lists all users with XSS risk
+ *
+ * It would be great to combine this with risk trusts in user table,
+ * unfortunately nobody implemented user trust UI yet :-(
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class riskxss_result extends \core\check\result {
+
+    /**
+     * Constructor
+     */
+    public function __construct() {
+
+        global $DB;
+        $this->params = array('capallow' => CAP_ALLOW);
+        $this->sqlfrom = "FROM (SELECT DISTINCT rcx.contextid, rcx.roleid
+                           FROM {role_capabilities} rcx
+                           JOIN {capabilities} cap ON (cap.name = rcx.capability AND
+                                " . $DB->sql_bitand('cap.riskbitmask', RISK_XSS) . " <> 0)
+                           WHERE rcx.permission = :capallow) rc,
+                     {context} c,
+                     {context} sc,
+            {role_assignments} ra,
+                        {user} u
+                         WHERE c.id = rc.contextid
+                           AND (sc.path = c.path OR
+                                sc.path LIKE " . $DB->sql_concat('c.path', "'/%'") . " OR
+                                c.path LIKE " . $DB->sql_concat('sc.path', "'/%'") . ")
+                           AND u.id = ra.userid AND u.deleted = 0
+                           AND ra.contextid = sc.id
+                           AND ra.roleid = rc.roleid";
+
+        $count = $DB->count_records_sql("SELECT COUNT(DISTINCT u.id) $this->sqlfrom", $this->params);
+
+        if ($count == 0) {
+            $this->status = result::OK;
+        } else {
+            $this->status = result::WARNING;
+        }
+
+        $this->summary = get_string('check_riskxss_warning', 'report_security', $count);
+
+    }
+
+    /**
+     * Showing the full list of user may be slow so defer it
+     *
+     * @return string
+     */
+    public function get_details(): string {
+
+        global $CFG, $DB;
+
+        $userfields = \user_picture::fields('u');
+        $users = $DB->get_records_sql("SELECT DISTINCT $userfields $this->sqlfrom", $this->params);
+        foreach ($users as $uid => $user) {
+            $url = "$CFG->wwwroot/user/view.php?id=$user->id";
+            $link = \html_writer::link($url, fullname($user, true) . ' (' . s($user->email) . ')');
+            $users[$uid] = \html_writer::tag('li' , $link);
+        }
+        $users = \html_writer::tag('ul', implode('', $users));
+
+        return get_string('check_riskxss_details', 'report_security', $users);
+    }
+}
+
diff --git a/lib/classes/check/check.php b/lib/classes/check/check.php
new file mode 100644 (file)
index 0000000..8042fe8
--- /dev/null
@@ -0,0 +1,118 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Base class for checks
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core\check;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Base class for checks
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class check {
+
+    /**
+     * @var $string $component - The component / plugin this task belongs to.
+     *
+     * This is autopopulated by the check manager.
+     */
+    protected $component = 'core';
+
+    /**
+     * Get the frankenstyle component name
+     *
+     * @return string
+     */
+    public function get_component(): string {
+        return $this->component;
+    }
+
+    /**
+     * Get the frankenstyle component name
+     *
+     * @param string $component name
+     */
+    public function set_component(string $component) {
+        $this->component = $component;
+    }
+
+    /**
+     * Get the check's id
+     *
+     * This defaults to the base name of the class which is ok in the most
+     * cases but if you have a check which can have multiple instances then
+     * you should override this to be unique.
+     *
+     * @return string must be unique within a component
+     */
+    public function get_id(): string {
+        $class = get_class($this);
+        $id = explode("\\", $class);
+        return end($id);
+    }
+
+    /**
+     * Get the check reference
+     *
+     * @return string must be globally unique
+     */
+    public function get_ref(): string {
+        $ref = $this->get_component();
+        if (!empty($ref)) {
+            $ref .= '_';
+        }
+        $ref .= $this->get_id();
+        return $ref;
+    }
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        $id = $this->get_id();
+        return get_string("check{$id}", $this->get_component());
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return null;
+    }
+
+    /**
+     * Return the result
+     *
+     * @return result object
+     */
+    abstract public function get_result(): result;
+
+}
+
diff --git a/lib/classes/check/environment/configrw.php b/lib/classes/check/environment/configrw.php
new file mode 100644 (file)
index 0000000..32d4221
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Verifies config.php is not writable anymore after installation
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\environment;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Verifies config.php is not writable anymore after installation
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class configrw extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_configrw_name', 'report_security');
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $CFG;
+        $details = get_string('check_configrw_details', 'report_security');
+
+        if (is_writable($CFG->dirroot . '/config.php')) {
+            $status = result::WARNING;
+            $summary = get_string('check_configrw_warning', 'report_security');
+        } else {
+            $status = result::OK;
+            $summary = get_string('check_configrw_ok', 'report_security');
+        }
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/environment/displayerrors.php b/lib/classes/check/environment/displayerrors.php
new file mode 100644 (file)
index 0000000..e99c669
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Verifies displaying of errors
+ *
+ * Problem for lib files and 3rd party code because we can not disable debugging
+ * in these scripts (they do not include config.php)
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\environment;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\result;
+use core\check\check;
+
+/**
+ * Verifies displaying of errors
+ *
+ * Problem for lib files and 3rd party code because we can not disable debugging
+ * in these scripts (they do not include config.php)
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class displayerrors extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_displayerrors_name', 'report_security');
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        $details = get_string('check_displayerrors_details', 'report_security');
+
+        if (defined('WARN_DISPLAY_ERRORS_ENABLED')) {
+            $status = result::WARNING;
+            $summary = get_string('check_displayerrors_error', 'report_security');
+        } else {
+            $status = result::OK;
+            $summary = get_string('check_displayerrors_ok', 'report_security');
+        }
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/environment/environment.php b/lib/classes/check/environment/environment.php
new file mode 100644 (file)
index 0000000..cf221b7
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Environment check
+ *
+ * @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\environment;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Environment check
+ *
+ * @package    core
+ * @copyright  2020 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class environment extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('environment', 'admin');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/environment.php'),
+            get_string('environment', 'admin'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $CFG;
+
+        require_once($CFG->libdir.'/environmentlib.php');
+        list($status, $details) = check_moodle_environment($CFG->release, ENV_SELECT_NEWER);
+
+        if ($status) {
+            $summary = get_string('environmentok', 'admin');
+            $status = result::OK;
+        } else {
+            $summary = get_string('environmenterrortodo', 'admin');
+            $status = result::ERROR;
+        }
+
+        return new result($status, $summary, '');
+    }
+}
+
diff --git a/lib/classes/check/environment/nodemodules.php b/lib/classes/check/environment/nodemodules.php
new file mode 100644 (file)
index 0000000..34780d1
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Check the presence of the node_modules directory.
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\environment;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Check the presence of the node_modules directory.
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class nodemodules extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_nodemodules_name', 'report_security');
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $CFG;
+        $summary = get_string('check_nodemodules_info', 'report_security');
+        $details = get_string('check_nodemodules_details', 'report_security', ['path' => $CFG->dirroot . '/node_modules']);
+
+        if (is_dir($CFG->dirroot . '/node_modules')) {
+            $status = result::WARNING;
+        } else {
+            $status = result::OK;
+        }
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/environment/preventexecpath.php b/lib/classes/check/environment/preventexecpath.php
new file mode 100644 (file)
index 0000000..a6f90cd
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Verifies the status of preventexecpath
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\environment;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Verifies the status of preventexecpath
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class preventexecpath extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_preventexecpath_name', 'report_security');
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $CFG;
+        $details = get_string('check_preventexecpath_details', 'report_security');
+        if (empty($CFG->preventexecpath)) {
+            $status = result::WARNING;
+            $summary = get_string('check_preventexecpath_warning', 'report_security');
+        } else {
+            $status = result::OK;
+            $summary = get_string('check_preventexecpath_ok', 'report_security');
+        }
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/environment/unsecuredataroot.php b/lib/classes/check/environment/unsecuredataroot.php
new file mode 100644 (file)
index 0000000..c243e27
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Verifies fatal misconfiguration of dataroot
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\environment;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\result;
+
+/**
+ * Verifies fatal misconfiguration of dataroot
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class unsecuredataroot extends \core\check\check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_unsecuredataroot_name', 'report_security');
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+
+        global $CFG;
+        require_once($CFG->libdir.'/adminlib.php');
+
+        $details = get_string('check_unsecuredataroot_details', 'report_security');
+
+        $insecuredataroot = is_dataroot_insecure(true);
+
+        if ($insecuredataroot == INSECURE_DATAROOT_WARNING) {
+            $status = result::ERROR;
+            $summary = get_string('check_unsecuredataroot_warning', 'report_security', $CFG->dataroot);
+
+        } else if ($insecuredataroot == INSECURE_DATAROOT_ERROR) {
+            $status = result::CRITICAL;
+            $summary = get_string('check_unsecuredataroot_error', 'report_security', $CFG->dataroot);
+
+        } else {
+            $status = result::OK;
+            $summary = get_string('check_unsecuredataroot_ok', 'report_security');
+        }
+        return new result($status, $summary, $details);
+    }
+}
+
+
diff --git a/lib/classes/check/environment/upgradecheck.php b/lib/classes/check/environment/upgradecheck.php
new file mode 100644 (file)
index 0000000..706f987
--- /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/>.
+
+/**
+ * Upgrade check
+ *
+ * @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\environment;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Upgrade check
+ *
+ * @package    core
+ * @copyright  2020 Brendan Heywood (brendan@catalyst-au.net)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class upgradecheck extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('checkupgradepending', 'admin');
+    }
+
+    /**
+     * A link to a place to action this
+     *
+     * @return action_link|null
+     */
+    public function get_action_link(): ?\action_link {
+        return new \action_link(
+            new \moodle_url('/admin/index.php?cache=1'),
+            get_string('notifications', 'admin'));
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $CFG;
+
+        require("$CFG->dirroot/version.php");
+        $newversion = "$release ($version)";
+
+        if ($version < $CFG->version) {
+            $status = result::ERROR;
+            $summary = get_string('downgradedcore', 'error');
+        } else if (moodle_needs_upgrading()) {
+            $status = result::ERROR;
+            $summary = get_string('cliupgradepending', 'admin');
+        } else {
+            $status = result::OK;
+            $summary = get_string('cliupgradenoneed', 'core_admin', $newversion);
+        }
+        return new result($status, $summary);
+    }
+}
+
diff --git a/lib/classes/check/environment/vendordir.php b/lib/classes/check/environment/vendordir.php
new file mode 100644 (file)
index 0000000..55da2bc
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Check the presence of the vendor directory.
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\environment;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Check the presence of the vendor directory.
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class vendordir extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name(): string {
+        return get_string('check_vendordir_name', 'report_security');
+    }
+
+    /**
+     * Return result
+     * @return result
+     */
+    public function get_result(): result {
+        global $CFG;
+        $details = get_string('check_vendordir_details', 'report_security', ['path' => $CFG->dirroot.'/vendor']);
+        $summary = get_string('check_vendordir_info', 'report_security');
+
+        if (is_dir($CFG->dirroot.'/vendor')) {
+            $status = result::WARNING;
+        } else {
+            $status = result::OK;
+        }
+        return new result($status, $summary, $details);
+    }
+}
+
diff --git a/lib/classes/check/http/cookiesecure.php b/lib/classes/check/http/cookiesecure.php
new file mode 100644 (file)
index 0000000..3f403be
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Verifies if https enabled only secure cookies allowed
+ *
+ * This prevents redirections and sending of cookies to unsecure port.
+ *
+ * @package    core
+ * @category   check
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\check\http;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\check\check;
+use core\check\result;
+
+/**
+ * Verifies if https enabled only secure cookies allowed
+ *
+ * This prevents redirections and sending of cookies to unsecure port.
+ *
+ * @copyright  2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @copyright  2008 petr Skoda
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cookiesecure extends check {
+
+    /**
+     * Get the short check name
+     *
+     * @return string
+     */
+    public function get_name