Merge branch 'MDL-68189' of https://github.com/NeillM/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 15 Apr 2020 22:13:32 +0000 (00:13 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 15 Apr 2020 22:13:32 +0000 (00:13 +0200)
342 files changed:
.eslintignore
.stylelintignore
Gruntfile.js
admin/cli/checks.php [new file with mode: 0644]
admin/contentbank.php [new file with mode: 0644]
admin/index.php
admin/renderer.php
admin/settings/badges.php
admin/settings/plugins.php
admin/tasklogs.php
admin/templates/tasklogs.mustache
admin/tool/analytics/classes/output/renderer.php
admin/tool/dataprivacy/amd/build/myrequestactions.min.js
admin/tool/dataprivacy/amd/build/myrequestactions.min.js.map
admin/tool/dataprivacy/amd/src/myrequestactions.js
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/dbops/restore_dbops.class.php
backup/util/dbops/tests/restore_dbops_test.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/activity_modules/block_activity_modules.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
calendar/amd/build/crud.min.js
calendar/amd/build/crud.min.js.map
calendar/amd/src/crud.js
config-dist.php
contentbank/classes/content.php [new file with mode: 0644]
contentbank/classes/contentbank.php [new file with mode: 0644]
contentbank/classes/contenttype.php [new file with mode: 0644]
contentbank/classes/helper.php [new file with mode: 0644]
contentbank/classes/output/bankcontent.php [new file with mode: 0644]
contentbank/classes/privacy/provider.php [new file with mode: 0644]
contentbank/contenttype/h5p/classes/content.php [new file with mode: 0644]
contentbank/contenttype/h5p/classes/contenttype.php [new file with mode: 0644]
contentbank/contenttype/h5p/classes/privacy/provider.php [new file with mode: 0644]
contentbank/contenttype/h5p/db/access.php [new file with mode: 0644]
contentbank/contenttype/h5p/lang/en/contenttype_h5p.php [new file with mode: 0644]
contentbank/contenttype/h5p/tests/behat/admin_upload_content.feature [new file with mode: 0644]
contentbank/contenttype/h5p/tests/behat/teacher_upload_content.feature [new file with mode: 0644]
contentbank/contenttype/h5p/tests/content_h5p_test.php [new file with mode: 0644]
contentbank/contenttype/h5p/tests/contenttype_h5p_test.php [new file with mode: 0644]
contentbank/contenttype/h5p/version.php [new file with mode: 0644]
contentbank/files_form.php [new file with mode: 0644]
contentbank/index.php [new file with mode: 0644]
contentbank/templates/bankcontent.mustache [new file with mode: 0644]
contentbank/templates/toolbar.mustache [new file with mode: 0644]
contentbank/tests/behat/access_permissions.feature [new file with mode: 0644]
contentbank/tests/content_test.php [new file with mode: 0644]
contentbank/tests/contentbank_test.php [new file with mode: 0644]
contentbank/tests/contenttype_test.php [new file with mode: 0644]
contentbank/tests/fixtures/testable_content.php [new file with mode: 0644]
contentbank/tests/fixtures/testable_contenttype.php [new file with mode: 0644]
contentbank/upload.php [new file with mode: 0644]
contentbank/view.php [new file with mode: 0644]
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/service/content_item_service.php
course/classes/management_renderer.php
course/classes/output/recommendations/activity_list.php
course/format/renderer.php
course/format/topics/format.js
course/format/topics/renderer.php
course/format/upgrade.txt
course/format/weeks/format.js
course/recommendations.php
course/resources.php
course/templates/activity_list.mustache
course/templates/activitychooser.mustache
course/templates/local/activitychooser/search.mustache
course/tests/behat/search_recommended_activities.feature [new file with mode: 0644]
course/tests/services_content_item_service_test.php
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/import/lib.php
grade/report/user/renderer.php
h5p/classes/core.php
h5p/classes/player.php
h5p/embed.php
h5p/js/embed.js
h5p/js/h5p_overrides.js
h5p/tests/fixtures/multiple-choice-2-6.h5p [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/cache.php
lang/en/contentbank.php [new file with mode: 0644]
lang/en/course.php
lang/en/error.php
lang/en/moodle.php
lang/en/plugin.php
lang/en/role.php
lib/adminlib.php
lib/amd/build/drawer.min.js
lib/amd/build/drawer.min.js.map
lib/amd/src/drawer.js
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/output/icon_system_fontawesome.php
lib/classes/plugin_manager.php
lib/classes/plugininfo/contenttype.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/access.php
lib/db/caches.php
lib/db/install.xml
lib/db/upgrade.php
lib/filelib.php
lib/mdn-polyfills/readme_moodle.txt [deleted file]
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/table/amd/build/dynamic.min.js
lib/table/amd/build/dynamic.min.js.map
lib/table/amd/build/local/dynamic/repository.min.js
lib/table/amd/build/local/dynamic/repository.min.js.map
lib/table/amd/build/local/dynamic/selectors.min.js
lib/table/amd/build/local/dynamic/selectors.min.js.map
lib/table/amd/src/dynamic.js
lib/table/amd/src/local/dynamic/repository.js
lib/table/amd/src/local/dynamic/selectors.js
lib/table/classes/external/dynamic/fetch.php
lib/tablelib.php
lib/templates/check/result.mustache [new file with mode: 0644]
lib/templates/check/result/critical.mustache [new file with mode: 0644]
lib/templates/check/result/error.mustache [new file with mode: 0644]
lib/templates/check/result/info.mustache [new file with mode: 0644]
lib/templates/check/result/na.mustache [new file with mode: 0644]
lib/templates/check/result/ok.mustache [new file with mode: 0644]
lib/templates/check/result/unknown.mustache [new file with mode: 0644]
lib/templates/check/result/warning.mustache [new file with mode: 0644]
lib/templates/drawer.mustache
lib/templates/initials_bar.mustache
lib/templates/paging_bar.mustache
lib/tests/check_test.php [new file with mode: 0644]
lib/tests/completionlib_test.php
lib/tests/component_test.php
lib/tests/moodlelib_test.php
lib/tests/tablelib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/yui/build/moodle-core-blocks/moodle-core-blocks-debug.js
lib/yui/build/moodle-core-blocks/moodle-core-blocks-min.js
lib/yui/build/moodle-core-blocks/moodle-core-blocks.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop-min.js
lib/yui/build/moodle-core-dragdrop/moodle-core-dragdrop.js
lib/yui/src/blocks/js/manager.js
lib/yui/src/dragdrop/js/dragdrop.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/book/tool/print/classes/output/renderer.php
mod/book/view.php
mod/choice/renderer.php
mod/data/field/multimenu/field.class.php
mod/forum/amd/build/local/grades/grader.min.js
mod/forum/amd/build/local/grades/grader.min.js.map
mod/forum/amd/build/local/grades/local/grader/selectors.min.js
mod/forum/amd/build/local/grades/local/grader/selectors.min.js.map
mod/forum/amd/build/local/grades/local/grader/user_picker.min.js
mod/forum/amd/build/local/grades/local/grader/user_picker.min.js.map
mod/forum/amd/build/local/grades/local/grader/user_picker/selectors.min.js
mod/forum/amd/build/local/grades/local/grader/user_picker/selectors.min.js.map
mod/forum/amd/src/local/grades/grader.js
mod/forum/amd/src/local/grades/local/grader/selectors.js
mod/forum/amd/src/local/grades/local/grader/user_picker.js
mod/forum/amd/src/local/grades/local/grader/user_picker/selectors.js
mod/forum/lang/en/forum.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/forum/templates/local/grades/grader.mustache
mod/forum/templates/local/grades/local/grader/content.mustache
mod/forum/templates/local/grades/local/grader/grading.mustache
mod/forum/templates/local/grades/local/grader/navigation.mustache
mod/forum/templates/local/grades/local/grader/user_picker.mustache
mod/forum/templates/local/grades/local/grader/user_picker/user.mustache
mod/h5pactivity/classes/event/course_module_instance_list_viewed.php
mod/h5pactivity/classes/event/course_module_viewed.php
mod/h5pactivity/classes/event/statement_received.php [new file with mode: 0644]
mod/h5pactivity/classes/local/attempt.php [new file with mode: 0644]
mod/h5pactivity/classes/privacy/provider.php
mod/h5pactivity/classes/xapi/handler.php [new file with mode: 0644]
mod/h5pactivity/db/access.php
mod/h5pactivity/db/install.xml
mod/h5pactivity/db/upgrade.php [new file with mode: 0644]
mod/h5pactivity/lang/en/h5pactivity.php
mod/h5pactivity/lib.php
mod/h5pactivity/tests/behat/add_h5pactivity.feature
mod/h5pactivity/tests/behat/sending_attempt.feature [new file with mode: 0644]
mod/h5pactivity/tests/event/course_module_instance_list_viewed_test.php [new file with mode: 0644]
mod/h5pactivity/tests/event/course_module_viewed_test.php [moved from mod/h5pactivity/tests/events_test.php with 61% similarity]
mod/h5pactivity/tests/event/statement_received_test.php [new file with mode: 0644]
mod/h5pactivity/tests/generator/lib.php
mod/h5pactivity/tests/local/attempt_test.php [new file with mode: 0644]
mod/h5pactivity/tests/privacy_test.php [new file with mode: 0644]
mod/h5pactivity/tests/xapi/handler_test.php [new file with mode: 0644]
mod/h5pactivity/version.php
mod/h5pactivity/view.php
mod/lesson/renderer.php
mod/wiki/renderer.php
mod/workshop/renderer.php
phpunit.xml.dist
pix/i/upload.png [new file with mode: 0644]
pix/i/upload.svg [new file with mode: 0644]
question/type/ddimageortext/rendererbase.php
question/type/ddmarker/renderer.php
question/type/ddwtos/renderer.php
report/eventlist/classes/renderer.php
report/insights/classes/output/renderer.php
report/log/classes/renderable.php
report/log/locallib.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.scss
theme/boost/scss/moodle/contentbank.scss [new file with mode: 0644]
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/modules.scss
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
user/classes/participants_table.php
user/index.php
user/renderer.php
version.php

index c111d11..a13efd1 100644 (file)
@@ -62,7 +62,7 @@ lib/amd/src/popper.js
 lib/geopattern-php/
 lib/php-jwt/
 lib/babel-polyfill/
-lib/mdn-polyfills/
+lib/polyfills/
 lib/emoji-data/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
index a828212..ac09a33 100644 (file)
@@ -63,7 +63,7 @@ lib/amd/src/popper.js
 lib/geopattern-php/
 lib/php-jwt/
 lib/babel-polyfill/
-lib/mdn-polyfills/
+lib/polyfills/
 lib/emoji-data/
 media/player/videojs/amd/src/video-lazy.js
 media/player/videojs/amd/src/Youtube-lazy.js
index bd39185..59ebfbb 100644 (file)
@@ -536,12 +536,8 @@ module.exports = function(grunt) {
         formatter.printResults(results);
 
         // Report on the results.
-        // We exit 1 if there is at least one error, otherwise we exit cleanly.
-        if (results.some(result => result.errors.length > 0)) {
-            done(1);
-        } else {
-            done(0);
-        }
+        // The done function takes a bool whereby a falsey statement causes the task to fail.
+        done(results.every(result => result.errors.length === 0));
     };
 
     tasks.startup = function() {
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);
+
diff --git a/admin/contentbank.php b/admin/contentbank.php
new file mode 100644 (file)
index 0000000..da29e6b
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Content bank and its plugins settings.
+ *
+ * @package    core
+ * @subpackage contentbank
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once('../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+
+$action = required_param('action', PARAM_ALPHANUMEXT);
+$name   = required_param('name', PARAM_PLUGIN);
+
+$syscontext = context_system::instance();
+$PAGE->set_url('/admin/contentbank.php');
+$PAGE->set_context($syscontext);
+
+require_admin();
+require_sesskey();
+
+$return = new moodle_url('/admin/settings.php', array('section' => 'managecontentbanktypes'));
+
+$plugins = core_plugin_manager::instance()->get_plugins_of_type('contenttype');
+$sortorder = array_flip(array_keys($plugins));
+
+if (!isset($plugins[$name])) {
+    print_error('contenttypenotfound', 'error', $return, $name);
+}
+
+switch ($action) {
+    case 'disable':
+        if ($plugins[$name]->is_enabled()) {
+            set_config('disabled', 1, 'contentbank_'. $name);
+        }
+        break;
+    case 'enable':
+        if (!$plugins[$name]->is_enabled()) {
+            unset_config('disabled', 'contentbank_'. $name);
+        }
+        break;
+    case 'up':
+        if ($sortorder[$name]) {
+            $currentindex = $sortorder[$name];
+            $seq = array_keys($plugins);
+            $seq[$currentindex] = $seq[$currentindex - 1];
+            $seq[$currentindex - 1] = $name;
+            set_config('contentbank_plugins_sortorder', implode(',', $seq));
+        }
+        break;
+    case 'down':
+        if ($sortorder[$name] < count($sortorder) - 1) {
+            $currentindex = $sortorder[$name];
+            $seq = array_keys($plugins);
+            $seq[$currentindex] = $seq[$currentindex + 1];
+            $seq[$currentindex + 1] = $name;
+            set_config('contentbank_plugins_sortorder', implode(',', $seq));
+        }
+        break;
+}
+core_plugin_manager::reset_caches();
+$cache = cache::make('core', 'contentbank_enabled_extensions');
+$cache->purge();
+$cache = cache::make('core', 'contentbank_context_extensions');
+$cache->purge();
+
+redirect($return);
index c391089..ee3b2e3 100644 (file)
@@ -843,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 6b3848b..278d4f3 100644 (file)
@@ -657,6 +657,19 @@ if ($hassiteconfig) {
     }
 }
 
+// Content bank content types.
+if ($hassiteconfig) {
+    $ADMIN->add('modules', new admin_category('contenbanksettings', new lang_string('contentbank')));
+    $temp = new admin_settingpage('managecontentbanktypes', new lang_string('managecontentbanktypes'));
+    $temp->add(new admin_setting_managecontentbankcontenttypes());
+    $ADMIN->add('contenbanksettings', $temp);
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('contenttype');
+    foreach ($plugins as $plugin) {
+        /** @var \core\plugininfo\contentbank $plugin */
+        $plugin->load_settings($ADMIN, 'contenbanksettings', $hassiteconfig);
+    }
+}
+
 /// Add all local plugins - must be always last!
 if ($hassiteconfig) {
     $ADMIN->add('modules', new admin_category('localplugins', new lang_string('localplugins')));
index 0fb5206..796f323 100644 (file)
@@ -62,6 +62,8 @@ if (null !== $logid) {
 $renderer = $PAGE->get_renderer('tool_task');
 
 echo $OUTPUT->header();
+
+// Output the search form.
 echo $OUTPUT->render_from_template('core_admin/tasklogs', (object) [
     'action' => $pageurl->out(),
     'filter' => $filter,
@@ -84,6 +86,7 @@ echo $OUTPUT->render_from_template('core_admin/tasklogs', (object) [
     ],
 ]);
 
+// Output any matching logs.
 $table = new \core_admin\task_log_table($filter, $result);
 $table->baseurl = $pageurl;
 $table->out(100, false);
index c3180a2..bb180b6 100644 (file)
@@ -17,7 +17,7 @@
 {{!
     @template core_admin/tasklogs
 
-    Task Logs template.
+    This is the template for the search form which appears above the task logs report.
 }}
 <form class="form-inline" method="GET" action="{{{action}}}">
     <label class="sr-only" for="tasklog-filter">{{#str}}filter{{/str}}</label>
index edd165d..0855ed1 100644 (file)
@@ -27,8 +27,7 @@ namespace tool_analytics\output;
 defined('MOODLE_INTERNAL') || die();
 
 use plugin_renderer_base;
-use templatable;
-use renderable;
+
 
 /**
  * Renderer class.
@@ -74,14 +73,12 @@ class renderer extends plugin_renderer_base {
      * @return string HTML
      */
     public function render_evaluate_results($results, $logs = array()) {
-        global $OUTPUT;
-
         $output = '';
 
         foreach ($results as $timesplittingid => $result) {
 
             if (!CLI_SCRIPT) {
-                $output .= $OUTPUT->box_start('generalbox mb-3');
+                $output .= $this->output->box_start('generalbox mb-3');
             }
 
             // Check that the array key is a string, not all results depend on time splitting methods (e.g. general errors).
@@ -90,47 +87,48 @@ class renderer extends plugin_renderer_base {
                 $langstrdata = (object)array('name' => $timesplitting->get_name(), 'id' => $timesplittingid);
 
                 if (CLI_SCRIPT) {
-                    $output .= $OUTPUT->heading(get_string('scheduledanalysisresultscli', 'tool_analytics', $langstrdata), 3);
+                    $output .= $this->output->heading(get_string('scheduledanalysisresultscli', 'tool_analytics', $langstrdata), 3);
                 } else {
-                    $output .= $OUTPUT->heading(get_string('scheduledanalysisresults', 'tool_analytics', $langstrdata), 3);
+                    $output .= $this->output->heading(get_string('scheduledanalysisresults', 'tool_analytics', $langstrdata), 3);
                 }
             }
 
             if ($result->status == 0) {
-                $output .= $OUTPUT->notification(get_string('goodmodel', 'tool_analytics'),
+                $output .= $this->output->notification(get_string('goodmodel', 'tool_analytics'),
                     \core\output\notification::NOTIFY_SUCCESS);
             } else if ($result->status === \core_analytics\model::NO_DATASET) {
-                $output .= $OUTPUT->notification(get_string('nodatatoevaluate', 'tool_analytics'),
+                $output .= $this->output->notification(get_string('nodatatoevaluate', 'tool_analytics'),
                     \core\output\notification::NOTIFY_WARNING);
             }
 
             if (isset($result->score)) {
                 // Score.
-                $output .= $OUTPUT->heading(get_string('accuracy', 'tool_analytics') . ': ' .
+                $output .= $this->output->heading(get_string('accuracy', 'tool_analytics') . ': ' .
                     round(floatval($result->score), 4) * 100  . '%', 4);
             }
 
             if (!empty($result->info)) {
                 foreach ($result->info as $message) {
-                    $output .= $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING);
+                    $output .= $this->output->notification($message, \core\output\notification::NOTIFY_WARNING);
                 }
             }
 
             if (!CLI_SCRIPT) {
-                $output .= $OUTPUT->box_end();
+                $output .= $this->output->box_end();
             }
         }
 
         // Info logged during evaluation.
         if (!empty($logs) && debugging()) {
-            $output .= $OUTPUT->heading(get_string('extrainfo', 'tool_analytics'), 3);
+            $output .= $this->output->heading(get_string('extrainfo', 'tool_analytics'), 3);
             foreach ($logs as $log) {
-                $output .= $OUTPUT->notification($log, \core\output\notification::NOTIFY_WARNING);
+                $output .= $this->output->notification($log, \core\output\notification::NOTIFY_WARNING);
             }
         }
 
         if (!CLI_SCRIPT) {
-            $output .= $OUTPUT->single_button(new \moodle_url('/admin/tool/analytics/index.php'), get_string('continue'), 'get');
+            $output .= $this->output->single_button(new \moodle_url('/admin/tool/analytics/index.php'),
+                    get_string('continue'), 'get');
         }
 
         return $output;
@@ -147,62 +145,68 @@ class renderer extends plugin_renderer_base {
      * @return string HTML
      */
     public function render_get_predictions_results($trainresults = false, $trainlogs = array(), $predictresults = false, $predictlogs = array()) {
-        global $OUTPUT;
-
         $output = '';
 
         if ($trainresults || (!empty($trainlogs) && debugging())) {
-            $output .= $OUTPUT->heading(get_string('trainingresults', 'tool_analytics'), 3);
+            $output .= $this->output->heading(get_string('trainingresults', 'tool_analytics'), 3);
         }
 
         if ($trainresults) {
             if ($trainresults->status == 0) {
-                $output .= $OUTPUT->notification(get_string('trainingprocessfinished', 'tool_analytics'),
+                $output .= $this->output->notification(
+                        get_string('trainingprocessfinished', 'tool_analytics'),
                     \core\output\notification::NOTIFY_SUCCESS);
             } else if ($trainresults->status === \core_analytics\model::NO_DATASET ||
                     $trainresults->status === \core_analytics\model::NOT_ENOUGH_DATA) {
-                $output .= $OUTPUT->notification(get_string('nodatatotrain', 'tool_analytics'),
+                $output .= $this->output->notification(
+                        get_string('nodatatotrain', 'tool_analytics'),
                     \core\output\notification::NOTIFY_WARNING);
             } else {
-                $output .= $OUTPUT->notification(get_string('generalerror', 'tool_analytics', $trainresults->status),
+                $output .= $this->output->notification(
+                        get_string('generalerror', 'tool_analytics', $trainresults->status),
                     \core\output\notification::NOTIFY_ERROR);
             }
         }
 
         if (!empty($trainlogs) && debugging()) {
-            $output .= $OUTPUT->heading(get_string('extrainfo', 'tool_analytics'), 4);
+            $output .= $this->output->heading(get_string('extrainfo', 'tool_analytics'), 4);
             foreach ($trainlogs as $log) {
-                $output .= $OUTPUT->notification($log, \core\output\notification::NOTIFY_WARNING);
+                $output .= $this->output->notification($log, \core\output\notification::NOTIFY_WARNING);
             }
         }
 
         if ($predictresults || (!empty($predictlogs) && debugging())) {
-            $output .= $OUTPUT->heading(get_string('predictionresults', 'tool_analytics'), 3, 'main mt-3');
+            $output .= $this->output->heading(
+                    get_string('predictionresults', 'tool_analytics'), 3, 'main mt-3');
         }
 
         if ($predictresults) {
             if ($predictresults->status == 0) {
-                $output .= $OUTPUT->notification(get_string('predictionprocessfinished', 'tool_analytics'),
+                $output .= $this->output->notification(
+                        get_string('predictionprocessfinished', 'tool_analytics'),
                     \core\output\notification::NOTIFY_SUCCESS);
             } else if ($predictresults->status === \core_analytics\model::NO_DATASET ||
                     $predictresults->status === \core_analytics\model::NOT_ENOUGH_DATA) {
-                $output .= $OUTPUT->notification(get_string('nodatatopredict', 'tool_analytics'),
+                $output .= $this->output->notification(
+                        get_string('nodatatopredict', 'tool_analytics'),
                     \core\output\notification::NOTIFY_WARNING);
             } else {
-                $output .= $OUTPUT->notification(get_string('generalerror', 'tool_analytics', $predictresults->status),
+                $output .= $this->output->notification(
+                        get_string('generalerror', 'tool_analytics', $predictresults->status),
                     \core\output\notification::NOTIFY_ERROR);
             }
         }
 
         if (!empty($predictlogs) && debugging()) {
-            $output .= $OUTPUT->heading(get_string('extrainfo', 'tool_analytics'), 4);
+            $output .= $this->output->heading(get_string('extrainfo', 'tool_analytics'), 4);
             foreach ($predictlogs as $log) {
-                $output .= $OUTPUT->notification($log, \core\output\notification::NOTIFY_WARNING);
+                $output .= $this->output->notification($log, \core\output\notification::NOTIFY_WARNING);
             }
         }
 
         if (!CLI_SCRIPT) {
-            $output .= $OUTPUT->single_button(new \moodle_url('/admin/tool/analytics/index.php'), get_string('continue'), 'get');
+            $output .= $this->output->single_button(new \moodle_url('/admin/tool/analytics/index.php'),
+                    get_string('continue'), 'get');
         }
 
         return $output;
@@ -236,17 +240,18 @@ class renderer extends plugin_renderer_base {
      * @return string HTML
      */
     public function render_analytics_disabled() {
-        global $OUTPUT, $PAGE, $FULLME;
+        global $FULLME;
 
-        $PAGE->set_url($FULLME);
-        $PAGE->set_title(get_string('pluginname', 'tool_analytics'));
-        $PAGE->set_heading(get_string('pluginname', 'tool_analytics'));
+        $this->page->set_url($FULLME);
+        $this->page->set_title(get_string('pluginname', 'tool_analytics'));
+        $this->page->set_heading(get_string('pluginname', 'tool_analytics'));
 
-        $output = $OUTPUT->header();
-        $output .= $OUTPUT->notification(get_string('analyticsdisabled', 'analytics'), \core\output\notification::NOTIFY_INFO);
+        $output = $this->output->header();
+        $output .= $this->output->notification(get_string('analyticsdisabled', 'analytics'),
+                \core\output\notification::NOTIFY_INFO);
         $output .= \html_writer::tag('a', get_string('continue'), ['class' => 'btn btn-primary',
             'href' => (new \moodle_url('/'))->out()]);
-        $output .= $OUTPUT->footer();
+        $output .= $this->output->footer();
 
         return $output;
     }
index f510cd2..a6ba759 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/myrequestactions.min.js and b/admin/tool/dataprivacy/amd/build/myrequestactions.min.js differ
index 6d17dee..c9f78ed 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/myrequestactions.min.js.map and b/admin/tool/dataprivacy/amd/build/myrequestactions.min.js.map differ
index 5ec631e..54f94fb 100644 (file)
@@ -28,8 +28,9 @@ define([
     'core/str',
     'core/modal_factory',
     'core/modal_events',
-    'core/templates'],
-function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) {
+    'core/templates',
+    'core/pending'],
+function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Pending) {
 
     /**
      * List of action selectors.
@@ -118,6 +119,7 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) {
         });
 
         $(ACTIONS.CONTACT_DPO).click(function(e) {
+            var pendingPromise = new Pending('dataprivacy/crud:initModal:contactdpo');
             e.preventDefault();
 
             var replyToEmail = $(this).data('replytoemail');
@@ -146,9 +148,12 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) {
                     type: ModalFactory.types.SAVE_CANCEL,
                     large: true
                 });
-            }).done(function(modal) {
+            }).then(function(modal) {
                 modal.setSaveButtonText(sendButtonText);
 
+                // Show the modal!
+                modal.show();
+
                 // Handle send event.
                 modal.getRoot().on(ModalEvents.save, function(e) {
                     var message = $('#message').val().trim();
@@ -169,9 +174,9 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) {
                     modal.destroy();
                 });
 
-                // Show the modal!
-                modal.show();
-            }).fail(Notification.exception);
+                return;
+            }).then(pendingPromise.resolve)
+            .catch(Notification.exception);
         });
     };
 
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..ceee867 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,7 @@ 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 +29,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 0911c68..eef73fc 100644 (file)
@@ -1394,7 +1394,9 @@ abstract class restore_dbops {
             // Note: for DB deleted users md5(username) is stored *sometimes* in the email field,
             //       hence we are looking there for usernames if not empty. See delete_user()
             // If match by id and mnethost and user is deleted in DB and
-            // match by username LIKE 'backup_email.%' or by non empty email = md5(username) => ok, return target user
+            // match by username LIKE 'substring(backup_email).%' where the substr length matches the retained data in the
+            // username field (100 - (timestamp + 1) characters), or by non empty email = md5(username) => ok, return target user.
+            $usernamelookup = core_text::substr($user->email, 0, 89) . '.%';
             if ($rec = $DB->get_record_sql("SELECT *
                                               FROM {user} u
                                              WHERE id = ?
@@ -1407,13 +1409,14 @@ abstract class restore_dbops {
                                                        AND email = ?
                                                        )
                                                    )",
-                                           array($user->id, $user->mnethostid, $user->email.'.%', md5($user->username)))) {
+                                           array($user->id, $user->mnethostid, $usernamelookup, md5($user->username)))) {
                 return $rec; // Matching user, deleted in DB found, return it
             }
 
             // 1D - Handle users deleted in backup file and "alive" in DB
             // If match by id and mnethost and user is deleted in backup file
-            // and match by email = email_without_time(backup_email) => ok, return target user
+            // and match by substring(email) = email_without_time(backup_email) where the substr length matches the retained data
+            // in the username field (100 - (timestamp + 1) characters) => ok, return target user.
             if ($user->deleted) {
                 // Note: for DB deleted users email is stored in username field, hence we
                 //       are looking there for emails. See delete_user()
@@ -1423,7 +1426,7 @@ abstract class restore_dbops {
                                                   FROM {user} u
                                                  WHERE id = ?
                                                    AND mnethostid = ?
-                                                   AND UPPER(email) = UPPER(?)",
+                                                   AND " . $DB->sql_substr('UPPER(email)', 1, 89) . " = UPPER(?)",
                                                array($user->id, $user->mnethostid, $trimemail))) {
                     return $rec; // Matching user, deleted in backup file found, return it
                 }
@@ -1470,7 +1473,8 @@ abstract class restore_dbops {
             // Note: for DB deleted users md5(username) is stored *sometimes* in the email field,
             //       hence we are looking there for usernames if not empty. See delete_user()
             // 2B1 - If match by mnethost and user is deleted in DB and not empty email = md5(username) and
-            //       (by username LIKE 'backup_email.%' or non-zero firstaccess) => ok, return target user
+            //       (by username LIKE 'substring(backup_email).%' or non-zero firstaccess) => ok, return target user.
+            $usernamelookup = core_text::substr($user->email, 0, 89) . '.%';
             if ($rec = $DB->get_record_sql("SELECT *
                                               FROM {user} u
                                              WHERE mnethostid = ?
@@ -1484,14 +1488,15 @@ abstract class restore_dbops {
                                                        AND firstaccess = ?
                                                        )
                                                    )",
-                                           array($user->mnethostid, md5($user->username), $user->email.'.%', $user->firstaccess))) {
+                                           array($user->mnethostid, md5($user->username), $usernamelookup, $user->firstaccess))) {
                 return $rec; // Matching user found, return it
             }
 
             // 2B2 - If match by mnethost and user is deleted in DB and
-            //       username LIKE 'backup_email.%' and non-zero firstaccess) => ok, return target user
+            //       username LIKE 'substring(backup_email).%' and non-zero firstaccess) => ok, return target user
             //       (this covers situations where md5(username) wasn't being stored so we require both
             //        the email & non-zero firstaccess to match)
+            $usernamelookup = core_text::substr($user->email, 0, 89) . '.%';
             if ($rec = $DB->get_record_sql("SELECT *
                                               FROM {user} u
                                              WHERE mnethostid = ?
@@ -1499,13 +1504,13 @@ abstract class restore_dbops {
                                                AND UPPER(username) LIKE UPPER(?)
                                                AND firstaccess != 0
                                                AND firstaccess = ?",
-                                           array($user->mnethostid, $user->email.'.%', $user->firstaccess))) {
+                                           array($user->mnethostid, $usernamelookup, $user->firstaccess))) {
                 return $rec; // Matching user found, return it
             }
 
             // 2C - Handle users deleted in backup file and "alive" in DB
             // If match mnethost and user is deleted in backup file
-            // and match by email = email_without_time(backup_email) and non-zero firstaccess=> ok, return target user
+            // and match by substring(email) = email_without_time(backup_email) and non-zero firstaccess=> ok, return target user.
             if ($user->deleted) {
                 // Note: for DB deleted users email is stored in username field, hence we
                 //       are looking there for emails. See delete_user()
@@ -1514,7 +1519,7 @@ abstract class restore_dbops {
                 if ($rec = $DB->get_record_sql("SELECT *
                                                   FROM {user} u
                                                  WHERE mnethostid = ?
-                                                   AND UPPER(email) = UPPER(?)
+                                                   AND " . $DB->sql_substr('UPPER(email)', 1, 89) . " = UPPER(?)
                                                    AND firstaccess != 0
                                                    AND firstaccess = ?",
                                                array($user->mnethostid, $trimemail, $user->firstaccess))) {
index d0fd2cc..59c127b 100644 (file)
@@ -119,4 +119,246 @@ class restore_dbops_testcase extends advanced_testcase {
             $this->assertSame('Table "backup_ids_temp" does not exist', $e->getMessage());
         }
     }
+
+    /**
+     * Data provider for {@link test_precheck_user()}
+     */
+    public function precheck_user_provider() {
+
+        $emailmultiplier = [
+            'shortmail' => 'normalusername@example.com',
+            'longmail' => str_repeat('a', 100)  // It's not validated, hence any string is ok.
+        ];
+
+        $providercases = [];
+
+        foreach ($emailmultiplier as $emailk => $email) {
+            // Get the related cases.
+            $cases = $this->precheck_user_cases($email);
+            // Rename them (keys).
+            foreach ($cases as $key => $case) {
+                $providercases[$key . ' - ' . $emailk] = $case;
+            }
+        }
+
+        return $providercases;
+    }
+
+    /**
+     * Get all the cases implemented in {@link restore_dbops::precheck_users()}
+     *
+     * @param string $email
+     */
+    private function precheck_user_cases($email) {
+        global $CFG;
+
+        $baseuserarr = [
+            'username' => 'normalusername',
+            'email'    => $email,
+            'mnethostid' => $CFG->mnet_localhost_id,
+            'firstaccess' => 123456789,
+            'deleted'    => 0,
+            'forceemailcleanup' => false, // Hack to force the DB record to have empty mail.
+            'forceduplicateadminallowed' => false]; // Hack to enable import_general_duplicate_admin_allowed.
+
+        return [
+            // Cases with samesite = true.
+            'samesite match existing (1A)' => [
+                'dbuser' => $baseuserarr,
+                'backupuser' => $baseuserarr,
+                'samesite' => true,
+                'outcome' => 'match'
+            ],
+            'samesite match existing anon (1B)' => [
+                'dbuser' => array_merge($baseuserarr, [
+                    'username' => 'anon01']),
+                'backupuser' => array_merge($baseuserarr, [
+                    'id' => -1, 'username' => 'anon01', 'firstname' => 'anonfirstname01',
+                    'lastname' => 'anonlastname01', 'email' => 'anon01@doesntexist.invalid']),
+                'samesite' => true,
+                'outcome' => 'match'
+            ],
+            'samesite match existing deleted in db, alive in backup, by db username (1C)' => [
+                'dbuser' => array_merge($baseuserarr, [
+                    'deleted' => 1]),
+                'backupuser' => array_merge($baseuserarr, [
+                    'username' => 'this_wont_match']),
+                'samesite' => true,
+                'outcome' => 'match'
+            ],
+            'samesite match existing deleted in db, alive in backup, by db email (1C)' => [
+                'dbuser' => array_merge($baseuserarr, [
+                    'deleted' => 1]),
+                'backupuser' => array_merge($baseuserarr, [
+                    'email' => 'this_wont_match']),
+                'samesite' => true,
+                'outcome' => 'match'
+            ],
+            'samesite match existing alive in db, deleted in backup (1D)' => [
+                'dbuser' => $baseuserarr,
+                'backupuser' => array_merge($baseuserarr, [
+                    'deleted' => 1]),
+                'samesite' => true,
+                'outcome' => 'match'
+            ],
+            'samesite conflict (1E)' => [
+                'dbuser' => $baseuserarr,
+                'backupuser' => array_merge($baseuserarr, ['id' => -1]),
+                'samesite' => true,
+                'outcome' => false
+            ],
+            'samesite create user (1F)' => [
+                'dbuser' => $baseuserarr,
+                'backupuser' => array_merge($baseuserarr, [
+                    'username' => 'newusername']),
+                'samesite' => false,
+                'outcome' => true
+            ],
+
+            // Cases with samesite = false.
+            'no samesite match existing, by db email (2A1)' => [
+                'dbuser' => $baseuserarr,
+                'backupuser' => array_merge($baseuserarr, [
+                    'firstaccess' => 0]),
+                'samesite' => false,
+                'outcome' => 'match'
+            ],
+            'no samesite match existing, by db firstaccess (2A1)' => [
+                'dbuser' => $baseuserarr,
+                'backupuser' => array_merge($baseuserarr, [
+                    'email' => 'this_wont_match@example.con']),
+                'samesite' => false,
+                'outcome' => 'match'
+            ],
+            'no samesite match existing anon (2A1 too)' => [
+                'dbuser' => array_merge($baseuserarr, [
+                    'username' => 'anon01']),
+                'backupuser' => array_merge($baseuserarr, [
+                    'id' => -1, 'username' => 'anon01', 'firstname' => 'anonfirstname01',
+                    'lastname' => 'anonlastname01', 'email' => 'anon01@doesntexist.invalid']),
+                'samesite' => false,
+                'outcome' => 'match'
+            ],
+            'no samesite match dupe admin (2A2)' => [
+                'dbuser' => array_merge($baseuserarr, [
+                    'username' => 'admin_old_site_id',
+                    'forceduplicateadminallowed' => true]),
+                'backupuser' => array_merge($baseuserarr, [
+                    'username' => 'admin']),
+                'samesite' => false,
+                'outcome' => 'match'
+            ],
+            'no samesite match existing deleted in db, alive in backup, by db username (2B1)' => [
+                'dbuser' => array_merge($baseuserarr, [
+                    'deleted' => 1]),
+                'backupuser' => array_merge($baseuserarr, [
+                    'firstaccess' => 0]),
+                'samesite' => false,
+                'outcome' => 'match'
+            ],
+            'no samesite match existing deleted in db, alive in backup, by db firstaccess (2B1)' => [
+                'dbuser' => array_merge($baseuserarr, [
+                    'deleted' => 1]),
+                'backupuser' => array_merge($baseuserarr, [
+                    'mail' => 'this_wont_match']),
+                'samesite' => false,
+                'outcome' => 'match'
+            ],
+            'no samesite match existing deleted in db, alive in backup (2B2)' => [
+                'dbuser' => array_merge($baseuserarr, [
+                    'deleted' => 1,
+                    'forceemailcleanup' => true]),
+                'backupuser' => $baseuserarr,
+                'samesite' => false,
+                'outcome' => 'match'
+            ],
+            'no samesite match existing alive in db, deleted in backup (2C)' => [
+                'dbuser' => $baseuserarr,
+                'backupuser' => array_merge($baseuserarr, [
+                    'deleted' => 1]),
+                'samesite' => false,
+                'outcome' => 'match'
+            ],
+            'no samesite conflict (2D)' => [
+                'dbuser' => $baseuserarr,
+                'backupuser' => array_merge($baseuserarr, [
+                    'email' => 'anotheruser@example.com', 'firstaccess' => 0]),
+                'samesite' => false,
+                'outcome' => false
+            ],
+            'no samesite create user (2E)' => [
+                'dbuser' => $baseuserarr,
+                'backupuser' => array_merge($baseuserarr, [
+                    'username' => 'newusername']),
+                'samesite' => false,
+                'outcome' => true
+            ],
+
+        ];
+    }
+
+    /**
+     * Test restore precheck_user method
+     *
+     * @dataProvider precheck_user_provider
+     * @covers restore_dbops::precheck_user()
+     *
+     * @param array $dbuser
+     * @param array $backupuser
+     * @param bool $samesite
+     * @param mixed $outcome
+     **/
+    public function test_precheck_user($dbuser, $backupuser, $samesite, $outcome) {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $dbuser = (object)$dbuser;
+        $backupuser = (object)$backupuser;
+
+        $siteid = null;
+
+        // If the backup user must be deleted, simulate it (by temp inserting to DB, deleting and fetching it back).
+        if ($backupuser->deleted) {
+            $backupuser->id = $DB->insert_record('user', array_merge((array)$backupuser, ['deleted' => 0]));
+            delete_user($backupuser);
+            $backupuser = $DB->get_record('user', ['id' => $backupuser->id]);
+            $DB->delete_records('user', ['id' => $backupuser->id]);
+            unset($backupuser->id);
+        }
+
+        // Create the db user, normally.
+        $dbuser->id = $DB->insert_record('user', array_merge((array)$dbuser, ['deleted' => 0]));
+        $backupuser->id = $backupuser->id ?? $dbuser->id;
+
+        // We may want to enable the import_general_duplicate_admin_allowed setting and look for old admin records.
+        if ($dbuser->forceduplicateadminallowed) {
+            set_config('import_general_duplicate_admin_allowed', true, 'backup');
+            $siteid = 'old_site_id';
+        }
+
+        // If the DB user must be deleted, do it and fetch it back.
+        if ($dbuser->deleted) {
+            delete_user($dbuser);
+            // We may want to clean the mail field (old behavior, not containing the current md5(username).
+            if ($dbuser->forceemailcleanup) {
+                $DB->set_field('user', 'email', '', ['id' => $dbuser->id]);
+            }
+        }
+
+        // Get the dbuser  record, because we may have changed it above.
+        $dbuser = $DB->get_record('user', ['id' => $dbuser->id]);
+
+        $method = (new ReflectionClass('restore_dbops'))->getMethod('precheck_user');
+        $method->setAccessible(true);
+        $result = $method->invoke(null, $backupuser, $samesite, $siteid);
+
+        if (is_bool($result)) {
+            $this->assertSame($outcome, $result);
+        } else {
+            $outcome = $dbuser; // Outcome is not bool, matching found, so it must be the dbuser,
+            // Just check ids, it means the expected match has been found in database.
+            $this->assertSame($outcome->id, $result->id);
+        }
+    }
 }
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 af67de2..6d3a60c 100644 (file)
@@ -52,8 +52,8 @@ class block_activity_modules extends block_list {
         $archetypes = array();
 
         foreach($modinfo->cms as $cm) {
-            // Exclude activities which are not visible or have no link (=label)
-            if (!$cm->uservisible or !$cm->has_view()) {
+            // Exclude activities that aren't visible or have no view link (e.g. label). Account for folder being displayed inline.
+            if (!$cm->uservisible || (!$cm->has_view() && strcmp($cm->modname, 'folder') !== 0)) {
                 continue;
             }
             if (array_key_exists($cm->modname, $modfullnames)) {
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 54e4ac1..d44cd94 100644 (file)
Binary files a/calendar/amd/build/crud.min.js and b/calendar/amd/build/crud.min.js differ
index 283f9da..c6e0e09 100644 (file)
Binary files a/calendar/amd/build/crud.min.js.map and b/calendar/amd/build/crud.min.js.map differ
index b757df8..7853a67 100644 (file)
@@ -35,6 +35,7 @@ define([
     'core_calendar/events',
     'core_calendar/modal_delete',
     'core_calendar/selectors',
+    'core/pending',
 ],
 function(
     $,
@@ -49,7 +50,8 @@ function(
     CalendarRepository,
     CalendarEvents,
     ModalDelete,
-    CalendarSelectors
+    CalendarSelectors,
+    Pending
 ) {
 
     /**
@@ -101,13 +103,6 @@ function(
             );
         }
 
-        deletePromise.then(function(deleteModal) {
-            deleteModal.show();
-
-            return;
-        })
-        .fail(Notification.exception);
-
         var stringsPromise = Str.get_strings(deleteStrings);
 
         var finalPromise = $.when(stringsPromise, deletePromise)
@@ -118,27 +113,33 @@ function(
                 deleteModal.setSaveButtonText(strings[0]);
             }
 
+            deleteModal.show();
+
             deleteModal.getRoot().on(ModalEvents.save, function() {
+                var pendingPromise = new Pending('calendar/crud:initModal:deletedevent');
                 CalendarRepository.deleteEvent(eventId, false)
                     .then(function() {
                         $('body').trigger(CalendarEvents.deleted, [eventId, false]);
                         return;
                     })
+                    .then(pendingPromise.resolve)
                     .catch(Notification.exception);
             });
 
             deleteModal.getRoot().on(CalendarEvents.deleteAll, function() {
+                var pendingPromise = new Pending('calendar/crud:initModal:deletedallevent');
                 CalendarRepository.deleteEvent(eventId, true)
                     .then(function() {
                         $('body').trigger(CalendarEvents.deleted, [eventId, true]);
                         return;
                     })
+                    .then(pendingPromise.resolve)
                     .catch(Notification.exception);
             });
 
             return deleteModal;
         })
-        .fail(Notification.exception);
+        .catch(Notification.exception);
 
         return finalPromise;
     }
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.
diff --git a/contentbank/classes/content.php b/contentbank/classes/content.php
new file mode 100644 (file)
index 0000000..b535b2f
--- /dev/null
@@ -0,0 +1,219 @@
+<?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/>.
+
+/**
+ * Content manager class
+ *
+ * @package    core_contentbank
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_contentbank;
+
+use stored_file;
+use stdClass;
+use coding_exception;
+use moodle_url;
+
+/**
+ * Content manager class
+ *
+ * @package    core_contentbank
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class content {
+
+    /** @var stdClass $content The content of the current instance. **/
+    protected $content  = null;
+
+    /**
+     * Content bank constructor
+     *
+     * @param stdClass $content A contentbanck_content record.
+     * @throws coding_exception If content type is not right.
+     */
+    public function __construct(stdClass $content) {
+        // Content type should exist and be linked to plugin classname.
+        $classname = $content->contenttype.'\\content';
+        if (get_class($this) != $classname) {
+            throw new coding_exception(get_string('contenttypenotfound', 'error', $content->contenttype));
+        }
+        $typeclass = $content->contenttype.'\\contenttype';
+        if (!class_exists($typeclass)) {
+            throw new coding_exception(get_string('contenttypenotfound', 'error', $content->contenttype));
+        }
+        // A record with the id must exist in 'contenbank_content' table.
+        // To improve performance, we are only checking the id is set, but no querying the database.
+        if (!isset($content->id)) {
+            throw new coding_exception(get_string('invalidcontentid', 'error'));
+        }
+        $this->content = $content;
+    }
+
+    /**
+     * Returns $this->content.
+     *
+     * @return stdClass  $this->content.
+     */
+    public function get_content(): stdClass {
+        return $this->content;
+    }
+
+    /**
+     * Returns $this->content->contenttype.
+     *
+     * @return string  $this->content->contenttype.
+     */
+    public function get_content_type(): string {
+        return $this->content->contenttype;
+    }
+
+    /**
+     * Updates content_bank table with information in $this->content.
+     *
+     * @return boolean  True if the content has been succesfully updated. False otherwise.
+     * @throws \coding_exception if not loaded.
+     */
+    public function update_content(): bool {
+        global $USER, $DB;
+
+        // A record with the id must exist in 'contenbank_content' table.
+        // To improve performance, we are only checking the id is set, but no querying the database.
+        if (!isset($this->content->id)) {
+            throw new coding_exception(get_string('invalidcontentid', 'error'));
+        }
+        $this->content->usermodified = $USER->id;
+        $this->content->timemodified = time();
+        return $DB->update_record('contentbank_content', $this->content);
+    }
+
+    /**
+     * Returns the name of the content.
+     *
+     * @return string   The name of the content.
+     */
+    public function get_name(): string {
+        return $this->content->name;
+    }
+
+    /**
+     * Returns the content ID.
+     *
+     * @return int   The content ID.
+     */
+    public function get_id(): int {
+        return $this->content->id;
+    }
+
+    /**
+     * Change the content instanceid value.
+     *
+     * @param int $instanceid    New instanceid for this content
+     * @return boolean           True if the instanceid has been succesfully updated. False otherwise.
+     */
+    public function set_instanceid(int $instanceid): bool {
+        $this->content->instanceid = $instanceid;
+        return $this->update_content();
+    }
+
+    /**
+     * Returns the $instanceid of this content.
+     *
+     * @return int   contentbank instanceid
+     */
+    public function get_instanceid(): int {
+        return $this->content->instanceid;
+    }
+
+    /**
+     * Change the content config values.
+     *
+     * @param string $configdata    New config information for this content
+     * @return boolean              True if the configdata has been succesfully updated. False otherwise.
+     */
+    public function set_configdata(string $configdata): bool {
+        $this->content->configdata = $configdata;
+        return $this->update_content();
+    }
+
+    /**
+     * Return the content config values.
+     *
+     * @return mixed   Config information for this content (json decoded)
+     */
+    public function get_configdata() {
+        return $this->content->configdata;
+    }
+
+    /**
+     * Returns the $file related to this content.
+     *
+     * @return stored_file  File stored in content bank area related to the given itemid.
+     * @throws \coding_exception if not loaded.
+     */
+    public function get_file(): ?stored_file {
+        $itemid = $this->get_id();
+        $fs = get_file_storage();
+        $files = $fs->get_area_files(
+            $this->content->contextid,
+            'contentbank',
+            'public',
+            $itemid,
+            'itemid, filepath, filename',
+            false
+        );
+        if (!empty($files)) {
+            $file = reset($files);
+            return $file;
+        }
+        return null;
+    }
+
+    /**
+     * Returns the file url related to this content.
+     *
+     * @return string       URL of the file stored in content bank area related to the given itemid.
+     * @throws \coding_exception if not loaded.
+     */
+    public function get_file_url(): string {
+        if (!$file = $this->get_file()) {
+            return '';
+        }
+        $fileurl = moodle_url::make_pluginfile_url(
+            $this->content->contextid,
+            'contentbank',
+            'public',
+            $file->get_itemid(),
+            $file->get_filepath(),
+            $file->get_filename()
+        );
+
+        return $fileurl;
+    }
+
+    /**
+     * Returns user has access permission for the content itself (based on what plugin needs).
+     *
+     * @return bool     True if content could be accessed. False otherwise.
+     */
+    public function can_view(): bool {
+        // There's no capability at content level to check,
+        // but plugins can overwrite this method in case they want to check something related to content properties.
+        return true;
+    }
+}
diff --git a/contentbank/classes/contentbank.php b/contentbank/classes/contentbank.php
new file mode 100644 (file)
index 0000000..2e89d42
--- /dev/null
@@ -0,0 +1,158 @@
+<?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/>.
+
+/**
+ * Content bank manager class
+ *
+ * @package    core_contentbank
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_contentbank;
+
+/**
+ * Content bank manager class
+ *
+ * @package    core_contentbank
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class contentbank {
+
+    /**
+     * Obtains the list of core_contentbank_content objects currently active.
+     *
+     * The list does not include players which are disabled.
+     *
+     * @return string[] Array of contentbank contenttypes.
+     */
+    private function get_enabled_content_types(): array {
+        $enabledtypes = \core\plugininfo\contenttype::get_enabled_plugins();
+        $types = [];
+        foreach ($enabledtypes as $name) {
+            $classname = "\\contenttype_$name\\contenttype";
+            if (class_exists($classname)) {
+                $types[] = $name;
+            }
+        }
+        return $types;
+    }
+
+    /**
+     * Obtains an array of supported extensions by active plugins.
+     *
+     * @return array The array with all the extensions supported and the supporting plugin names.
+     */
+    public function load_all_supported_extensions(): array {
+        $extensionscache = \cache::make('core', 'contentbank_enabled_extensions');
+        $supportedextensions = $extensionscache->get('enabled_extensions');
+        if ($supportedextensions === false) {
+            // Load all enabled extensions.
+            $supportedextensions = [];
+            foreach ($this->get_enabled_content_types() as $type) {
+                $classname = "\\contenttype_$type\\contenttype";
+                if (class_exists($classname)) {
+                    $manager = new $classname;
+                    if ($manager->is_feature_supported($manager::CAN_UPLOAD)) {
+                        $extensions = $manager->get_manageable_extensions();
+                        foreach ($extensions as $extension) {
+                            if (array_key_exists($extension, $supportedextensions)) {
+                                $supportedextensions[$extension][] = $type;
+                            } else {
+                                $supportedextensions[$extension] = [$type];
+                            }
+                        }
+                    }
+                }
+            }
+            $extensionscache->set('enabled_extensions', $supportedextensions);
+        }
+        return $supportedextensions;
+    }
+
+    /**
+     * Obtains an array of supported extensions in the given context.
+     *
+     * @param \context $context Optional context to check (default null)
+     * @return array The array with all the extensions supported and the supporting plugin names.
+     */
+    public function load_context_supported_extensions(\context $context = null): array {
+        $extensionscache = \cache::make('core', 'contentbank_context_extensions');
+
+        $contextextensions = $extensionscache->get($context->id);
+        if ($contextextensions === false) {
+            $contextextensions = [];
+            $supportedextensions = $this->load_all_supported_extensions();
+            foreach ($supportedextensions as $extension => $types) {
+                foreach ($types as $type) {
+                    $classname = "\\contenttype_$type\\contenttype";
+                    if (class_exists($classname)) {
+                        $manager = new $classname($context);
+                        if ($manager->can_upload()) {
+                            $contextextensions[$extension] = $type;
+                            break;
+                        }
+                    }
+                }
+            }
+            $extensionscache->set($context->id, $contextextensions);
+        }
+        return $contextextensions;
+    }
+
+    /**
+     * Obtains a string with all supported extensions by active plugins.
+     * Mainly to use as filepicker options parameter.
+     *
+     * @param \context $context   Optional context to check (default null)
+     * @return string A string with all the extensions supported.
+     */
+    public function get_supported_extensions_as_string(\context $context = null) {
+        $supported = $this->load_context_supported_extensions($context);
+        $extensions = array_keys($supported);
+        return implode(',', $extensions);
+    }
+
+    /**
+     * Returns the file extension for a file.
+     *
+     * @param  string $filename The name of the file
+     * @return string The extension of the file
+     */
+    public function get_extension(string $filename) {
+        $dot = strrpos($filename, '.');
+        if ($dot === false) {
+            return '';
+        }
+        return strtolower(substr($filename, $dot));
+    }
+
+    /**
+     * Get the first content bank plugin supports a file extension.
+     *
+     * @param string $extension Content file extension
+     * @param \context $context $context     Optional context to check (default null)
+     * @return string contenttype name supports the file extension or null if the extension is not supported by any allowed plugin.
+     */
+    public function get_extension_supporter(string $extension, \context $context = null): ?string {
+        $supporters = $this->load_context_supported_extensions($context);
+        if (array_key_exists($extension, $supporters)) {
+            return $supporters[$extension];
+        }
+        return null;
+    }
+}
diff --git a/contentbank/classes/contenttype.php b/contentbank/classes/contenttype.php
new file mode 100644 (file)
index 0000000..f406282
--- /dev/null
@@ -0,0 +1,213 @@
+<?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/>.
+
+/**
+ * Content type manager class
+ *
+ * @package    core_contentbank
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_contentbank;
+
+use coding_exception;
+use moodle_url;
+
+/**
+ * Content type manager class
+ *
+ * @package    core_contentbank
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class contenttype {
+
+    /** Plugin implements uploading feature */
+    const CAN_UPLOAD = 'upload';
+
+    /** @var context This content's context. **/
+    protected $context = null;
+
+    /**
+     * Content type constructor
+     *
+     * @param \context $context Optional context to check (default null)
+     */
+    public function __construct(\context $context = null) {
+        if (empty($context)) {
+            $context = \context_system::instance();
+        }
+        $this->context = $context;
+    }
+
+    /**
+     * Fills content_bank table with appropiate information.
+     *
+     * @param stdClass $content  An optional content record compatible object (default null)
+     * @return content       Object with content bank information.
+     */
+    public function create_content(\stdClass $content = null): ?content {
+        global $USER, $DB;
+
+        $record = new \stdClass();
+        $record->contenttype = $this->get_contenttype_name();
+        $record->contextid = $this->context->id;
+        $record->name = $content->name ?? '';
+        $record->usercreated = $content->usercreated ?? $USER->id;
+        $record->timecreated = time();
+        $record->usermodified = $record->usercreated;
+        $record->timemodified = $record->timecreated;
+        $record->configdata = $content->configdata ?? '';
+        $record->id = $DB->insert_record('contentbank_content', $record);
+        if ($record->id) {
+            $classname = '\\'.$record->contenttype.'\\content';
+            return new $classname($record);
+        }
+        return null;
+    }
+
+    /**
+     * Returns the contenttype name of this content.
+     *
+     * @return string   Content type of the current instance
+     */
+    public function get_contenttype_name(): string {
+        $classname = get_class($this);
+        $contenttype = explode('\\', $classname);
+        return array_shift($contenttype);
+    }
+
+    /**
+     * Returns the plugin name of the current instance.
+     *
+     * @return string   Plugin name of the current instance
+     */
+    public function get_plugin_name(): string {
+        $contenttype = $this->get_contenttype_name();
+        $plugin = explode('_', $contenttype);
+        return array_pop($plugin);
+    }
+
+    /**
+     * Returns the URL where the content will be visualized.
+     *
+     * @param stdClass $record  Th content to be displayed.
+     * @return string            URL where to visualize the given content.
+     */
+    public function get_view_url(\stdClass $record): string {
+        return new moodle_url('/contentbank/view.php', ['id' => $record->id]);
+    }
+
+    /**
+     * Returns the HTML content to add to view.php visualizer.
+     *
+     * @param stdClass $record  Th content to be displayed.
+     * @return string            HTML code to include in view.php.
+     */
+    public function get_view_content(\stdClass $record): string {
+        // Main contenttype class can visualize the content, but plugins could overwrite visualization.
+        return '';
+    }
+
+    /**
+     * Returns the HTML code to render the icon for content bank contents.
+     *
+     * @param string $contentname   The contentname to add as alt value to the icon.
+     * @return string               HTML code to render the icon
+     */
+    public function get_icon(string $contentname): string {
+        global $OUTPUT;
+        return $OUTPUT->pix_icon('f/unknown-64', $contentname, 'moodle', ['class' => 'iconsize-big']);
+    }
+
+    /**
+     * Returns user has access capability for the main content bank and the content itself (base on is_access_allowed from plugin).
+     *
+     * @return bool     True if content could be accessed. False otherwise.
+     */
+    final public function can_access(): bool {
+        $classname = 'contenttype/'.$this->get_plugin_name();
+        $capability = $classname.":access";
+        $hascapabilities = has_capability('moodle/contentbank:access', $this->context)
+            && has_capability($capability, $this->context);
+        return $hascapabilities && $this->is_access_allowed();
+    }
+
+    /**
+     * Returns user has access capability for the content itself.
+     *
+     * @return bool     True if content could be accessed. False otherwise.
+     */
+    protected function is_access_allowed(): bool {
+        // Plugins can overwrite this function to add any check they need.
+        return true;
+    }
+
+    /**
+     * Returns the user has permission to upload new content.
+     *
+     * @return bool     True if content could be uploaded. False otherwise.
+     */
+    final public function can_upload(): bool {
+        if (!$this->is_feature_supported(self::CAN_UPLOAD)) {
+            return false;
+        }
+        if (!$this->can_access()) {
+            return false;
+        }
+
+        $classname = 'contenttype/'.$this->get_plugin_name();
+        $uploadcap = $classname.':upload';
+        $hascapabilities = has_capability('moodle/contentbank:upload', $this->context)
+            && has_capability($uploadcap, $this->context);
+        return $hascapabilities && $this->is_upload_allowed();
+    }
+
+    /**
+     * Returns plugin allows uploading.
+     *
+     * @return bool     True if plugin allows uploading. False otherwise.
+     */
+    protected function is_upload_allowed(): bool {
+        // Plugins can overwrite this function to add any check they need.
+        return true;
+    }
+
+    /**
+     * Returns the plugin supports the feature.
+     *
+     * @param string $feature Feature code e.g CAN_UPLOAD
+     * @return bool     True if content could be uploaded. False otherwise.
+     */
+    final public function is_feature_supported(string $feature): bool {
+        return in_array($feature, $this->get_implemented_features());
+    }
+
+    /**
+     * Return an array of implemented features by the plugins.
+     *
+     * @return array
+     */
+    abstract protected function get_implemented_features(): array;
+
+    /**
+     * Return an array of extensions the plugins could manage.
+     *
+     * @return array
+     */
+    abstract public function get_manageable_extensions(): array;
+}
diff --git a/contentbank/classes/helper.php b/contentbank/classes/helper.php
new file mode 100644 (file)
index 0000000..463f7ed
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains helper class for the content bank.
+ *
+ * @package    core_contentbank
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_contentbank;
+
+/**
+ * Helper class for the content bank.
+ *
+ * @package    core_contentbank
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class helper {
+
+    /**
+     * Getting content bank page ready for the breadcrumbs.
+     *
+     * @param \context $context Context of the current page.
+     * @param string $title Title of the current page.
+     * @param bool $internal True if is an internal page, false otherwise.
+     */
+    public static function get_page_ready(\context $context, string $title, bool $internal = false): void {
+        global $PAGE, $DB;
+
+        $PAGE->set_context($context);
+        $cburl = new \moodle_url('/contentbank/index.php', ['contextid' => $context->id]);
+
+        switch ($context->contextlevel) {
+            case CONTEXT_COURSE:
+                $courseid = $context->instanceid;
+                $course = $DB->get_record('course', ['id' => $courseid], '*', MUST_EXIST);
+                $PAGE->set_course($course);
+                \navigation_node::override_active_url(new \moodle_url('/course/view.php', ['id' => $courseid]));
+                $PAGE->navbar->add($title, $cburl);
+                $PAGE->set_pagelayout('incourse');
+                break;
+            case CONTEXT_COURSECAT:
+                $coursecat = $context->instanceid;
+                \navigation_node::override_active_url(new \moodle_url('/course/index.php', ['categoryid' => $coursecat]));
+                $PAGE->navbar->add($title, $cburl);
+                $PAGE->set_pagelayout('coursecategory');
+                break;
+            default:
+                if ($node = $PAGE->navigation->find('contentbank', \global_navigation::TYPE_CUSTOM)) {
+                    $node->make_active();
+                }
+                $PAGE->set_pagelayout('standard');
+        }
+    }
+}
diff --git a/contentbank/classes/output/bankcontent.php b/contentbank/classes/output/bankcontent.php
new file mode 100644 (file)
index 0000000..ac1a855
--- /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/>.
+
+/**
+ * core_contentbank specific renderers
+ *
+ * @package   core_contentbank
+ * @copyright  2020 Ferran Recio <ferran@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_contentbank\output;
+
+use renderable;
+use templatable;
+use renderer_base;
+use stdClass;
+
+/**
+ * Class containing data for bank content
+ *
+ * @copyright  2020 Ferran Recio <ferran@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class bankcontent implements renderable, templatable {
+
+    /**
+     * @var \core_contentbank\content[]    Array of content bank contents.
+     */
+    private $contents;
+
+    /**
+     * @var array   $toolbar object.
+     */
+    private $toolbar;
+
+    /**
+     * @var \context    Given context. Null by default.
+     */
+    private $context;
+
+    /**
+     * Construct this renderable.
+     *
+     * @param \core_contentbank\content[] $contents   Array of content bank contents.
+     * @param array $toolbar     List of content bank toolbar options.
+     * @param \context $context Optional context to check (default null)
+     */
+    public function __construct(array $contents, array $toolbar, \context $context = null) {
+        $this->contents = $contents;
+        $this->toolbar = $toolbar;
+        $this->context = $context;
+    }
+
+    /**
+     * Export the data.
+     *
+     * @param renderer_base $output
+     * @return stdClass
+     */
+    public function export_for_template(renderer_base $output): stdClass {
+        $data = new stdClass();
+        $contentdata = array();
+        foreach ($this->contents as $content) {
+            $record = $content->get_content();
+            $managerclass = $content->get_content_type().'\\contenttype';
+            if (class_exists($managerclass)) {
+                $manager = new $managerclass($this->context);
+                if ($manager->can_access()) {
+                    $name = $content->get_name();
+                    $contentdata[] = array(
+                        'name' => $name,
+                        'link' => $manager->get_view_url($record),
+                        'icon' => $manager->get_icon($name)
+                    );
+                }
+            }
+        }
+        $data->contents = $contentdata;
+        $data->tools = $this->toolbar;
+        return $data;
+    }
+}
diff --git a/contentbank/classes/privacy/provider.php b/contentbank/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..3869f3d
--- /dev/null
@@ -0,0 +1,129 @@
+<?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/>.
+
+/**
+ * Privacy provider implementation for core_contentbank.
+ *
+ * @package    core_contentbank
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_contentbank\privacy;
+
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\approved_userlist;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\userlist;
+
+/**
+ * Privacy provider implementation for core_contentbank.
+ *
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements
+    \core_privacy\local\metadata\provider,
+    \core_privacy\local\request\core_userlist_provider,
+    \core_privacy\local\request\plugin\provider {
+
+    /**
+     * Returns metadata.
+     * TODO: MDL-67798.
+     *
+     * @param collection $collection The initialised collection to add items to.
+     * @return collection A listing of user data stored through this system.
+     */
+    public static function get_metadata(collection $collection) : collection {
+        // We are not implementing a proper privacy provider for now.
+        // A right privacy provider will be implemented in MDL-67798.
+
+        $collection->add_database_table('contentbank_content', [
+            'usercreated' => 'privacy:metadata:content:usercreated',
+            'usermodified' => 'privacy:metadata:content:usermodified',
+        ], 'privacy:metadata:userid');
+
+        return $collection;
+    }
+
+    /**
+     * TODO: MDL-67798.
+     *
+     * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
+     */
+    public static function get_users_in_context(userlist $userlist) {
+        // We are not implementing a proper privacy provider for now.
+        // A right privacy provider will be implemented in MDL-67798.
+    }
+
+    /**
+     * TODO: MDL-67798.
+     *
+     * @param   approved_userlist       $userlist The approved context and user information to delete information for.
+     */
+    public static function delete_data_for_users(approved_userlist $userlist) {
+        // We are not implementing a proper privacy provider for now.
+        // A right privacy provider will be implemented in MDL-67798.
+    }
+
+    /**
+     * TODO: MDL-67798.
+     * Get the list of contexts that contain user information for the specified user.
+     *
+     * @param   int         $userid     The user to search.
+     * @return  contextlist   $contextlist  The contextlist containing the list of contexts used in this plugin.
+     */
+    public static function get_contexts_for_userid(int $userid) : contextlist {
+        // We are not implementing a proper privacy provider for now.
+        // A right privacy provider will be implemented in MDL-67798.
+
+        return (new contextlist());
+    }
+
+    /**
+     * TODO: MDL-67798.
+     * Export all user data for the specified user, in the specified contexts.
+     *
+     * @param   approved_contextlist    $contextlist    The approved contexts to export information for.
+     */
+    public static function export_user_data(approved_contextlist $contextlist) {
+        // We are not implementing a proper privacy provider for now.
+        // A right privacy provider will be implemented in MDL-67798.
+    }
+
+    /**
+     * TODO: MDL-67798.
+     * Delete all data for all users in the specified context.
+     *
+     * @param   context                 $context   The specific context to delete data for.
+     */
+    public static function delete_data_for_all_users_in_context(\context $context) {
+        // We are not implementing a proper privacy provider for now.
+        // A right privacy provider will be implemented in MDL-67798.
+    }
+
+    /**
+     * TODO: MDL-67798.
+     * Delete all user data for the specified user, in the specified contexts.
+     *
+     * @param   approved_contextlist    $contextlist    The approved contexts and user information to delete information for.
+     */
+    public static function delete_data_for_user(approved_contextlist $contextlist) {
+        // We are not implementing a proper privacy provider for now.
+        // A right privacy provider will be implemented in MDL-67798.
+    }
+}
diff --git a/contentbank/contenttype/h5p/classes/content.php b/contentbank/contenttype/h5p/classes/content.php
new file mode 100644 (file)
index 0000000..1457476
--- /dev/null
@@ -0,0 +1,38 @@
+<?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/>.
+
+/**
+ * H5P Content manager class
+ *
+ * @package    contenttype_h5p
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace contenttype_h5p;
+
+use stdClass;
+use html_writer;
+
+/**
+ * H5P Content manager class
+ *
+ * @package    contenttype_h5p
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class content extends \core_contentbank\content {
+}
diff --git a/contentbank/contenttype/h5p/classes/contenttype.php b/contentbank/contenttype/h5p/classes/contenttype.php
new file mode 100644 (file)
index 0000000..6c100cb
--- /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/>.
+
+/**
+ * H5P content type manager class
+ *
+ * @package    contenttype_h5p
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace contenttype_h5p;
+
+use stdClass;
+use html_writer;
+
+/**
+ * H5P content bank manager class
+ *
+ * @package    contenttype_h5p
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class contenttype extends \core_contentbank\contenttype {
+
+    /**
+     * Returns the HTML content to add to view.php visualizer.
+     *
+     * @param stdClass $record  Th content to be displayed.
+     * @return string            HTML code to include in view.php.
+     */
+    public function get_view_content(\stdClass $record): string {
+        $content = new content($record);
+        $fileurl = $content->get_file_url();
+        $html = html_writer::tag('h2', $content->get_name());
+        $html .= \core_h5p\player::display($fileurl, new \stdClass(), true);
+        return $html;
+    }
+
+    /**
+     * Returns the HTML code to render the icon for H5P content types.
+     *
+     * @param string $contentname   The contentname to add as alt value to the icon.
+     * @return string            HTML code to render the icon
+     */
+    public function get_icon(string $contentname): string {
+        global $OUTPUT;
+        return $OUTPUT->pix_icon('f/h5p-64', $contentname, 'moodle', ['class' => 'iconsize-big']);
+    }
+
+    /**
+     * Return an array of implemented features by this plugin.
+     *
+     * @return array
+     */
+    protected function get_implemented_features(): array {
+        return [self::CAN_UPLOAD];
+    }
+
+    /**
+     * Return an array of extensions this contenttype could manage.
+     *
+     * @return array
+     */
+    public function get_manageable_extensions(): array {
+        return ['.h5p'];
+    }
+
+    /**
+     * Returns user has access capability for the content itself.
+     *
+     * @return bool     True if content could be accessed. False otherwise.
+     */
+    protected function is_access_allowed(): bool {
+        return true;
+    }
+}
diff --git a/contentbank/contenttype/h5p/classes/privacy/provider.php b/contentbank/contenttype/h5p/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..48aa9e2
--- /dev/null
@@ -0,0 +1,44 @@
+<?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/>.
+
+/**
+ * Privacy provider implementation for core_contentbank.
+ *
+ * @package    contenttype_h5p
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace contenttype_h5p\privacy;
+
+/**
+ * Privacy provider implementation for contenttype_h5p.
+ *
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
diff --git a/contentbank/contenttype/h5p/db/access.php b/contentbank/contenttype/h5p/db/access.php
new file mode 100644 (file)
index 0000000..95db4aa
--- /dev/null
@@ -0,0 +1,47 @@
+<?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/>.
+
+/**
+ * H5P content bank contenttype capabilities.
+ *
+ * @package    contenttype_h5p
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$capabilities = [
+    'contenttype/h5p:access' => array(
+        'captype' => 'read',
+        'contextlevel' => CONTEXT_COURSE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'coursecreator' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW,
+        )
+    ),
+    'contenttype/h5p:upload' => [
+        'riskbitmask' => RISK_SPAM,
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_COURSE,
+        'archetypes' => [
+            'manager' => CAP_ALLOW,
+            'coursecreator' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW,
+        ]
+    ],
+];
diff --git a/contentbank/contenttype/h5p/lang/en/contenttype_h5p.php b/contentbank/contenttype/h5p/lang/en/contenttype_h5p.php
new file mode 100644 (file)
index 0000000..10ea721
--- /dev/null
@@ -0,0 +1,29 @@
+<?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 plugin 'contenttype_h5p'
+ *
+ * @package    contenttype_h5p
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['pluginname'] = 'H5P';
+$string['pluginname_help'] = 'Content bank to upload and share H5P content';
+$string['privacy:metadata'] = 'The H5P content bank plugin does not store any personal data.';
+$string['h5p:access'] = 'Access to H5P content in the content bank';
+$string['h5p:upload'] = 'Upload a new H5P content';
diff --git a/contentbank/contenttype/h5p/tests/behat/admin_upload_content.feature b/contentbank/contenttype/h5p/tests/behat/admin_upload_content.feature
new file mode 100644 (file)
index 0000000..8d03118
--- /dev/null
@@ -0,0 +1,73 @@
+@core @core_contentbank @contenttype_h5p @_file_upload @_switch_iframe @javascript
+Feature: H5P file upload to content bank for admins
+  In order import new H5P content to content bank
+  As an admin
+  I need to be able to upload a new .h5p file to content bank
+
+  Background:
+    Given I log in as "admin"
+    And I follow "Manage private files..."
+    And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager
+    And I click on "Save changes" "button"
+    And I press "Customise this page"
+    And I add the "Navigation" block if not present
+    And I expand "Site pages" node
+    And I click on "Content bank" "link"
+
+  Scenario: Admins can upload .h5p extension files to content bank
+    Given I should not see "filltheblanks.h5p"
+    When I click on "Upload" "link"
+    And I click on "Choose a file..." "button"
+    And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
+    And I click on "filltheblanks.h5p" "link"
+    And I click on "Select this file" "button"
+    And I click on "Save changes" "button"
+    And I wait until the page is ready
+    Then I should see "filltheblanks.h5p"
+
+  Scenario: Admins can see uploaded H5P contents
+    Given I should not see "filltheblanks.h5p"
+    When I click on "Upload" "link"
+    And I click on "Choose a file..." "button"
+    And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
+    And I click on "filltheblanks.h5p" "link"
+    And I click on "Select this file" "button"
+    And I click on "Save changes" "button"
+    And I wait until the page is ready
+    And I click on "filltheblanks.h5p" "link"
+    And I switch to "h5p-player" class iframe
+    And I switch to "h5p-iframe" class iframe
+    Then I should see "Of which countries"
+
+  Scenario: Users can't see content managed by disabled plugins
+    Given I click on "Upload" "link"
+    And I click on "Choose a file..." "button"
+    And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
+    And I click on "filltheblanks.h5p" "link"
+    And I click on "Select this file" "button"
+    And I click on "Save changes" "button"
+    And I wait until the page is ready
+    And I should see "filltheblanks.h5p"
+    And I navigate to "Plugins > Content bank > Manage content bank content types" in site administration
+    And I click on "Disable" "icon" in the "H5P" "table_row"
+    And I wait until the page is ready
+    When I click on "Content bank" "link"
+    Then I should not see "filltheblanks.h5p"
+
+  Scenario: Contents in a context are not available from other contexts
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+    When I click on "Upload" "link"
+    And I click on "Choose a file..." "button"
+    And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
+    And I click on "filltheblanks.h5p" "link"
+    And I click on "Select this file" "button"
+    And I click on "Save changes" "button"
+    And I wait until the page is ready
+    Then I should see "filltheblanks.h5p"
+    And I am on "Course 1" course homepage
+    And I add the "Navigation" block if not present
+    And I expand "Site pages" node
+    And I click on "Content bank" "link"
+    And I should not see "filltheblanks.h5p"
\ No newline at end of file
diff --git a/contentbank/contenttype/h5p/tests/behat/teacher_upload_content.feature b/contentbank/contenttype/h5p/tests/behat/teacher_upload_content.feature
new file mode 100644 (file)
index 0000000..9c25ac9
--- /dev/null
@@ -0,0 +1,73 @@
+@core @core_contentbank @contenttype_h5p @_file_upload @_switch_iframe @javascript
+Feature: H5P file upload to content bank for non admins
+  In order import new H5P content to content bank
+  As an admin
+  I need to be able to upload a new .h5p file to content bank
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+      | teacher2 | Teacher   | 2        | teacher2@example.com |
+    And the following "categories" exist:
+      | name  | category | idnumber |
+      | Cat 1 | 0        | CAT1     |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | CAT1     |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | teacher2 | C1     | editingteacher |
+    And I log in as "teacher1"
+    And I follow "Manage private files..."
+    And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager
+    And I click on "Save changes" "button"
+
+  Scenario: Teachers can not access system level content bank
+    Given I press "Customise this page"
+    And I add the "Navigation" block if not present
+    When I expand "Site pages" node
+    Then I should not see "Content bank"
+
+  Scenario: Teachers can access course level content bank
+    Given I am on "Course 1" course homepage with editing mode on
+    And I add the "Navigation" block if not present
+    When I expand "Site pages" node
+    Then I should see "Content bank"
+
+  Scenario: Teachers can upload .h5p extension files to course content bank
+    Given I am on "Course 1" course homepage with editing mode on
+    And I add the "Navigation" block if not present
+    When I expand "Site pages" node
+    And I click on "Content bank" "link"
+    Then I should not see "filltheblanks.h5p"
+    And I click on "Upload" "link"
+    And I click on "Choose a file..." "button"
+    And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
+    And I click on "filltheblanks.h5p" "link"
+    And I click on "Select this file" "button"
+    And I click on "Save changes" "button"
+    And I wait until the page is ready
+    And I should see "filltheblanks.h5p"
+
+  Scenario: Other teachers can see uploaded H5P contents
+    Given I am on "Course 1" course homepage with editing mode on
+    And I add the "Navigation" block if not present
+    When I expand "Site pages" node
+    And I click on "Content bank" "link"
+    And I click on "Upload" "link"
+    And I click on "Choose a file..." "button"
+    And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
+    And I click on "filltheblanks.h5p" "link"
+    And I click on "Select this file" "button"
+    And I click on "Save changes" "button"
+    And I wait until the page is ready
+    And I should see "filltheblanks.h5p"
+    And I log out
+    When I log in as "teacher2"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add the "Navigation" block if not present
+    And I expand "Site pages" node
+    And I click on "Content bank" "link"
+    Then I should see "filltheblanks.h5p"
diff --git a/contentbank/contenttype/h5p/tests/content_h5p_test.php b/contentbank/contenttype/h5p/tests/content_h5p_test.php
new file mode 100644 (file)
index 0000000..a4082f8
--- /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/>.
+
+/**
+ * Test for H5P content bank plugin.
+ *
+ * @package    contenttype_h5p
+ * @category   test
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Test for H5P content bank plugin.
+ *
+ * @package    contenttype_h5p
+ * @category   test
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \contenttype_h5p\content
+ */
+class contenttype_h5p_content_plugin_testcase extends advanced_testcase {
+
+    /**
+     * Tests for uploaded file.
+     *
+     * @covers ::get_file
+     */
+    public function test_upload_file() {
+        $this->resetAfterTest();
+
+        // Create content.
+        $record = new stdClass();
+        $record->name = 'Test content';
+        $record->configdata = '';
+        $contenttype = new \contenttype_h5p\contenttype(context_system::instance());
+        $content = $contenttype->create_content($record);
+
+        // Create a dummy file.
+        $filename = 'content.h5p';
+        $dummy = array(
+            'contextid' => \context_system::instance()->id,
+            'component' => 'contentbank',
+            'filearea' => 'public',
+            'itemid' => $content->get_id(),
+            'filepath' => '/',
+            'filename' => $filename
+        );
+        $fs = get_file_storage();
+        $fs->create_file_from_string($dummy, 'dummy content');
+
+        $file = $content->get_file();
+        $this->assertInstanceOf(\stored_file::class, $file);
+        $this->assertEquals($filename, $file->get_filename());
+    }
+}
diff --git a/contentbank/contenttype/h5p/tests/contenttype_h5p_test.php b/contentbank/contenttype/h5p/tests/contenttype_h5p_test.php
new file mode 100644 (file)
index 0000000..a2daa82
--- /dev/null
@@ -0,0 +1,67 @@
+<?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/>.
+
+/**
+ * Test for H5P content bank plugin.
+ *
+ * @package    contenttype_h5p
+ * @category   test
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Test for H5P content bank plugin.
+ *
+ * @package    contenttype_h5p
+ * @category   test
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \contenttype_h5p\contenttype
+ */
+class contenttype_h5p_contenttype_plugin_testcase extends advanced_testcase {
+
+    /**
+     * Tests can_upload behavior.
+     *
+     * @covers ::can_upload
+     */
+    public function test_can_upload() {
+        $this->resetAfterTest();
+
+        $systemcontext = \context_system::instance();
+        $systemtype = new \contenttype_h5p\contenttype($systemcontext);
+
+        // Admins can upload.
+        $this->setAdminUser();
+        $this->assertTrue($systemtype->can_upload());
+
+        // Teacher can upload in the course but not at system level.
+        $course = $this->getDataGenerator()->create_course();
+        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $coursecontext = \context_course::instance($course->id);
+        $coursetype = new \contenttype_h5p\contenttype($coursecontext);
+        $this->setUser($teacher);
+        $this->assertTrue($coursetype->can_upload());
+        $this->assertFalse($systemtype->can_upload());
+
+        // Users can't upload.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+        $this->assertFalse($coursetype->can_upload());
+        $this->assertFalse($systemtype->can_upload());
+    }
+}
diff --git a/contentbank/contenttype/h5p/version.php b/contentbank/contenttype/h5p/version.php
new file mode 100644 (file)
index 0000000..548ef13
--- /dev/null
@@ -0,0 +1,29 @@
+<?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/>.
+
+/**
+ * Version details
+ *
+ * @package   contenttype_h5p
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2020041500.00;         // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2020041500.00;         // Requires this Moodle version
+$plugin->component = 'contenttype_h5p'; // Full name of the plugin (used for diagnostics).
diff --git a/contentbank/files_form.php b/contentbank/files_form.php
new file mode 100644 (file)
index 0000000..d94f61f
--- /dev/null
@@ -0,0 +1,73 @@
+<?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/>.
+
+/**
+ * Upload files to content bank form
+ *
+ * @package    core_contentbank
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once("$CFG->libdir/formslib.php");
+
+/**
+ * Class contentbank_files_form
+ *
+ * @package    core_contentbank
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class contentbank_files_form extends moodleform {
+
+    /**
+     * Add elements to this form.
+     */
+    public function definition() {
+        $mform = $this->_form;
+
+        $mform->addElement('hidden', 'contextid', $this->_customdata['contextid']);
+        $mform->setType('contextid', PARAM_INT);
+
+        $options = $this->_customdata['options'];
+        $mform->addElement('filepicker', 'file', get_string('file', 'core_contentbank'), null, $options);
+        $mform->addHelpButton('file', 'file', 'core_contentbank');
+        $mform->addRule('file', null, 'required');
+
+        $this->add_action_buttons(true, get_string('savechanges'));
+
+        $data = $this->_customdata['data'];
+        $this->set_data($data);
+    }
+
+    /**
+     * Validate incoming data.
+     *
+     * @param array $data
+     * @param array $files
+     * @return array
+     */
+    public function validation($data, $files) {
+        $errors = array();
+        $draftitemid = $data['file'];
+        if (file_is_draft_area_limit_reached($draftitemid, $this->_customdata['options']['areamaxbytes'])) {
+            $errors['file'] = get_string('userquotalimit', 'error');
+        }
+        return $errors;
+    }
+}
diff --git a/contentbank/index.php b/contentbank/index.php
new file mode 100644 (file)
index 0000000..01d1db6
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * List content in content bank.
+ *
+ * @package    core_contentbank
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require('../config.php');
+
+require_login();
+
+$contextid    = optional_param('contextid', \context_system::instance()->id, PARAM_INT);
+$context = context::instance_by_id($contextid, MUST_EXIST);
+
+require_capability('moodle/contentbank:access', $context);
+
+$title = get_string('contentbank');
+\core_contentbank\helper::get_page_ready($context, $title);
+if ($PAGE->course) {
+    require_login($PAGE->course->id);
+}
+$PAGE->set_url('/contentbank/index.php');
+$PAGE->set_context($context);
+$PAGE->set_title($title);
+$PAGE->set_heading($title);
+$PAGE->set_pagetype('contenbank');
+
+// Get all contents managed by active plugins to render.
+$foldercontents = array();
+$contents = $DB->get_records('contentbank_content', ['contextid' => $contextid]);
+foreach ($contents as $content) {
+    $plugin = core_plugin_manager::instance()->get_plugin_info($content->contenttype);
+    if (!$plugin || !$plugin->is_enabled()) {
+        continue;
+    }
+    $contentclass = "\\$content->contenttype\\content";
+    if (class_exists($contentclass)) {
+        $contentmanager = new $contentclass($content);
+        if ($contentmanager->can_view()) {
+            $foldercontents[] = $contentmanager;
+        }
+    }
+}
+
+// Get the toolbar ready.
+$toolbar = array ();
+if (has_capability('moodle/contentbank:upload', $context)) {
+    // Don' show upload button if there's no plugin to support any file extension.
+    $cb = new \core_contentbank\contentbank();
+    $accepted = $cb->get_supported_extensions_as_string($context);
+    if (!empty($accepted)) {
+        $importurl = new moodle_url('/contentbank/upload.php', ['contextid' => $contextid]);
+        $toolbar[] = array('name' => 'Upload', 'link' => $importurl, 'icon' => 'i/upload');
+    }
+}
+
+echo $OUTPUT->header();
+echo $OUTPUT->box_start('generalbox');
+
+$folder = new \core_contentbank\output\bankcontent($foldercontents, $toolbar, $context);
+echo $OUTPUT->render($folder);
+
+echo $OUTPUT->box_end();
+echo $OUTPUT->footer();
diff --git a/contentbank/templates/bankcontent.mustache b/contentbank/templates/bankcontent.mustache
new file mode 100644 (file)
index 0000000..5b1a1dd
--- /dev/null
@@ -0,0 +1,73 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_contentbank/list
+
+    Example context (json):
+    {
+        "contents": [
+            {
+                "name": "accordion.h5p",
+                "link": "http://something/contentbank/contenttype/h5p/view.php?url=http://something/pluginfile.php/1/contentbank/public/accordion.h5p",
+                "icon" : "<img class='icon iconsize-big' alt='accordion.h5p' aria-hidden='true' src='http://something/theme/image.php/boost/core/1581597850/f/h5p-64'>"
+            },
+            {
+                "name": "resume.pdf",
+                "icon": "<img class='icon iconsize-big' alt='resume.pdf' aria-hidden='true' src='http://something/theme/image.php/boost/core/1584597850/f/pdf-64'>"
+            }
+        ],
+        "tools": [
+            {
+                "name": "Upload",
+                "link": "http://something/contentbank/contenttype/h5p/view.php?url=http://something/pluginfile.php/1/contentbank/public/accordion.h5p",
+                "icon" : "i/upload"
+            },
+            {
+                "icon": "i/export"
+            }
+        ]
+    }
+
+}}
+{{>core_contentbank/toolbar}}
+<div class="content-bank-container card">
+    <div class="content-bank">
+        <div class="cb-navbar">
+            {{#pix}} i/folder {{/pix}}
+        </div>
+        <div class="cb-content-wrapper">
+        {{#contents}}
+            <div class="cb-content">
+                <div class="cb-iconview">
+                    <div class="cb-file text-center position-relative">
+                        {{#link}}<a href="{{{ link }}}">{{/link}}
+                            <div style="position:relative;">
+                                <div class="cb-thumbnail text-center d-block" style="width: 110px; height: 110px;">
+                                    {{{ icon }}}
+                                </div>
+                            </div>
+                            <div class="cb-contentname-field position-absolute overflow-visible">
+                                <div class="cb-contentname text-truncate" style="width: 112px;">{{{ name }}}</div>
+                            </div>
+                        {{#link}}</a>{{/link}}
+                    </div>
+                </div>
+            </div>
+        {{/contents}}
+        </div>
+    </div>
+</div>
diff --git a/contentbank/templates/toolbar.mustache b/contentbank/templates/toolbar.mustache
new file mode 100644 (file)
index 0000000..7a041a6
--- /dev/null
@@ -0,0 +1,47 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_contentbank/toolbar
+
+    Example context (json):
+    {
+        "tools": [
+            {
+                "name": "Upload",
+                "link": "http://something/contentbank/contenttype/h5p/view.php?url=http://something/pluginfile.php/1/contentbank/public/accordion.h5p",
+                "icon" : "i/upload"
+            },
+            {
+                "icon": "i/export"
+            }
+        ]
+    }
+
+}}
+<div class="content-bank-toolbar card border-0 mb-3">
+    <div class="content-bank">
+        <div class="cb-toolbar float-sm-right">
+        {{#tools}}
+            {{#link}}<a href="{{{ link }}}" title="{{{ name }}}">{{/link}}
+                <div class="cb-tool btn btn-secondary btn-sm">
+                    {{#pix}} {{{ icon }}} {{/pix}}
+                </div>
+            {{#link}}</a>{{/link}}
+        {{/tools}}
+        </div>
+    </div>
+</div>
diff --git a/contentbank/tests/behat/access_permissions.feature b/contentbank/tests/behat/access_permissions.feature
new file mode 100644 (file)
index 0000000..de1fde0
--- /dev/null
@@ -0,0 +1,29 @@
+@core @core_contentbank
+Feature: Access permission to content Bank
+  In order to control access to content bank
+  As an admin
+  I need to be able to configure users' permissions
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email             |
+      | teacher1 | Teacher   | 1        | user1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+
+  Scenario: Admins access content bank
+    Given I log in as "admin"
+    Then "Content bank" "link" should exist
+
+  Scenario: Editing teachers can access content bank at course level
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    Then "Content bank" "link" should exist
+
+  Scenario: Editing teachers can't access content bank at system level
+    Given I log in as "teacher1"
+    Then "Content bank" "link" should not exist
\ No newline at end of file
diff --git a/contentbank/tests/content_test.php b/contentbank/tests/content_test.php
new file mode 100644 (file)
index 0000000..04b722f
--- /dev/null
@@ -0,0 +1,107 @@
+<?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/>.
+
+/**
+ * Test for content bank contenttype class.
+ *
+ * @package    core_contentbank
+ * @category   test
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_contentbank;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
+require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
+
+use stdClass;
+use context_system;
+use contenttype_testable\contenttype as contenttype;
+/**
+ * Test for content bank contenttype class.
+ *
+ * @package    core_contentbank
+ * @category   test
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \core_contentbank\content
+ *
+ */
+class core_contenttype_content_testcase extends \advanced_testcase {
+
+    /**
+     * Tests for behaviour of get_name().
+     *
+     * @covers ::get_name
+     */
+    public function test_get_name() {
+        $this->resetAfterTest();
+
+        // Create content.
+        $record = new stdClass();
+        $record->name = 'Test content';
+        $record->configdata = '';
+
+        $contenttype = new contenttype(context_system::instance());
+        $content = $contenttype->create_content($record);
+        $this->assertEquals($record->name, $content->get_name());
+    }
+
+    /**
+     * Tests for behaviour of get_content_type().
+     *
+     * @covers ::get_content_type
+     */
+    public function test_get_content_type() {
+        $this->resetAfterTest();
+
+        // Create content.
+        $record = new stdClass();
+        $record->name = 'Test content';
+        $record->configdata = '';
+
+        $contenttype = new contenttype(context_system::instance());
+        $content = $contenttype->create_content($record);
+        $this->assertEquals('contenttype_testable', $content->get_content_type());
+    }
+
+    /**
+     * Tests for 'configdata' behaviour.
+     *
+     * @covers ::set_configdata
+     */
+    public function test_configdata_changes() {
+        $this->resetAfterTest();
+
+        $configdata = "{img: 'icon.svg'}";
+
+        // Create content.
+        $record = new stdClass();
+        $record->configdata = $configdata;
+
+        $contenttype = new contenttype(context_system::instance());
+        $content = $contenttype->create_content($record);
+        $this->assertEquals($configdata, $content->get_configdata());
+
+        $configdata = "{alt: 'Name'}";
+        $content->set_configdata($configdata);
+        $this->assertEquals($configdata, $content->get_configdata());
+    }
+}
diff --git a/contentbank/tests/contentbank_test.php b/contentbank/tests/contentbank_test.php
new file mode 100644 (file)
index 0000000..199b73a
--- /dev/null
@@ -0,0 +1,178 @@
+<?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/>.
+
+/**
+ * Test for extensions manager.
+ *
+ * @package    core_contentbank
+ * @category   test
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
+
+/**
+ * Test for extensions manager.
+ *
+ * @package    core_contentbank
+ * @category   test
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \core_contentbank\contentbank
+ */
+class core_contentbank_testcase extends advanced_testcase {
+    /**
+     * Data provider for test_get_extension_supporter.
+     *
+     * @return  array
+     */
+    public function get_extension_provider() {
+        return [
+            'H5P file' => ['something.h5p', '.h5p'],
+            'PDF file' => ['something.pdf', '.pdf']
+        ];
+    }
+
+    /**
+     * Tests for get_extension() function.
+     *
+     * @dataProvider    get_extension_provider
+     * @param   string  $filename    The filename given
+     * @param   string   $expected   The extension of the file
+     *
+     * @covers ::get_extension
+     */
+    public function test_get_extension(string $filename, string $expected) {
+        $this->resetAfterTest();
+
+        $cb = new \core_contentbank\contentbank();
+
+        $extension = $cb->get_extension($filename);
+        $this->assertEquals($expected, $extension);
+    }
+
+    /**
+     * Data provider for test_load_context_supported_extensions.
+     *
+     * @return  array
+     */
+    public function get_extension_supporters_provider() {
+        return [
+            'H5P first' => [['.h5p' => ['h5p', 'testable']], '.h5p', 'h5p'],
+            'Testable first (but upload not implemented)' => [['.h5p' => ['testable', 'h5p']], '.h5p', 'h5p'],
+        ];
+    }
+
+    /**
+     * Tests for get_extension_supporter() function with admin permissions.
+     *
+     * @dataProvider    get_extension_supporters_provider
+     * @param   array   $supporters   The content type plugin supporters for each extension
+     * @param   string  $extension    The extension of the file given
+     * @param   string  $expected   The supporter contenttype of the file
+     *
+     * @covers ::load_context_supported_extensions
+     */
+    public function test_get_extension_supporter_for_admins(array $supporters, string $extension, string $expected) {
+        $this->resetAfterTest();
+
+        $cb = new \core_contentbank\contentbank();
+        $expectedsupporters = [$extension => $expected];
+
+        $systemcontext = context_system::instance();
+
+        // All contexts allowed for admins.
+        $this->setAdminUser();
+        $contextsupporters = $cb->load_context_supported_extensions($systemcontext);
+        $this->assertEquals($expectedsupporters, $contextsupporters);
+    }
+
+    /**
+     * Tests for get_extension_supporter() function with user default permissions.
+     *
+     * @dataProvider    get_extension_supporters_provider
+     * @param   array   $supporters   The content type plugin supporters for each extension
+     * @param   string  $extension    The extension of the file given
+     * @param   string  $expected   The supporter contenttype of the file
+     *
+     * @covers ::load_context_supported_extensions
+     */
+    public function test_get_extension_supporter_for_users(array $supporters, string $extension, string $expected) {
+        $this->resetAfterTest();
+
+        $cb = new \core_contentbank\contentbank();
+        $systemcontext = context_system::instance();
+
+        // Set a user with no permissions.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        // Users with no capabilities can't upload content.
+        $contextsupporters = $cb->load_context_supported_extensions($systemcontext);
+        $this->assertEquals([], $contextsupporters);
+    }
+
+    /**
+     * Tests for get_extension_supporter() function with teacher defaul permissions.
+     *
+     * @dataProvider    get_extension_supporters_provider
+     * @param   array   $supporters   The content type plugin supporters for each extension
+     * @param   string  $extension    The extension of the file given
+     * @param   string  $expected   The supporter contenttype of the file
+     *
+     * @covers ::load_context_supported_extensions
+     */
+    public function test_get_extension_supporter_for_teachers(array $supporters, string $extension, string $expected) {
+        $this->resetAfterTest();
+
+        $cb = new \core_contentbank\contentbank();
+        $expectedsupporters = [$extension => $expected];
+
+        $course = $this->getDataGenerator()->create_course();
+        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $this->setUser($teacher);
+        $coursecontext = context_course::instance($course->id);
+
+        // Teachers has permission in their context to upload supported by H5P content type.
+        $contextsupporters = $cb->load_context_supported_extensions($coursecontext);
+        $this->assertEquals($expectedsupporters, $contextsupporters);
+    }
+
+    /**
+     * Tests for get_extension_supporter() function.
+     *
+     * @dataProvider    get_extension_supporters_provider
+     * @param   array   $supporters   The content type plugin supporters for each extension
+     * @param   string  $extension    The extension of the file given
+     * @param   string  $expected   The supporter contenttype of the file
+     *
+     * @covers ::get_extension_supporter
+     */
+    public function test_get_extension_supporter(array $supporters, string $extension, string $expected) {
+        $this->resetAfterTest();
+
+        $cb = new \core_contentbank\contentbank();
+        $systemcontext = context_system::instance();
+        $this->setAdminUser();
+
+        $supporter = $cb->get_extension_supporter($extension, $systemcontext);
+        $this->assertEquals($expected, $supporter);
+    }
+}
diff --git a/contentbank/tests/contenttype_test.php b/contentbank/tests/contenttype_test.php
new file mode 100644 (file)
index 0000000..ec231a0
--- /dev/null
@@ -0,0 +1,160 @@
+<?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/>.
+
+/**
+ * Test for content bank contenttype class.
+ *
+ * @package    core_contentbank
+ * @category   test
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_contentbank;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
+require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
+
+use stdClass;
+use context_system;
+use contenttype_testable\contenttype as contenttype;
+/**
+ * Test for content bank contenttype class.
+ *
+ * @package    core_contentbank
+ * @category   test
+ * @copyright  2020 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \core_contentbank\contenttype
+ *
+ */
+class core_contenttype_contenttype_testcase extends \advanced_testcase {
+
+    /**
+     * Tests get_contenttype_name result.
+     *