Merge branch 'MDL-68826-master' of git://github.com/ferranrecio/moodle
authorAdrian Greeve <abgreeve@gmail.com>
Fri, 5 Jun 2020 05:34:00 +0000 (13:34 +0800)
committerAdrian Greeve <abgreeve@gmail.com>
Fri, 5 Jun 2020 05:35:03 +0000 (13:35 +0800)
274 files changed:
admin/index.php
admin/renderer.php
admin/settings/subsystems.php
admin/settings/top.php
admin/settings/userfeedback.php [new file with mode: 0644]
admin/settings/users.php
admin/templates/setting.mustache
admin/tool/moodlenet/amd/build/instance_form.min.js [new file with mode: 0644]
admin/tool/moodlenet/amd/build/instance_form.min.js.map [new file with mode: 0644]
admin/tool/moodlenet/amd/build/select_page.min.js [new file with mode: 0644]
admin/tool/moodlenet/amd/build/select_page.min.js.map [new file with mode: 0644]
admin/tool/moodlenet/amd/build/selectors.min.js [new file with mode: 0644]
admin/tool/moodlenet/amd/build/selectors.min.js.map [new file with mode: 0644]
admin/tool/moodlenet/amd/build/validator.min.js [new file with mode: 0644]
admin/tool/moodlenet/amd/build/validator.min.js.map [new file with mode: 0644]
admin/tool/moodlenet/amd/src/instance_form.js [new file with mode: 0644]
admin/tool/moodlenet/amd/src/select_page.js [new file with mode: 0644]
admin/tool/moodlenet/amd/src/selectors.js [new file with mode: 0644]
admin/tool/moodlenet/amd/src/validator.js [new file with mode: 0644]
admin/tool/moodlenet/classes/external.php [new file with mode: 0644]
admin/tool/moodlenet/classes/local/import_backup_helper.php [new file with mode: 0644]
admin/tool/moodlenet/classes/local/import_handler_info.php [new file with mode: 0644]
admin/tool/moodlenet/classes/local/import_handler_registry.php [new file with mode: 0644]
admin/tool/moodlenet/classes/local/import_info.php [new file with mode: 0644]
admin/tool/moodlenet/classes/local/import_processor.php [new file with mode: 0644]
admin/tool/moodlenet/classes/local/import_strategy.php [new file with mode: 0644]
admin/tool/moodlenet/classes/local/import_strategy_file.php [new file with mode: 0644]
admin/tool/moodlenet/classes/local/import_strategy_link.php [new file with mode: 0644]
admin/tool/moodlenet/classes/local/remote_resource.php [new file with mode: 0644]
admin/tool/moodlenet/classes/local/url.php [new file with mode: 0644]
admin/tool/moodlenet/classes/moodlenet_user_profile.php [new file with mode: 0644]
admin/tool/moodlenet/classes/output/renderer.php [new file with mode: 0644]
admin/tool/moodlenet/classes/output/select_page.php [new file with mode: 0644]
admin/tool/moodlenet/classes/privacy/provider.php [new file with mode: 0644]
admin/tool/moodlenet/classes/profile_manager.php [new file with mode: 0644]
admin/tool/moodlenet/db/services.php [new file with mode: 0644]
admin/tool/moodlenet/db/upgrade.php [new file with mode: 0644]
admin/tool/moodlenet/import.php [new file with mode: 0644]
admin/tool/moodlenet/index.php [new file with mode: 0644]
admin/tool/moodlenet/lang/en/tool_moodlenet.php [new file with mode: 0644]
admin/tool/moodlenet/lib.php [new file with mode: 0644]
admin/tool/moodlenet/options.php [new file with mode: 0644]
admin/tool/moodlenet/pix/MoodleNet.png [new file with mode: 0644]
admin/tool/moodlenet/pix/MoodleNet.svg [new file with mode: 0644]
admin/tool/moodlenet/pix/courses.svg [new file with mode: 0644]
admin/tool/moodlenet/select.php [new file with mode: 0644]
admin/tool/moodlenet/settings.php [new file with mode: 0644]
admin/tool/moodlenet/templates/chooser_footer.mustache [new file with mode: 0644]
admin/tool/moodlenet/templates/chooser_footer_close_mnet.mustache [new file with mode: 0644]
admin/tool/moodlenet/templates/chooser_moodlenet.mustache [new file with mode: 0644]
admin/tool/moodlenet/templates/import_confirmation.mustache [new file with mode: 0644]
admin/tool/moodlenet/templates/import_options_select.mustache [new file with mode: 0644]
admin/tool/moodlenet/templates/select_page.mustache [new file with mode: 0644]
admin/tool/moodlenet/templates/view-cards.mustache [new file with mode: 0644]
admin/tool/moodlenet/tests/import_backup_helper_test.php [new file with mode: 0644]
admin/tool/moodlenet/tests/import_handler_info_test.php [new file with mode: 0644]
admin/tool/moodlenet/tests/import_handler_registry_test.php [new file with mode: 0644]
admin/tool/moodlenet/tests/import_info_test.php [new file with mode: 0644]
admin/tool/moodlenet/tests/import_processor_test.php [new file with mode: 0644]
admin/tool/moodlenet/tests/lib_test.php [new file with mode: 0644]
admin/tool/moodlenet/tests/profile_manager_test.php [new file with mode: 0644]
admin/tool/moodlenet/tests/remote_resource_test.php [new file with mode: 0644]
admin/tool/moodlenet/tests/url_test.php [new file with mode: 0644]
admin/tool/moodlenet/version.php [new file with mode: 0644]
backup/moodle2/restore_qtype_plugin.class.php
backup/tests/quiz_restore_decode_links_test.php [new file with mode: 0644]
badges/edit.php
badges/tests/behat/add_badge.feature
blocks/tests/externallib_test.php
config-dist.php
contentbank/amd/build/sort.min.js
contentbank/amd/build/sort.min.js.map
contentbank/amd/src/sort.js
contentbank/classes/output/bankcontent.php
contentbank/classes/privacy/provider.php
contentbank/contenttype/h5p/classes/form/editor.php
contentbank/contenttype/h5p/tests/behat/admin_upload_content.feature
contentbank/contenttype/h5p/tests/behat/teacher_upload_content.feature
contentbank/index.php
contentbank/lib.php [new file with mode: 0644]
contentbank/templates/bankcontent.mustache
contentbank/templates/bankcontent/toolbar.mustache
contentbank/templates/bankcontent/toolbar_dropdown.mustache
contentbank/tests/behat/edit_content.feature
contentbank/tests/behat/view_preferences.feature [new file with mode: 0644]
contentbank/tests/privacy_test.php
course/amd/build/activitychooser.min.js
course/amd/build/activitychooser.min.js.map
course/amd/build/local/activitychooser/dialogue.min.js
course/amd/build/local/activitychooser/dialogue.min.js.map
course/amd/build/local/activitychooser/repository.min.js
course/amd/build/local/activitychooser/repository.min.js.map
course/amd/src/activitychooser.js
course/amd/src/local/activitychooser/dialogue.js
course/amd/src/local/activitychooser/repository.js
course/classes/local/entity/activity_chooser_footer.php [new file with mode: 0644]
course/externallib.php
course/format/upgrade.txt
course/lib.php
course/templates/activitychooser.mustache
course/templates/local/activitychooser/footer_partial.mustache [new file with mode: 0644]
course/templates/local/activitychooser/help.mustache
course/tests/behat/activity_chooser.feature
course/tests/behat/rename_roles.feature
filter/displayh5p/filter.php
grade/edit/tree/index.php
grade/edit/tree/lib.php
grade/report/grader/lib.php
grade/report/lib.php
grade/tests/behat/behat_grade.php
grade/tests/behat/grade_item_duplication.feature [new file with mode: 0644]
group/tests/behat/create_groups.feature
group/tests/behat/group_description.feature
h5p/ajax.php
h5p/amd/build/editor_display.min.js
h5p/amd/build/editor_display.min.js.map
h5p/amd/src/editor_display.js
h5p/classes/core.php
h5p/classes/editor.php
h5p/classes/editor_ajax.php
h5p/classes/framework.php
h5p/classes/helper.php
h5p/tests/editor_ajax_test.php
install/lang/mwl/langconfig.php [new file with mode: 0644]
lang/en/admin.php
lang/en/contentbank.php
lang/en/deprecated.txt
lang/en/grades.php
lang/en/moodle.php
lang/en/user.php
lib/accesslib.php
lib/adminlib.php
lib/amd/build/userfeedback.min.js [new file with mode: 0644]
lib/amd/build/userfeedback.min.js.map [new file with mode: 0644]
lib/amd/src/userfeedback.js [new file with mode: 0644]
lib/behat/classes/behat_core_generator.php
lib/behat/classes/partial_named_selector.php
lib/behat/core_behat_file_helper.php
lib/behat/form_field/behat_form_field.php
lib/behat/form_field/behat_form_filemanager.php
lib/classes/event/userfeedback_give.php [new file with mode: 0644]
lib/classes/event/userfeedback_remind.php [new file with mode: 0644]
lib/classes/external/userfeedback/generate_url.php [new file with mode: 0644]
lib/classes/external/userfeedback/record_action.php [new file with mode: 0644]
lib/classes/notification.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/user.php
lib/classes/userfeedback.php [new file with mode: 0644]
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/deprecatedlib.php
lib/form/templates/element-advcheckbox-inline.mustache
lib/form/templates/element-checkbox-inline.mustache
lib/form/templates/element-filemanager.mustache
lib/form/templates/element-filepicker.mustache
lib/form/templates/element-group-inline.mustache
lib/form/templates/element-group.mustache
lib/form/templates/element-password-inline.mustache
lib/form/templates/element-password.mustache
lib/form/templates/element-template.mustache
lib/form/templates/element-text-inline.mustache
lib/form/templates/element-text.mustache
lib/form/templates/element-url.mustache
lib/grade/grade_item.php
lib/grade/tests/grade_item_test.php
lib/myprofilelib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/questionlib.php
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/src/dynamic.js
lib/table/amd/src/local/dynamic/repository.js
lib/table/classes/external/dynamic/get.php [moved from lib/table/classes/external/dynamic/fetch.php with 99% similarity]
lib/table/classes/local/filter/filter.php
lib/table/classes/local/filter/filterset.php
lib/table/tests/external/dynamic/get_test.php [moved from lib/table/tests/external/dynamic/fetch_test.php with 94% similarity]
lib/templates/campaign_content.mustache [new file with mode: 0644]
lib/templates/filemanager_fileselect.mustache
lib/templates/local/notification/cta.mustache [new file with mode: 0644]
lib/templates/userfeedback_footer_link.mustache [new file with mode: 0644]
lib/tests/external/userfeedback/generate_url_test.php [new file with mode: 0644]
lib/tests/external/userfeedback/record_action_test.php [new file with mode: 0644]
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js
lib/yui/src/notification/js/dialogue.js
message/output/popup/templates/notification_popover.mustache
message/templates/message_drawer_view_settings_body_content.mustache
message/templates/message_index.mustache
mod/h5pactivity/classes/external/get_results.php
mod/h5pactivity/classes/output/attempt.php
mod/h5pactivity/classes/output/result/sequencing.php
mod/h5pactivity/classes/output/result/truefalse.php
mod/h5pactivity/pix/icon.svg
mod/h5pactivity/tests/behat/contentbank_link.feature
mod/h5pactivity/version.php
mod/lti/launch.php
mod/lti/locallib.php
mod/lti/service/gradebookservices/backup/moodle2/restore_ltiservice_gradebookservices_subplugin.class.php
mod/lti/service/gradebookservices/classes/local/resources/lineitem.php
mod/lti/service/gradebookservices/classes/local/resources/lineitems.php
mod/lti/service/gradebookservices/classes/local/service/gradebookservices.php
mod/lti/service/gradebookservices/tests/gradebookservices_test.php
mod/lti/view.php
mod/quiz/classes/output/edit_renderer.php
mod/quiz/styles.css
mod/url/lib.php
my/index.php
pix/i/bullhorn.svg [new file with mode: 0644]
privacy/classes/local/request/moodle_content_writer.php
question/type/multichoice/renderer.php
question/type/multichoice/styles.css
repository/filepicker.js
repository/tests/behat/cancel_add_file.feature
repository/tests/behat/select_file.feature
repository/upload/tests/behat/behat_repository_upload.php
theme/boost/amd/build/loader.min.js
theme/boost/amd/build/loader.min.js.map
theme/boost/amd/build/pending.min.js
theme/boost/amd/build/pending.min.js.map
theme/boost/amd/src/loader.js
theme/boost/amd/src/pending.js
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/contentbank.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/question.scss
theme/boost/style/moodle.css
theme/boost/templates/columns1.mustache
theme/boost/templates/columns2.mustache
theme/boost/templates/secure.mustache
theme/classic/scss/classic/post.scss
theme/classic/style/moodle.css
theme/classic/templates/columns.mustache
theme/classic/templates/contentonly.mustache
theme/classic/templates/secure.mustache
theme/classic/tests/behat/behat_theme_classic_behat_repository_upload.php
user/amd/build/local/participantsfilter/filter.min.js
user/amd/build/local/participantsfilter/filter.min.js.map
user/amd/build/local/participantsfilter/filtertypes/keyword.min.js
user/amd/build/local/participantsfilter/filtertypes/keyword.min.js.map
user/amd/build/local/participantsfilter/selectors.min.js
user/amd/build/local/participantsfilter/selectors.min.js.map
user/amd/build/participantsfilter.min.js
user/amd/build/participantsfilter.min.js.map
user/amd/build/unified_filter.min.js.map
user/amd/build/unified_filter_datasource.min.js.map
user/amd/src/local/participantsfilter/filter.js
user/amd/src/local/participantsfilter/filtertypes/keyword.js
user/amd/src/local/participantsfilter/selectors.js
user/amd/src/participantsfilter.js
user/amd/src/unified_filter.js
user/amd/src/unified_filter_datasource.js
user/classes/output/participants_filter.php
user/classes/output/unified_filter.php
user/classes/privacy/provider.php
user/classes/table/participants_search.php
user/editlib.php
user/index.php
user/lib.php
user/renderer.php
user/templates/local/participantsfilter/autocomplete_selection_items.mustache
user/templates/local/participantsfilter/filterrow.mustache
user/templates/unified_filter.mustache
user/tests/behat/filter_participants.feature
user/tests/behat/filter_participants_showall.feature
user/tests/behat/view_participants_groups.feature
user/tests/table/participants_search_test.php
user/tests/userlib_test.php
user/upgrade.txt
version.php

index ee3b2e3..5660318 100644 (file)
@@ -899,6 +899,9 @@ if (empty($CFG->disabledevlibdirscheck) && (is_dir($CFG->dirroot.'/vendor') || i
 // Check if the site is being foced onto ssl.
 $overridetossl = !empty($CFG->overridetossl);
 
+// Check if moodle campaign content setting is enabled or not.
+$showcampaigncontent = !isset($CFG->showcampaigncontent) || $CFG->showcampaigncontent;
+
 admin_externalpage_setup('adminnotifications');
 
 $output = $PAGE->get_renderer('core', 'admin');
@@ -906,4 +909,5 @@ $output = $PAGE->get_renderer('core', 'admin');
 echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, $cronoverdue, $dbproblems,
                                        $maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
                                        $registered, $cachewarnings, $eventshandlers, $themedesignermode, $devlibdir,
-                                       $mobileconfigured, $overridetossl, $invalidforgottenpasswordurl, $croninfrequent);
+                                       $mobileconfigured, $overridetossl, $invalidforgottenpasswordurl, $croninfrequent,
+                                       $showcampaigncontent);
index 45f46a5..cda9ed6 100644 (file)
@@ -282,6 +282,7 @@ class core_admin_renderer extends plugin_renderer_base {
      * @param bool $overridetossl Whether or not ssl is being forced.
      * @param bool $invalidforgottenpasswordurl Whether the forgotten password URL does not link to a valid URL.
      * @param bool $croninfrequent If true, warn that cron hasn't run in the past few minutes
+     * @param bool $showcampaigncontent Whether the campaign content should be visible or not.
      *
      * @return string HTML to output.
      */
@@ -289,7 +290,9 @@ class core_admin_renderer extends plugin_renderer_base {
             $cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch,
             $buggyiconvnomb, $registered, array $cachewarnings = array(), $eventshandlers = 0,
             $themedesignermode = false, $devlibdir = false, $mobileconfigured = false,
-            $overridetossl = false, $invalidforgottenpasswordurl = false, $croninfrequent = false) {
+            $overridetossl = false, $invalidforgottenpasswordurl = false, $croninfrequent = false,
+            $showcampaigncontent = false) {
+
         global $CFG;
         $output = '';
 
@@ -312,6 +315,7 @@ class core_admin_renderer extends plugin_renderer_base {
         $output .= $this->registration_warning($registered);
         $output .= $this->mobile_configuration_warning($mobileconfigured);
         $output .= $this->forgotten_password_url_warning($invalidforgottenpasswordurl);
+        $output .= $this->campaign_content($showcampaigncontent);
 
         //////////////////////////////////////////////////////////////////////////////////////////////////
         ////  IT IS ILLEGAL AND A VIOLATION OF THE GPL TO HIDE, REMOVE OR MODIFY THIS COPYRIGHT NOTICE ///
@@ -878,6 +882,20 @@ class core_admin_renderer extends plugin_renderer_base {
         return $output;
     }
 
+    /**
+     * Display campaign content.
+     *
+     * @param bool $showcampaigncontent Whether the campaign content should be visible or not.
+     * @return string the campaign content raw html.
+     */
+    protected function campaign_content(bool $showcampaigncontent): string {
+        if (!$showcampaigncontent) {
+            return '';
+        }
+
+        return $this->render_from_template('core/campaign_content', ['lang' => current_language()]);
+    }
+
     /**
      * Display a warning about the forgotten password URL not linking to a valid URL.
      *
index 14a1331..01de61c 100644 (file)
@@ -74,7 +74,4 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
             new lang_string('configallowemojipickerincompatible', 'admin')
         ));
     }
-
-    $optionalsubsystems->add(new admin_setting_configcheckbox('enablemoodlenet', new lang_string('enablemoodlenet', 'admin'),
-        new lang_string('enablemoodlenet_desc', 'admin'), 1, 1, 0));
 }
index 8717843..992436b 100644 (file)
@@ -20,6 +20,9 @@ $moodleservices = new admin_settingpage('moodleservices', new lang_string('moodl
     'admin'));
 $ADMIN->add('root', $moodleservices);
 
+$userfeedback = new admin_settingpage('userfeedback', new lang_string('feedbacksettings', 'admin'));
+$ADMIN->add('root', $userfeedback);
+
 if ($hassiteconfig) {
     $optionalsubsystems = new admin_settingpage('optionalsubsystems', new lang_string('advancedfeatures', 'admin'));
     $ADMIN->add('root', $optionalsubsystems);
diff --git a/admin/settings/userfeedback.php b/admin/settings/userfeedback.php
new file mode 100644 (file)
index 0000000..b7f3128
--- /dev/null
@@ -0,0 +1,49 @@
+<?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/>.
+
+/**
+ * This file contains call to feedback settings
+ *
+ * @package    core
+ * @copyright  2020 Shamim Rezaie <shamim@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+if ($hassiteconfig) {
+
+    $userfeedback->add(new admin_setting_configcheckbox('enableuserfeedback',
+            new lang_string('enableuserfeedback', 'admin'),
+            new lang_string('enableuserfeedback_desc', 'admin'), 1, 1, 0));
+
+    $options = [
+        core_userfeedback::REMIND_AFTER_UPGRADE => new lang_string('userfeedbackafterupgrade', 'admin'),
+        core_userfeedback::REMIND_PERIODICALLY => new lang_string('userfeedbackperiodically', 'admin'),
+        core_userfeedback::REMIND_NEVER => new lang_string('never'),
+    ];
+    $userfeedback->add(new admin_setting_configselect('userfeedback_nextreminder',
+            new lang_string('userfeedbacknextreminder', 'admin'),
+            new lang_string('userfeedbacknextreminder_desc', 'admin'), 1, $options));
+    $userfeedback->hide_if('userfeedback_nextreminder', 'enableuserfeedback');
+
+    $userfeedback->add(new admin_setting_configtext('userfeedback_remindafter',
+            new lang_string('userfeedbackremindafter', 'admin'),
+            new lang_string('userfeedbackremindafter_desc', 'admin'), 90, PARAM_INT));
+    $userfeedback->hide_if('userfeedback_remindafter', 'enableuserfeedback');
+    $userfeedback->hide_if('userfeedback_remindafter', 'userfeedback_nextreminder', 'eq', 3);
+
+}
index b9612cb..0b7c4a4 100644 (file)
@@ -186,6 +186,7 @@ if ($hassiteconfig
                              'email' => new lang_string('email'),
                              'city' => new lang_string('city'),
                              'country' => new lang_string('country'),
+                             'moodlenetprofile' => new lang_string('moodlenetprofile', 'user'),
                              'timezone' => new lang_string('timezone'),
                              'webpage' => new lang_string('webpage'),
                              'icqnumber' => new lang_string('icqnumber'),
index a3a3a22..498aa2c 100644 (file)
 }}
 <div class="form-item row" id="{{id}}">
     <div class="form-label col-sm-3 text-sm-right">
-        <label {{#labelfor}}for="{{labelfor}}"{{/labelfor}}>
-            {{{title}}}
-            {{#override}}
-                <div class="alert alert-info">{{override}}</div>
-            {{/override}}
-            {{#warning}}
-                <div class="alert alert-warning">{{warning}}</div>
-            {{/warning}}
-        </label>
+        {{#customcontrol}}
+            <p {{#labelfor}}id="{{labelfor}}_label"{{/labelfor}}>
+                {{{title}}}
+                {{#override}}
+                    <div class="alert alert-info">{{override}}</div>
+                {{/override}}
+                {{#warning}}
+                    <div class="alert alert-warning">{{warning}}</div>
+                {{/warning}}
+            </p>
+        {{/customcontrol}}
+        {{^customcontrol}}
+            <label {{#labelfor}}for="{{labelfor}}"{{/labelfor}}>
+                {{{title}}}
+                {{#override}}
+                    <div class="alert alert-info">{{override}}</div>
+                {{/override}}
+                {{#warning}}
+                    <div class="alert alert-warning">{{warning}}</div>
+                {{/warning}}
+            </label>
+        {{/customcontrol}}
         <span class="form-shortname d-block small text-muted">{{{name}}}</span>
     </div>
     <div class="form-setting col-sm-9">
         {{#dependenton}}<div class="form-dependenton mb-4 text-muted">{{{.}}}</div>{{/dependenton}}
     </div>
 </div>
+{{#customcontrol}}
+    {{#js}}
+        require(['jquery'], function($) {
+            $('#{{id}}_label').css('cursor', 'default');
+            $('#{{id}}_label').click(function() {
+                $('#{{id}}')
+                    .find('button, a, input:not([type="hidden"]), select, textarea, [tabindex]')
+                    .filter(':not([disabled]):not([tabindex="0"]):not([tabindex="-1"])')
+                    .first().focus();
+            });
+        });
+    {{/js}}
+{{/customcontrol}}
diff --git a/admin/tool/moodlenet/amd/build/instance_form.min.js b/admin/tool/moodlenet/amd/build/instance_form.min.js
new file mode 100644 (file)
index 0000000..a6f0561
Binary files /dev/null and b/admin/tool/moodlenet/amd/build/instance_form.min.js differ
diff --git a/admin/tool/moodlenet/amd/build/instance_form.min.js.map b/admin/tool/moodlenet/amd/build/instance_form.min.js.map
new file mode 100644 (file)
index 0000000..3e2853c
Binary files /dev/null and b/admin/tool/moodlenet/amd/build/instance_form.min.js.map differ
diff --git a/admin/tool/moodlenet/amd/build/select_page.min.js b/admin/tool/moodlenet/amd/build/select_page.min.js
new file mode 100644 (file)
index 0000000..5588960
Binary files /dev/null and b/admin/tool/moodlenet/amd/build/select_page.min.js differ
diff --git a/admin/tool/moodlenet/amd/build/select_page.min.js.map b/admin/tool/moodlenet/amd/build/select_page.min.js.map
new file mode 100644 (file)
index 0000000..49e4757
Binary files /dev/null and b/admin/tool/moodlenet/amd/build/select_page.min.js.map differ
diff --git a/admin/tool/moodlenet/amd/build/selectors.min.js b/admin/tool/moodlenet/amd/build/selectors.min.js
new file mode 100644 (file)
index 0000000..b1db64f
Binary files /dev/null and b/admin/tool/moodlenet/amd/build/selectors.min.js differ
diff --git a/admin/tool/moodlenet/amd/build/selectors.min.js.map b/admin/tool/moodlenet/amd/build/selectors.min.js.map
new file mode 100644 (file)
index 0000000..8ef6f4b
Binary files /dev/null and b/admin/tool/moodlenet/amd/build/selectors.min.js.map differ
diff --git a/admin/tool/moodlenet/amd/build/validator.min.js b/admin/tool/moodlenet/amd/build/validator.min.js
new file mode 100644 (file)
index 0000000..1c4d02a
Binary files /dev/null and b/admin/tool/moodlenet/amd/build/validator.min.js differ
diff --git a/admin/tool/moodlenet/amd/build/validator.min.js.map b/admin/tool/moodlenet/amd/build/validator.min.js.map
new file mode 100644 (file)
index 0000000..34c0c5b
Binary files /dev/null and b/admin/tool/moodlenet/amd/build/validator.min.js.map differ
diff --git a/admin/tool/moodlenet/amd/src/instance_form.js b/admin/tool/moodlenet/amd/src/instance_form.js
new file mode 100644 (file)
index 0000000..a1443a0
--- /dev/null
@@ -0,0 +1,169 @@
+// 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/>.
+
+/**
+ * Our basic form manager for when a user either enters
+ * their profile url or just wants to browse.
+ *
+ * This file is a mishmash of JS functions we need for both the standalone (M3.7, M3.8)
+ * plugin & Moodle 3.9 functions. The 3.9 Functions have a base understanding that certain
+ * things exist i.e. directory structures for templates. When this feature goes 3.9+ only
+ * The goal is that we can quickly gut all AMD modules into bare JS files and use ES6 guidelines.
+ * Till then this will have to do.
+ *
+ * @module     tool_moodlenet/instance_form
+ * @package    tool_moodlenet
+ * @copyright  2020 Mathew May <mathew.solutions>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['tool_moodlenet/validator',
+        'tool_moodlenet/selectors',
+        'core/loadingicon',
+        'core/templates',
+        'core/notification',
+        'jquery'],
+    function(Validator,
+             Selectors,
+             LoadingIcon,
+             Templates,
+             Notification,
+             $) {
+
+    /**
+     * Add the event listeners to our form.
+     *
+     * @method registerListenerEvents
+     * @param {HTMLElement} page The whole page element for our form area
+     */
+    var registerListenerEvents = function registerListenerEvents(page) {
+        page.addEventListener('click', function(e) {
+
+            // Our fake submit button / browse button.
+            if (e.target.matches(Selectors.action.submit)) {
+                var input = page.querySelector('[data-var="mnet-link"]');
+                var overlay = page.querySelector(Selectors.region.spinner);
+                var validationArea = document.querySelector(Selectors.region.validationArea);
+
+                overlay.classList.remove('d-none');
+                var spinner = LoadingIcon.addIconToContainerWithPromise(overlay);
+                Validator.validation(input)
+                    .then(function(result) {
+                        spinner.resolve();
+                        overlay.classList.add('d-none');
+                        if (result.result) {
+                            input.classList.remove('is-invalid'); // Just in case the class has been applied already.
+                            input.classList.add('is-valid');
+                            validationArea.innerText = result.message;
+                            validationArea.classList.remove('text-error');
+                            validationArea.classList.add('text-success');
+                            // Give the user some time to see their input is valid.
+                            setTimeout(function() {
+                                window.location = result.domain;
+                            }, 1000);
+                        } else {
+                            input.classList.add('is-invalid');
+                            validationArea.innerText = result.message;
+                            validationArea.classList.add('text-error');
+                        }
+                        return;
+                }).catch();
+            }
+        });
+    };
+
+    /**
+     * Given a user wishes to see the MoodleNet profile url form transition them there.
+     *
+     * @method chooserNavigateToMnet
+     * @param {HTMLElement} showMoodleNet The chooser's area for ment
+     * @param {Object} footerData Our footer object to render out
+     * @param {jQuery} carousel Our carousel instance to manage
+     * @param {jQuery} modal Our modal instance to manage
+     */
+    var chooserNavigateToMnet = function(showMoodleNet, footerData, carousel, modal) {
+        showMoodleNet.innerHTML = '';
+
+        // Add a spinner.
+        var spinnerPromise = LoadingIcon.addIconToContainer(showMoodleNet);
+
+        // Used later...
+        var transitionPromiseResolver = null;
+        var transitionPromise = new Promise(resolve => {
+            transitionPromiseResolver = resolve;
+        });
+
+        $.when(
+            spinnerPromise,
+            transitionPromise
+        ).then(function() {
+                Templates.replaceNodeContents(showMoodleNet, footerData.customcarouseltemplate, '');
+                return;
+        }).catch(Notification.exception);
+
+        // We apply our handlers in here to minimise plugin dependency in the Chooser.
+        registerListenerEvents(showMoodleNet);
+
+        // Move to the next slide, and resolve the transition promise when it's done.
+        carousel.one('slid.bs.carousel', function() {
+            transitionPromiseResolver();
+        });
+        // Trigger the transition between 'pages'.
+        carousel.carousel(2);
+        // eslint-disable-next-line max-len
+        modal.setFooter(Templates.render('tool_moodlenet/chooser_footer_close_mnet', {}));
+    };
+
+    /**
+     * Given a user no longer wishes to see the MoodleNet profile url form transition them from there.
+     *
+     * @method chooserNavigateFromMnet
+     * @param {jQuery} carousel Our carousel instance to manage
+     * @param {jQuery} modal Our modal instance to manage
+     * @param {Object} footerData Our footer object to render out
+     */
+    var chooserNavigateFromMnet = function(carousel, modal, footerData) {
+        // Trigger the transition between 'pages'.
+        carousel.carousel(0);
+        modal.setFooter(footerData.customfootertemplate);
+    };
+
+        /**
+         * Create the custom listener that would handle anything in the footer.
+         *
+         * @param {Event} e The event being triggered.
+         * @param {Object} footerData The data generated from the exporter.
+         * @param {Object} modal The chooser modal.
+         */
+    var footerClickListener = function(e, footerData, modal) {
+        if (e.target.matches(Selectors.action.showMoodleNet) || e.target.closest(Selectors.action.showMoodleNet)) {
+            e.preventDefault();
+            const carousel = $(modal.getBody()[0].querySelector(Selectors.region.carousel));
+            const showMoodleNet = carousel.find(Selectors.region.moodleNet)[0];
+
+            chooserNavigateToMnet(showMoodleNet, footerData, carousel, modal);
+        }
+        // From the help screen go back to the module overview.
+        if (e.target.matches(Selectors.action.closeOption)) {
+            const carousel = $(modal.getBody()[0].querySelector(Selectors.region.carousel));
+
+            chooserNavigateFromMnet(carousel, modal, footerData);
+        }
+    };
+
+    return {
+        footerClickListener: footerClickListener
+    };
+});
diff --git a/admin/tool/moodlenet/amd/src/select_page.js b/admin/tool/moodlenet/amd/src/select_page.js
new file mode 100644 (file)
index 0000000..d9a1dc4
--- /dev/null
@@ -0,0 +1,198 @@
+// 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/>.
+
+/**
+ * When returning to Moodle let the user select which course to add the resource to.
+ *
+ * @module     tool_moodlenet/select_page
+ * @package    tool_moodlenet
+ * @copyright  2020 Mathew May <mathew.solutions>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define([
+    'core/ajax',
+    'core/templates',
+    'tool_moodlenet/selectors',
+    'core/notification'
+], function(
+    Ajax,
+    Templates,
+    Selectors,
+    Notification
+) {
+    /**
+     * @var {string} The id corresponding to the import.
+     */
+    var importId;
+
+    /**
+     * Set up the page.
+     *
+     * @method init
+     * @param {string} importIdString the string ID of the import.
+     */
+    var init = function(importIdString) {
+        importId = importIdString;
+        var page = document.querySelector(Selectors.region.selectPage);
+        registerListenerEvents(page);
+        addCourses(page);
+    };
+
+    /**
+     * Renders the 'no-courses' template.
+     *
+     * @param {HTMLElement} areaReplace the DOM node to replace.
+     * @returns {Promise}
+     */
+    var renderNoCourses = function(areaReplace) {
+        return Templates.renderPix('courses', 'tool_moodlenet').then(function(img) {
+            return img;
+        }).then(function(img) {
+            var temp = document.createElement('div');
+            temp.innerHTML = img.trim();
+            return Templates.render('core_course/no-courses', {
+                nocoursesimg: temp.firstChild.src
+            });
+        }).then(function(html, js) {
+            Templates.replaceNodeContents(areaReplace, html, js);
+            areaReplace.classList.add('mx-auto');
+            areaReplace.classList.add('w-25');
+            return;
+        });
+    };
+
+    /**
+     * Render the course cards for those supplied courses.
+     *
+     * @param {HTMLElement} areaReplace the DOM node to replace.
+     * @param {Array<courses>} courses the courses to render.
+     * @returns {Promise}
+     */
+    var renderCourses = function(areaReplace, courses) {
+        return Templates.render('tool_moodlenet/view-cards', {
+            courses: courses
+        }).then(function(html, js) {
+            Templates.replaceNodeContents(areaReplace, html, js);
+            areaReplace.classList.remove('mx-auto');
+            areaReplace.classList.remove('w-25');
+            return;
+        });
+    };
+
+    /**
+     * For a given input, the page & what to replace fetch courses and manage icons too.
+     *
+     * @method searchCourses
+     * @param {string} inputValue What to search for
+     * @param {HTMLElement} page The whole page element for our page
+     * @param {HTMLElement} areaReplace The Element to replace the contents of
+     */
+    var searchCourses = function(inputValue, page, areaReplace) {
+        var searchIcon = page.querySelector(Selectors.region.searchIcon);
+        var clearIcon = page.querySelector(Selectors.region.clearIcon);
+
+        if (inputValue !== '') {
+            searchIcon.classList.add('d-none');
+            clearIcon.parentElement.classList.remove('d-none');
+        } else {
+            searchIcon.classList.remove('d-none');
+            clearIcon.parentElement.classList.add('d-none');
+        }
+        var args = {
+            searchvalue: inputValue,
+        };
+        Ajax.call([{
+            methodname: 'tool_moodlenet_search_courses',
+            args: args
+        }])[0].then(function(result) {
+            if (result.courses.length === 0) {
+                return renderNoCourses(areaReplace);
+            } else {
+                // Add the importId to the course link
+                result.courses.forEach(function(course) {
+                    course.viewurl += '&id=' + importId;
+                });
+                return renderCourses(areaReplace, result.courses);
+            }
+        }).catch(Notification.exception);
+    };
+
+    /**
+     * Add the event listeners to our page.
+     *
+     * @method registerListenerEvents
+     * @param {HTMLElement} page The whole page element for our page
+     */
+    var registerListenerEvents = function(page) {
+        var input = page.querySelector(Selectors.region.searchInput);
+        var courseArea = page.querySelector(Selectors.region.courses);
+        var clearIcon = page.querySelector(Selectors.region.clearIcon);
+        clearIcon.addEventListener('click', function() {
+            input.value = '';
+            searchCourses('', page, courseArea);
+        });
+
+        input.addEventListener('input', debounce(function() {
+            searchCourses(input.value, page, courseArea);
+        }, 300));
+    };
+
+    /**
+     * Fetch the courses to show the user. We use the same WS structure & template as the search for consistency.
+     *
+     * @method addCourses
+     * @param {HTMLElement} page The whole page element for our course page
+     */
+    var addCourses = function(page) {
+        var courseArea = page.querySelector(Selectors.region.courses);
+        searchCourses('', page, courseArea);
+    };
+
+    /**
+     * Define our own debounce function as Moodle 3.7 does not have it.
+     *
+     * @method debounce
+     * @from underscore.js
+     * @copyright 2009-2020 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+     * @licence MIT
+     * @param {function} func The function we want to keep calling
+     * @param {number} wait Our timeout
+     * @param {boolean} immediate Do we want to apply the function immediately
+     * @return {function}
+     */
+    var debounce = function(func, wait, immediate) {
+        var timeout;
+        return function() {
+            var context = this;
+            var args = arguments;
+            var later = function() {
+                timeout = null;
+                if (!immediate) {
+                    func.apply(context, args);
+                }
+            };
+            var callNow = immediate && !timeout;
+            clearTimeout(timeout);
+            timeout = setTimeout(later, wait);
+            if (callNow) {
+                func.apply(context, args);
+            }
+        };
+    };
+    return {
+        init: init,
+    };
+});
diff --git a/admin/tool/moodlenet/amd/src/selectors.js b/admin/tool/moodlenet/amd/src/selectors.js
new file mode 100644 (file)
index 0000000..5feb0f3
--- /dev/null
@@ -0,0 +1,45 @@
+// 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/>.
+
+/**
+ * Define all of the selectors we will be using within MoodleNet plugin.
+ *
+ * @module     tool_moodlenet/selectors
+ * @package    tool_moodlenet
+ * @copyright  2020 Mathew May <mathew.solutions>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([], function() {
+    return {
+        action: {
+            browse: '[data-action="browse"]',
+            submit: '[data-action="submit"]',
+            showMoodleNet: '[data-action="show-moodlenet"]',
+            closeOption: '[data-action="close-chooser-option-summary"]',
+        },
+        region: {
+            clearIcon: '[data-region="clear-icon"]',
+            courses: '[data-region="mnet-courses"]',
+            instancePage: '[data-region="moodle-net"]',
+            searchInput: '[data-region="search-input"]',
+            searchIcon: '[data-region="search-icon"]',
+            selectPage: '[data-region="moodle-net-select"]',
+            spinner: '[data-region="spinner"]',
+            validationArea: '[data-region="validation-area"]',
+            carousel: '[data-region="carousel"]',
+            moodleNet: '[data-region="pluginCarousel"]',
+        },
+    };
+});
diff --git a/admin/tool/moodlenet/amd/src/validator.js b/admin/tool/moodlenet/amd/src/validator.js
new file mode 100644 (file)
index 0000000..704bd67
--- /dev/null
@@ -0,0 +1,59 @@
+// 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/>.
+
+/**
+ * Our validator that splits the user's input then fires off to a webservice
+ *
+ * @module     tool_moodlenet/validator
+ * @package    tool_moodlenet
+ * @copyright  2020 Mathew May <mathew.solutions>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/ajax', 'core/str', 'core/notification'], function($, Ajax, Str, Notification) {
+    /**
+     * Handle form validation
+     *
+     * @method validation
+     * @param {HTMLElement} inputElement The element the user entered text into.
+     * @return {Promise} Was the users' entry a valid profile URL?
+     */
+    var validation = function validation(inputElement) {
+        var inputValue = inputElement.value;
+
+        // They didn't submit anything or they gave us a simple string that we can't do anything with.
+        if (inputValue === "" || !inputValue.includes("@")) {
+            // Create a promise and immediately reject it.
+            $.when(Str.get_string('profilevalidationerror', 'tool_moodlenet')).then(function(strings) {
+                return Promise.reject().catch(function() {
+                    return {result: false, message: strings[0]};
+                });
+            }).fail(Notification.exception);
+        }
+
+        return Ajax.call([{
+            methodname: 'tool_moodlenet_verify_webfinger',
+            args: {
+                profileurl: inputValue,
+                course: inputElement.dataset.courseid,
+                section: inputElement.dataset.sectionid
+            }
+        }])[0].then(function(result) {
+            return result;
+        }).catch();
+    };
+    return {
+        validation: validation,
+    };
+});
diff --git a/admin/tool/moodlenet/classes/external.php b/admin/tool/moodlenet/classes/external.php
new file mode 100644 (file)
index 0000000..5fbfd20
--- /dev/null
@@ -0,0 +1,189 @@
+<?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/>.
+
+/**
+ * This is the external API for this component.
+ *
+ * @package    tool_moodlenet
+ * @copyright  2020 Mathew May {@link https://mathew.solutions}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_moodlenet;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir .'/externallib.php');
+require_once($CFG->libdir . '/filelib.php');
+require_once(__DIR__ . '/../lib.php');
+
+use core_course\external\course_summary_exporter;
+use external_api;
+use external_function_parameters;
+use external_value;
+use external_single_structure;
+
+/**
+ * This is the external API for this component.
+ *
+ * @copyright  2020 Mathew May {@link https://mathew.solutions}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class external extends external_api {
+
+    /**
+     * verify_webfinger parameters
+     *
+     * @return external_function_parameters
+     */
+    public static function verify_webfinger_parameters() {
+        return new external_function_parameters(
+            array(
+                'profileurl' => new external_value(PARAM_RAW, 'The profile url that the user has given us', VALUE_REQUIRED),
+                'course' => new external_value(PARAM_INT, 'The course we are adding to', VALUE_REQUIRED),
+                'section' => new external_value(PARAM_INT, 'The section within the course we are adding to', VALUE_REQUIRED),
+            )
+        );
+    }
+
+    /**
+     * Figure out if the passed content resolves with a WebFinger account.
+     *
+     * @param string $profileurl The profile url that the user states exists
+     * @param int $course The course we are adding to
+     * @param int $section The section within the course we are adding to
+     * @return array Contains the result and domain if any
+     * @throws \invalid_parameter_exception
+     */
+    public static function verify_webfinger(string $profileurl, int $course, int $section) {
+        global $USER;
+
+        $params = self::validate_parameters(self::verify_webfinger_parameters(), [
+                'profileurl' => $profileurl,
+                'section' => $section,
+                'course' => $course
+            ]
+        );
+        try {
+            $mnetprofile = new moodlenet_user_profile($params['profileurl'], $USER->id);
+        } catch (\Exception $e) {
+            return [
+                'result' => false,
+                'message' => get_string('profilevalidationfail', 'tool_moodlenet'),
+            ];
+        }
+
+        $userlink = profile_manager::get_moodlenet_profile_link($mnetprofile);
+
+        // There were no problems verifying the account so lets store it.
+        if ($userlink['result'] === true) {
+            profile_manager::save_moodlenet_user_profile($mnetprofile);
+            $userlink['domain'] = generate_mnet_endpoint($mnetprofile->get_profile_name(), $course, $section);
+        }
+
+        return $userlink;
+    }
+
+    /**
+     * verify_webfinger return.
+     *
+     * @return \external_description
+     */
+    public static function verify_webfinger_returns() {
+        return new external_single_structure([
+            'result' => new external_value(PARAM_BOOL, 'Was the passed content a valid WebFinger?'),
+            'message' => new external_value(PARAM_TEXT, 'Our message for the user'),
+            'domain' => new external_value(PARAM_RAW, 'Domain to redirect the user to', VALUE_OPTIONAL),
+        ]);
+    }
+
+    /**
+     * search_courses_parameters
+     *
+     * @return external_function_parameters
+     */
+    public static function search_courses_parameters() {
+        return new external_function_parameters(
+            array(
+                'searchvalue' => new external_value(PARAM_RAW, 'search value'),
+            )
+        );
+    }
+
+    /**
+     * For some given input find and return any course that matches it.
+     *
+     * @param string $searchvalue The profile url that the user states exists
+     * @return array Contains the result set of courses for the value
+     */
+    public static function search_courses(string $searchvalue) {
+        global $OUTPUT;
+
+        $params = self::validate_parameters(
+            self::search_courses_parameters(),
+            ['searchvalue' => $searchvalue]
+        );
+        self::validate_context(\context_system::instance());
+
+        $courses = array();
+
+        if ($arrcourses = \core_course_category::search_courses(array('search' => $params['searchvalue']))) {
+            foreach ($arrcourses as $course) {
+                if (has_capability('moodle/course:manageactivities', \context_course::instance($course->id))) {
+                    $data = new \stdClass();
+                    $data->id = $course->id;
+                    $data->fullname = $course->fullname;
+                    $data->hidden = $course->visible;
+                    $options = [
+                        'course' => $course->id,
+                    ];
+                    $viewurl = new \moodle_url('/admin/tool/moodlenet/options.php', $options);
+                    $data->viewurl = $viewurl->out(false);
+                    $category = \core_course_category::get($course->category);
+                    $data->coursecategory = $category->name;
+                    $courseimage = course_summary_exporter::get_course_image($data);
+                    if (!$courseimage) {
+                        $courseimage = $OUTPUT->get_generated_image_for_id($data->id);
+                    }
+                    $data->courseimage = $courseimage;
+                    $courses[] = $data;
+                }
+            }
+        }
+        return array(
+            'courses' => $courses
+        );
+    }
+
+    /**
+     * search_courses_returns.
+     *
+     * @return \external_description
+     */
+    public static function search_courses_returns() {
+        return new external_single_structure([
+            'courses' => new \external_multiple_structure(
+                new external_single_structure([
+                    'id' => new external_value(PARAM_INT, 'course id'),
+                    'fullname' => new external_value(PARAM_TEXT, 'course full name'),
+                    'hidden' => new external_value(PARAM_INT, 'is the course visible'),
+                    'viewurl' => new external_value(PARAM_URL, 'Next step of import'),
+                    'coursecategory' => new external_value(PARAM_TEXT, 'Category name'),
+                    'courseimage' => new external_value(PARAM_RAW, 'course image'),
+                ]))
+        ]);
+    }
+}
diff --git a/admin/tool/moodlenet/classes/local/import_backup_helper.php b/admin/tool/moodlenet/classes/local/import_backup_helper.php
new file mode 100644 (file)
index 0000000..a2f923a
--- /dev/null
@@ -0,0 +1,194 @@
+<?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 the import_backup_helper class.
+ *
+ * @package tool_moodlenet
+ * @copyright 2020 Adrian Greeve <adrian@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_moodlenet\local;
+
+/**
+ * The import_backup_helper class.
+ *
+ * The import_backup_helper objects provide a means to prepare a backup for for restoration of a course or activity backup file.
+ *
+ * @copyright 2020 Adrian Greeve <adrian@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class import_backup_helper {
+
+    /** @var remote_resource $remoteresource A file resource to be restored. */
+    protected $remoteresource;
+
+    /** @var user $user The user trying to restore a file. */
+    protected $user;
+
+    /** @var context $context The context we are trying to restore this file into. */
+    protected $context;
+
+    /** @var int $useruploadlimit The size limit that this user can upload in this context. */
+    protected $useruploadlimit;
+
+    /**
+     * Constructor for the import backup helper.
+     *
+     * @param remote_resource $remoteresource A remote file resource
+     * @param \stdClass       $user           The user importing a file.
+     * @param \context        $context        Context to restore into.
+     */
+    public function __construct(remote_resource $remoteresource, \stdClass $user, \context $context) {
+        $this->remoteresource = $remoteresource;
+        $this->user = $user;
+        $this->context = $context;
+
+        $maxbytes = 0;
+        if ($this->context->contextlevel == CONTEXT_COURSE) {
+            $course = get_course($this->context->instanceid);
+            $maxbytes = $course->maxbytes;
+        }
+        $this->useruploadlimit = get_user_max_upload_file_size($this->context, get_config('core', 'maxbytes'),
+                $maxbytes, 0, $this->user);
+    }
+
+    /**
+     * Return a stored user draft file for processing.
+     *
+     * @return \stored_file The imported file to ultimately be restored.
+     */
+    public function get_stored_file(): \stored_file {
+
+        // Check if the user can upload a backup to this context.
+        require_capability('moodle/restore:uploadfile', $this->context, $this->user->id);
+
+        // Before starting a potentially lengthy download, try to ensure the file size does not exceed the upload size restrictions
+        // for the user. This is a time saving measure.
+        // This is a naive check, that serves only to catch files if they provide the content length header.
+        // Because of potential content encoding (compression), the stored file will be checked again after download as well.
+        $size = $this->remoteresource->get_download_size() ?? -1;
+        if ($this->size_exceeds_upload_limit($size)) {
+            throw new \moodle_exception('uploadlimitexceeded', 'tool_moodlenet', '', ['filesize' => $size,
+                'uploadlimit' => $this->useruploadlimit]);
+        }
+
+        [$filepath, $filename] = $this->remoteresource->download_to_requestdir();
+        \core\antivirus\manager::scan_file($filepath, $filename, true);
+
+        // Check the final size of file against the user upload limits.
+        $localsize = filesize(sprintf('%s/%s', $filepath, $filename));
+        if ($this->size_exceeds_upload_limit($localsize)) {
+            throw new \moodle_exception('uploadlimitexceeded', 'tool_moodlenet', '', ['filesize' => $localsize,
+                'uploadlimit' => $this->useruploadlimit]);
+        }
+
+        return $this->create_user_draft_stored_file($filename, $filepath);
+    }
+
+    /**
+     * Does the size exceed the upload limit for the current import, taking into account user and core settings.
+     *
+     * @param int $sizeinbytes
+     * @return bool true if exceeded, false otherwise.
+     */
+    protected function size_exceeds_upload_limit(int $sizeinbytes): bool {
+        $maxbytes = 0;
+        if ($this->context->contextlevel == CONTEXT_COURSE) {
+            $course = get_course($this->context->instanceid);
+            $maxbytes = $course->maxbytes;
+        }
+        $maxbytes = get_user_max_upload_file_size($this->context, get_config('core', 'maxbytes'), $maxbytes, 0,
+            $this->user);
+        if ($maxbytes != USER_CAN_IGNORE_FILE_SIZE_LIMITS && $sizeinbytes > $maxbytes) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Create a file in the user drafts ready for use by plugins implementing dndupload_handle().
+     *
+     * @param string $filename the name of the file on disk
+     * @param string $path the path where the file is stored on disk
+     * @return \stored_file
+     */
+    protected function create_user_draft_stored_file(string $filename, string $path): \stored_file {
+        global $CFG;
+
+        $record = new \stdClass();
+        $record->filearea = 'draft';
+        $record->component = 'user';
+        $record->filepath = '/';
+        $record->itemid   = file_get_unused_draft_itemid();
+        $record->license  = $CFG->sitedefaultlicense;
+        $record->author   = '';
+        $record->filename = clean_param($filename, PARAM_FILE);
+        $record->contextid = \context_user::instance($this->user->id)->id;
+        $record->userid = $this->user->id;
+
+        $fullpathwithname = sprintf('%s/%s', $path, $filename);
+
+        $fs = get_file_storage();
+
+        return  $fs->create_file_from_pathname($record, $fullpathwithname);
+    }
+
+    /**
+     * Looks for a context that this user has permission to upload backup files to.
+     * This gets a list of roles that the user has, checks for the restore:uploadfile capability and then sends back a context
+     * that has this permission if available.
+     *
+     * This starts with the highest context level and moves down i.e. system -> category -> course.
+     *
+     * @param  int $userid The user ID that we are looking for a working context for.
+     * @return \context A context that allows the upload of backup files.
+     */
+    public static function get_context_for_user(int $userid): ?\context {
+        global $DB;
+
+        if (is_siteadmin()) {
+            return \context_system::instance();
+        }
+
+        $sql = "SELECT ctx.id, ctx.contextlevel, ctx.instanceid, ctx.path, ctx.depth, ctx.locked
+                  FROM {context} ctx
+                  JOIN {role_assignments} r ON ctx.id = r.contextid
+                 WHERE r.userid = :userid AND ctx.contextlevel IN (:contextsystem, :contextcategory, :contextcourse)
+              ORDER BY ctx.contextlevel ASC";
+
+        $params = [
+            'userid' => $userid,
+            'contextsystem' => CONTEXT_SYSTEM,
+            'contextcategory' => CONTEXT_COURSECAT,
+            'contextcourse' => CONTEXT_COURSE
+        ];
+        $records = $DB->get_records_sql($sql, $params);
+        foreach ($records as $record) {
+            \context_helper::preload_from_record($record);
+            if ($record->contextlevel == CONTEXT_COURSECAT) {
+                $context = \context_coursecat::instance($record->instanceid);
+            } else if ($record->contextlevel == CONTEXT_COURSE) {
+                $context = \context_course::instance($record->instanceid);
+            } else {
+                $context = \context_system::instance();
+            }
+            if (has_capability('moodle/restore:uploadfile', $context, $userid)) {
+                return $context;
+            }
+        }
+        return null;
+    }
+}
diff --git a/admin/tool/moodlenet/classes/local/import_handler_info.php b/admin/tool/moodlenet/classes/local/import_handler_info.php
new file mode 100644 (file)
index 0000000..8517f37
--- /dev/null
@@ -0,0 +1,91 @@
+<?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 the import_handler_info class.
+ *
+ * @package tool_moodlenet
+ * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_moodlenet\local;
+
+/**
+ * The import_handler_info class.
+ *
+ * An import_handler_info object represent an resource import handler for a particular module.
+ *
+ * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class import_handler_info {
+
+    /** @var string $modulename the name of the module. */
+    protected $modulename;
+
+    /** @var string $description the description. */
+    protected $description;
+
+    /** @var import_strategy $importstrategy the strategy which will be used to import resources handled by this handler */
+    protected $importstrategy;
+
+    /**
+     * The import_handler_info constructor.
+     *
+     * @param string $modulename the name of the module handling the file extension. E.g. 'label'.
+     * @param string $description A description of how the module handles files of this extension type.
+     * @param import_strategy $strategy the strategy which will be used to import the resource.
+     * @throws \coding_exception
+     */
+    public function __construct(string $modulename, string $description, import_strategy $strategy) {
+        if (empty($modulename)) {
+            throw new \coding_exception("Module name cannot be empty.");
+        }
+        if (empty($description)) {
+            throw new \coding_exception("Description cannot be empty.");
+        }
+        $this->modulename = $modulename;
+        $this->description = $description;
+        $this->importstrategy = $strategy;
+    }
+
+    /**
+     * Get the name of the module.
+     *
+     * @return string the module name, e.g. 'label'.
+     */
+    public function get_module_name(): string {
+        return $this->modulename;
+    }
+
+    /**
+     * Get a human readable, localised description of how the file is handled by the module.
+     *
+     * @return string the localised description.
+     */
+    public function get_description(): string {
+        return $this->description;
+    }
+
+    /**
+     * Get the import strategy used by this handler.
+     *
+     * @return import_strategy the import strategy object.
+     */
+    public function get_strategy(): import_strategy {
+        return $this->importstrategy;
+    }
+}
diff --git a/admin/tool/moodlenet/classes/local/import_handler_registry.php b/admin/tool/moodlenet/classes/local/import_handler_registry.php
new file mode 100644 (file)
index 0000000..b5860b5
--- /dev/null
@@ -0,0 +1,188 @@
+<?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 the import_handler_registry class.
+ *
+ * @package tool_moodlenet
+ * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_moodlenet\local;
+
+/**
+ * The import_handler_registry class.
+ *
+ * The import_handler_registry objects represent a register of modules handling various file extensions for a given course and user.
+ * Only modules which are available to the user in the course are included in the register for that user.
+ *
+ * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class import_handler_registry {
+
+    /**
+     * @var array array containing the names and messages of all modules handling import of resources as a 'file' type.
+     */
+    protected $filehandlers = [];
+
+    /**
+     * @var array $typehandlers the array of modules registering as handlers of other, non-file types, indexed by typename.
+     */
+    protected $typehandlers = [];
+
+    /**
+     * @var array $registry the aggregate of all registrations made by plugins, indexed by 'file' and 'type'.
+     */
+    protected $registry = [];
+
+    /**
+     * @var \context_course the course context object.
+     */
+    protected $context;
+
+    /**
+     * @var \stdClass a course object.
+     */
+    protected $course;
+
+    /**
+     * @var \stdClass a user object.
+     */
+    protected $user;
+
+    /**
+     * The import_handler_registry constructor.
+     *
+     * @param \stdClass $course the course, which impacts available handlers.
+     * @param \stdClass $user the user, which impacts available handlers.
+     */
+    public function __construct(\stdClass $course, \stdClass $user) {
+        $this->course = $course;
+        $this->user = $user;
+        $this->context = \context_course::instance($course->id);
+
+        // Generate the full list of handlers for all extensions for this user and course.
+        $this->populate_handlers();
+    }
+
+    /**
+     * Get all handlers for the remote resource, depending on the strategy being used to import the resource.
+     *
+     * @param remote_resource $resource the remote resource.
+     * @param import_strategy $strategy an import_strategy instance.
+     * @return import_handler_info[] the array of import_handler_info handlers.
+     */
+    public function get_resource_handlers_for_strategy(remote_resource $resource, import_strategy $strategy): array {
+        return $strategy->get_handlers($this->registry, $resource);
+    }
+
+    /**
+     * Get a specific handler for the resource, belonging to a specific module and for a specific strategy.
+     *
+     * @param remote_resource $resource the remote resource.
+     * @param string $modname the name of the module, e.g. 'label'.
+     * @param import_strategy $strategy a string representing how to treat the resource. e.g. 'file', 'link'.
+     * @return import_handler_info|null the import_handler_info object, if found, otherwise null.
+     */
+    public function get_resource_handler_for_mod_and_strategy(remote_resource $resource, string $modname,
+            import_strategy $strategy): ?import_handler_info {
+        foreach ($strategy->get_handlers($this->registry, $resource) as $handler) {
+            if ($handler->get_module_name() === $modname) {
+                return $handler;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Build up a list of extension handlers by leveraging the dndupload_register callbacks.
+     */
+    protected function populate_handlers() {
+        // Generate a dndupload_handler object, just so we can call ->is_known_type() on the types being registered by plugins.
+        // We must vet each type which is reported to be handled against the list of known, supported types.
+        global $CFG;
+        require_once($CFG->dirroot . '/course/dnduploadlib.php');
+        $dndhandlers = new \dndupload_handler($this->course);
+
+        // Get the list of mods enabled at site level first. We need to cross check this.
+        $pluginman = \core_plugin_manager::instance();
+        $sitemods = $pluginman->get_plugins_of_type('mod');
+        $sitedisabledmods = array_filter($sitemods, function(\core\plugininfo\mod $modplugininfo){
+            return !$modplugininfo->is_enabled();
+        });
+        $sitedisabledmods = array_map(function($modplugininfo) {
+            return $modplugininfo->name;
+        }, $sitedisabledmods);
+
+        // Loop through all modules to find the registered handlers.
+        $mods = get_plugin_list_with_function('mod', 'dndupload_register');
+        foreach ($mods as $component => $funcname) {
+            list($modtype, $modname) = \core_component::normalize_component($component);
+            if (!empty($sitedisabledmods) && array_key_exists($modname, $sitedisabledmods)) {
+                continue; // Module is disabled at the site level.
+            }
+            if (!course_allowed_module($this->course, $modname, $this->user)) {
+                continue; // User does not have permission to add this module to the course.
+            }
+
+            if (!$resp = component_callback($component, 'dndupload_register')) {
+                continue;
+            };
+
+            if (isset($resp['files'])) {
+                foreach ($resp['files'] as $file) {
+                    $this->register_file_handler($file['extension'], $modname, $file['message']);
+                }
+            }
+            if (isset($resp['types'])) {
+                foreach ($resp['types'] as $type) {
+                    if (!$dndhandlers->is_known_type($type['identifier'])) {
+                        throw new \coding_exception("Trying to add handler for unknown type $type");
+                    }
+                    $this->register_type_handler($type['identifier'], $modname, $type['message']);
+                }
+            }
+        }
+        $this->registry = [
+            'files' => $this->filehandlers,
+            'types' => $this->typehandlers
+        ];
+    }
+
+    /**
+     * Adds a type handler to the list.
+     *
+     * @param string $identifier the name of the type.
+     * @param string $module the name of the module, e.g. 'label'.
+     * @param string $message the message describing how the module handles the type.
+     */
+    protected function register_type_handler(string $identifier, string $module,  string $message) {
+        $this->typehandlers[$identifier][] = ['module' => $module, 'message' => $message];
+    }
+
+    /**
+     * Adds a file extension handler to the list.
+     *
+     * @param string $extension the extension, e.g. 'png'.
+     * @param string $module the name of the module handling this extension
+     * @param string $message the message describing how the module handles the extension.
+     */
+    protected function register_file_handler(string $extension, string $module, string $message) {
+        $extension = strtolower($extension);
+        $this->filehandlers[$extension][] = ['module' => $module, 'message' => $message];
+    }
+}
+
diff --git a/admin/tool/moodlenet/classes/local/import_info.php b/admin/tool/moodlenet/classes/local/import_info.php
new file mode 100644 (file)
index 0000000..818f4c5
--- /dev/null
@@ -0,0 +1,126 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+/**
+ * Contains the import_info class.
+ *
+ * @package tool_moodlenet
+ * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_moodlenet\local;
+
+/**
+ * Class import_info, describing objects which represent a resource being imported by a user.
+ *
+ * Objects of this class encapsulate both:
+ * - information about the resource (remote_resource).
+ * - config data pertaining to the import process, such as the destination course and section
+ *   and how the resource should be treated (i.e. the type and the name of the module selected as the import handler)
+ *
+ * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class import_info {
+
+    /** @var int $userid the user conducting this import. */
+    protected $userid;
+
+    /** @var remote_resource $resource the resource being imported. */
+    protected $resource;
+
+    /** @var \stdClass $config config data pertaining to the import process, e.g. course, section, type. */
+    protected $config;
+
+    /** @var string $id string identifier for this object. */
+    protected $id;
+
+    /**
+     * The import_controller constructor.
+     *
+     * @param int $userid the id of the user performing the import.
+     * @param remote_resource $resource the resource being imported.
+     * @param \stdClass $config import config like 'course', 'section', 'type'.
+     */
+    public function __construct(int $userid, remote_resource $resource, \stdClass $config) {
+        $this->userid = $userid;
+        $this->resource = $resource;
+        $this->config = $config;
+        $this->id = md5($resource->get_url()->get_value());
+    }
+
+    /**
+     * Get the id of this object.
+     */
+    public function get_id() {
+        return $this->id;
+    }
+
+    /**
+     * Get the remote resource being imported.
+     *
+     * @return remote_resource the remote resource being imported.
+     */
+    public function get_resource(): remote_resource {
+        return $this->resource;
+    }
+
+    /**
+     * Get the configuration data pertaining to the import.
+     *
+     * @return \stdClass the import configuration data.
+     */
+    public function get_config(): \stdClass {
+        return $this->config;
+    }
+
+    /**
+     * Set the configuration data pertaining to the import.
+     *
+     * @param \stdClass $config the configuration data to set.
+     */
+    public function set_config(\stdClass $config): void {
+        $this->config  = $config;
+    }
+
+    /**
+     * Get an import_info object by id.
+     *
+     * @param string $id the id of the import_info object to load.
+     * @return mixed an import_info object if found, otherwise null.
+     */
+    public static function load(string $id): ?import_info {
+        // This currently lives in the session, so we don't need userid.
+        // It might be useful if we ever move to another storage mechanism however, where we would need it.
+        global $SESSION;
+        return isset($SESSION->moodlenetimports[$id]) ? unserialize($SESSION->moodlenetimports[$id]) : null;
+    }
+
+    /**
+     * Save this object to a store which is accessible across requests.
+     */
+    public function save(): void {
+        global $SESSION;
+        $SESSION->moodlenetimports[$this->id] = serialize($this);
+    }
+
+    /**
+     * Remove all information about an import from the store.
+     */
+    public function purge(): void {
+        global $SESSION;
+        unset($SESSION->moodlenetimports[$this->id]);
+    }
+}
diff --git a/admin/tool/moodlenet/classes/local/import_processor.php b/admin/tool/moodlenet/classes/local/import_processor.php
new file mode 100644 (file)
index 0000000..8afd4d3
--- /dev/null
@@ -0,0 +1,206 @@
+<?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 the import_processor class.
+ *
+ * @package tool_moodlenet
+ * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_moodlenet\local;
+
+/**
+ * The import_processor class.
+ *
+ * The import_processor objects provide a means to import a remote resource into a course section, delegating the handling of
+ * content to the relevant module, via its dndupload_handler callback.
+ *
+ * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class import_processor {
+
+    /** @var object The course that we are uploading to */
+    protected $course = null;
+
+    /** @var int The section number we are uploading to */
+    protected $section = null;
+
+    /** @var import_handler_registry $handlerregistry registry object to use for cross checking the supplied handler.*/
+    protected $handlerregistry;
+
+    /** @var import_handler_info $handlerinfo information about the module handling the import.*/
+    protected $handlerinfo;
+
+    /** @var \stdClass $user the user conducting the import.*/
+    protected $user;
+
+    /** @var remote_resource $remoteresource the remote resource being imported.*/
+    protected $remoteresource;
+
+    /** @var string[] $descriptionoverrides list of modules which support having their descriptions updated, post-import. */
+    protected $descriptionoverrides = ['folder', 'page', 'resource', 'scorm', 'url'];
+
+    /**
+     * The import_processor constructor.
+     *
+     * @param \stdClass $course the course object.
+     * @param int $section the section number in the course, starting at 0.
+     * @param remote_resource $remoteresource the remote resource to import.
+     * @param import_handler_info $handlerinfo information about which module is handling the import.
+     * @param import_handler_registry $handlerregistry A registry of import handlers, to use for validation.
+     * @throws \coding_exception If any of the params are invalid.
+     */
+    public function __construct(\stdClass $course, int $section, remote_resource $remoteresource, import_handler_info $handlerinfo,
+            import_handler_registry $handlerregistry) {
+
+        global $DB, $USER;
+
+        if ($section < 0) {
+            throw new \coding_exception("Invalid section number $section. Must be > 0.");
+        }
+        if (!$DB->record_exists('modules', array('name' => $handlerinfo->get_module_name()))) {
+            throw new \coding_exception("Module {$handlerinfo->get_module_name()} does not exist");
+        }
+
+        $this->course = $course;
+        $this->section = $section;
+        $this->handlerregistry = $handlerregistry;
+        $this->user = $USER;
+        $this->remoteresource = $remoteresource;
+        $this->handlerinfo = $handlerinfo;
+
+        // ALL handlers must have a strategy and ANY strategy can process ANY resource.
+        // It is therefore NOT POSSIBLE to have a resource that CANNOT be processed by a handler.
+        // So, there's no need to verify that the remote_resource CAN be handled by the handler. It always can.
+    }
+
+    /**
+     * Run the import process, including file download, module creation and cleanup (cache purge, etc).
+     */
+    public function process(): void {
+        // Allow the strategy to do setup for this file import.
+        $moduledata = $this->handlerinfo->get_strategy()->import($this->remoteresource, $this->user, $this->course, $this->section);
+
+        // Create the course module, and add that information to the data to be sent to the plugin handling the resource.
+        $cmdata = $this->create_course_module($this->course, $this->section, $this->handlerinfo->get_module_name());
+        $moduledata->coursemodule = $cmdata->id;
+
+        // Now, send the data to the handling plugin to let it set up.
+        $instanceid = plugin_callback('mod', $this->handlerinfo->get_module_name(), 'dndupload', 'handle', [$moduledata],
+            'invalidfunction');
+        if ($instanceid == 'invalidfunction') {
+            $name = $this->handlerinfo->get_module_name();
+            throw new \coding_exception("$name does not support drag and drop upload (missing {$name}_dndupload_handle function)");
+        }
+
+        // Now, update the module description if the module supports it and only if it's not currently set.
+        $this->update_module_description($instanceid);
+
+        // Finish setting up the course module.
+        $this->finish_setup_course_module($instanceid, $cmdata->id);
+    }
+
+    /**
+     * Update the module's description (intro), if that feature is supported.
+     *
+     * @param int $instanceid the instance id of the module to update.
+     */
+    protected function update_module_description(int $instanceid): void {
+        global $DB, $CFG;
+        require_once($CFG->libdir . '/moodlelib.php');
+
+        if (plugin_supports('mod', $this->handlerinfo->get_module_name(), FEATURE_MOD_INTRO, true)) {
+            require_once($CFG->libdir . '/editorlib.php');
+            require_once($CFG->libdir . '/modinfolib.php');
+
+            $rec = $DB->get_record($this->handlerinfo->get_module_name(), ['id' => $instanceid]);
+
+            if (empty($rec->intro) || in_array($this->handlerinfo->get_module_name(), $this->descriptionoverrides)) {
+                $updatedata = (object)[
+                    'id' => $instanceid,
+                    'intro' => clean_param($this->remoteresource->get_description(), PARAM_TEXT),
+                    'introformat' => editors_get_preferred_format()
+                ];
+
+                $DB->update_record($this->handlerinfo->get_module_name(), $updatedata);
+
+                rebuild_course_cache($this->course->id, true);
+            }
+        }
+    }
+
+    /**
+     * Create the course module to hold the file/content that has been uploaded.
+     * @param \stdClass $course the course object.
+     * @param int $section the section.
+     * @param string $modname the name of the module, e.g. 'label'.
+     * @return \stdClass the course module data.
+     */
+    protected function create_course_module(\stdClass $course, int $section, string $modname): \stdClass {
+        global $CFG;
+        require_once($CFG->dirroot . '/course/modlib.php');
+        list($module, $context, $cw, $cm, $data) = prepare_new_moduleinfo_data($course, $modname, $section);
+        $data->visible = false; // The module is created in a hidden state.
+        $data->coursemodule = $data->id = add_course_module($data);
+        return $data;
+    }
+
+    /**
+     * Finish off any course module setup, such as adding to the course section and firing events.
+     *
+     * @param int $instanceid id returned by the mod when it was created.
+     * @param int $cmid the course module record id, for removal if something went wrong.
+     */
+    protected function finish_setup_course_module($instanceid, int $cmid): void {
+        global $DB;
+
+        if (!$instanceid) {
+            // Something has gone wrong - undo everything we can.
+            course_delete_module($cmid);
+            throw new \moodle_exception('errorcreatingactivity', 'moodle', '', $this->handlerinfo->get_module_name());
+        }
+
+        // Note the section visibility.
+        $visible = get_fast_modinfo($this->course)->get_section_info($this->section)->visible;
+
+        $DB->set_field('course_modules', 'instance', $instanceid, array('id' => $cmid));
+
+        // Rebuild the course cache after update action.
+        rebuild_course_cache($this->course->id, true);
+
+        course_add_cm_to_section($this->course, $cmid, $this->section);
+
+        set_coursemodule_visible($cmid, $visible);
+        if (!$visible) {
+            $DB->set_field('course_modules', 'visibleold', 1, array('id' => $cmid));
+        }
+
+        // Retrieve the final info about this module.
+        $info = get_fast_modinfo($this->course, $this->user->id);
+        if (!isset($info->cms[$cmid])) {
+            // The course module has not been properly created in the course - undo everything.
+            course_delete_module($cmid);
+            throw new \moodle_exception('errorcreatingactivity', 'moodle', '', $this->handlerinfo->get_module_name());
+        }
+        $mod = $info->get_cm($cmid);
+
+        // Trigger course module created event.
+        $event = \core\event\course_module_created::create_from_cm($mod);
+        $event->trigger();
+    }
+}
+
diff --git a/admin/tool/moodlenet/classes/local/import_strategy.php b/admin/tool/moodlenet/classes/local/import_strategy.php
new file mode 100644 (file)
index 0000000..d647e26
--- /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/>.
+/**
+ * Contains the import_strategy interface.
+ *
+ * @package tool_moodlenet
+ * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_moodlenet\local;
+
+/**
+ * The import_strategy interface.
+ *
+ * This provides a contract allowing different import strategies to be implemented.
+ *
+ * An import_strategy encapsulates the logic used to prepare a remote_resource for import into Moodle in some way and is used by the
+ * import_processor (to perform aforementioned preparations) before it hands control of the import over to a course module plugin.
+ *
+ * We may wish to have many strategies because the preparation steps may vary depending on how the resource is to be treated.
+ * E.g. We may wish to import as a file in which case download steps will be required, or we may simply wish to import the remote
+ * resource as a link, in which cases setup steps will not require any file download.
+ *
+ * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface import_strategy {
+
+    /**
+     * Get an array of import_handler_info objects supported by this import strategy, based on the registrydata and resource.
+     *
+     * Implementations should check the registry data for any entries which align with their import strategy and should create
+     * import_handler_info objects to represent each relevant entry. If an entry represents a module, or handling type which does
+     * not align with the strategy, that item should simply be skipped.
+     *
+     * E.g. If one strategy aims to import all remote resources as files (e.g. import_strategy_file), it would only generate a list
+     * of import_handler_info objects created from those registry entries of type 'file', as those entries represent the modules
+     * which have said they can handle resources as files.
+     *
+     * @param array $registrydata The fully populated handler registry.
+     * @param remote_resource $resource the remote resource.
+     * @return import_handler_info[] the array of import_handler_info objects, or an empty array if none were matched.
+     */
+    public function get_handlers(array $registrydata, remote_resource $resource): array;
+
+    /**
+     * Called during import to perform required import setup steps.
+     *
+     * @param remote_resource $resource the resource to import.
+     * @param \stdClass $user the user to import on behalf of.
+     * @param \stdClass $course the course into which the remote resource is being imported.
+     * @param int $section the section into which the remote resource is being imported.
+     * @return \stdClass the module data which will be passed on to the course module plugin.
+     */
+    public function import(remote_resource $resource, \stdClass $user, \stdClass $course, int $section): \stdClass;
+}
diff --git a/admin/tool/moodlenet/classes/local/import_strategy_file.php b/admin/tool/moodlenet/classes/local/import_strategy_file.php
new file mode 100644 (file)
index 0000000..e34092a
--- /dev/null
@@ -0,0 +1,170 @@
+<?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 the import_strategy_file class.
+ *
+ * @package tool_moodlenet
+ * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_moodlenet\local;
+
+use core\antivirus\manager as avmanager;
+
+/**
+ * The import_strategy_file class.
+ *
+ * The import_strategy_file objects contains the setup steps needed to prepare a resource for import as a file into Moodle. This
+ * ensures the remote_resource is first downloaded and put in a draft file area, ready for use as a file by the handling module.
+ *
+ * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class import_strategy_file implements import_strategy {
+
+    /**
+     * Get an array of import_handler_info objects representing modules supporting import of this file type.
+     *
+     * @param array $registrydata the fully populated registry.
+     * @param remote_resource $resource the remote resource.
+     * @return import_handler_info[] the array of import_handler_info objects.
+     */
+    public function get_handlers(array $registrydata, remote_resource $resource): array {
+        $handlers = [];
+        foreach ($registrydata['files'] as $index => $items) {
+            foreach ($items as $item) {
+                if ($index === $resource->get_extension() || $index === '*') {
+                    $handlers[] = new import_handler_info($item['module'], $item['message'], $this);
+                }
+            }
+        }
+        return $handlers;
+    }
+
+    /**
+     * Import the remote resource according to the rules of this strategy.
+     *
+     * @param remote_resource $resource the resource to import.
+     * @param \stdClass $user the user to import on behalf of.
+     * @param \stdClass $course the course into which the remote_resource is being imported.
+     * @param int $section the section into which the remote_resource is being imported.
+     * @return \stdClass the module data.
+     * @throws \moodle_exception if the file size means the upload limit is exceeded for the user.
+     */
+    public function import(remote_resource $resource, \stdClass $user, \stdClass $course, int $section): \stdClass {
+        // Before starting a potentially lengthy download, try to ensure the file size does not exceed the upload size restrictions
+        // for the user. This is a time saving measure.
+        // This is a naive check, that serves only to catch files if they provide the content length header.
+        // Because of potential content encoding (compression), the stored file will be checked again after download as well.
+        $size = $resource->get_download_size() ?? -1;
+        $useruploadlimit = $this->get_user_upload_limit($user, $course);
+        if ($this->size_exceeds_upload_limit($size, $useruploadlimit)) {
+            throw new \moodle_exception('uploadlimitexceeded', 'tool_moodlenet', '', ['filesize' => $size,
+                'uploadlimit' => $useruploadlimit]);
+        }
+
+        // Download the file into a request directory and scan it.
+        [$filepath, $filename] = $resource->download_to_requestdir();
+        avmanager::scan_file($filepath, $filename, true);
+
+        // Check the final size of file against the user upload limits.
+        $localsize = filesize(sprintf('%s/%s', $filepath, $filename));
+        if ($this->size_exceeds_upload_limit($localsize, $useruploadlimit)) {
+            throw new \moodle_exception('uploadlimitexceeded', 'tool_moodlenet', '', ['filesize' => $localsize,
+                'uploadlimit' => $useruploadlimit]);
+        }
+
+        // Store in the user draft file area.
+        $storedfile = $this->create_user_draft_stored_file($user, $filename, $filepath);
+
+        // Prepare the data to be sent to the modules dndupload_handle hook.
+        return $this->prepare_module_data($course, $resource, $storedfile->get_itemid());
+    }
+
+
+    /**
+     * Creates the data to pass to the dndupload_handle() hooks.
+     *
+     * @param \stdClass $course the course record.
+     * @param remote_resource $resource the resource being imported as a file.
+     * @param int $draftitemid the itemid of the draft file.
+     * @return \stdClass the data object.
+     */
+    protected function prepare_module_data(\stdClass $course, remote_resource $resource, int $draftitemid): \stdClass {
+        $data = new \stdClass();
+        $data->type = 'Files';
+        $data->course = $course;
+        $data->draftitemid = $draftitemid;
+        $data->displayname = $resource->get_name();
+        return $data;
+    }
+
+    /**
+     * Get the max file size limit for the user in the course.
+     *
+     * @param \stdClass $user the user to check.
+     * @param \stdClass $course the course to check in.
+     * @return int the file size limit, in bytes.
+     */
+    protected function get_user_upload_limit(\stdClass $user, \stdClass $course): int {
+        return get_user_max_upload_file_size(\context_course::instance($course->id), get_config('core', 'maxbytes'),
+            $course->maxbytes, 0, $user);
+    }
+
+    /**
+     * Does the size exceed the upload limit for the current import, taking into account user and core settings.
+     *
+     * @param int $sizeinbytes the size, in bytes.
+     * @param int $useruploadlimit the upload limit, in bytes.
+     * @return bool true if exceeded, false otherwise.
+     * @throws \dml_exception
+     */
+    protected function size_exceeds_upload_limit(int $sizeinbytes, int $useruploadlimit): bool {
+        if ($useruploadlimit != USER_CAN_IGNORE_FILE_SIZE_LIMITS && $sizeinbytes > $useruploadlimit) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Create a file in the user drafts ready for use by plugins implementing dndupload_handle().
+     *
+     * @param \stdClass $user the user object.
+     * @param string $filename the name of the file on disk
+     * @param string $path the path where the file is stored on disk
+     * @return \stored_file
+     */
+    protected function create_user_draft_stored_file(\stdClass $user, string $filename, string $path): \stored_file {
+        global $CFG;
+
+        $record = new \stdClass();
+        $record->filearea = 'draft';
+        $record->component = 'user';
+        $record->filepath = '/';
+        $record->itemid   = file_get_unused_draft_itemid();
+        $record->license  = $CFG->sitedefaultlicense;
+        $record->author   = '';
+        $record->filename = clean_param($filename, PARAM_FILE);
+        $record->contextid = \context_user::instance($user->id)->id;
+        $record->userid = $user->id;
+
+        $fullpathwithname = sprintf('%s/%s', $path, $filename);
+
+        $fs = get_file_storage();
+
+        return  $fs->create_file_from_pathname($record, $fullpathwithname);
+    }
+}
diff --git a/admin/tool/moodlenet/classes/local/import_strategy_link.php b/admin/tool/moodlenet/classes/local/import_strategy_link.php
new file mode 100644 (file)
index 0000000..ca04f06
--- /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 the import_strategy_link class.
+ *
+ * @package tool_moodlenet
+ * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_moodlenet\local;
+
+/**
+ * The import_strategy_link class.
+ *
+ * The import_strategy_link objects contains the setup steps needed to prepare a resource for import as a URL into Moodle.
+ *
+ * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class import_strategy_link implements import_strategy {
+
+    /**
+     * Get an array of import_handler_info objects representing modules supporting import of the resource.
+     *
+     * @param array $registrydata the fully populated registry.
+     * @param remote_resource $resource the remote resource.
+     * @return import_handler_info[] the array of import_handler_info objects.
+     */
+    public function get_handlers(array $registrydata, remote_resource $resource): array {
+        $handlers = [];
+        foreach ($registrydata['types'] as $identifier => $items) {
+            foreach ($items as $item) {
+                if ($identifier === 'url') {
+                    $handlers[] = new import_handler_info($item['module'], $item['message'], $this);
+                }
+            }
+        }
+        return $handlers;
+    }
+
+    /**
+     * Import the remote resource according to the rules of this strategy.
+     *
+     * @param remote_resource $resource the resource to import.
+     * @param \stdClass $user the user to import on behalf of.
+     * @param \stdClass $course the course into which the remote_resource is being imported.
+     * @param int $section the section into which the remote_resource is being imported.
+     * @return \stdClass the module data.
+     */
+    public function import(remote_resource $resource, \stdClass $user, \stdClass $course, int $section): \stdClass {
+        $data = new \stdClass();
+        $data->type = 'url';
+        $data->course = $course;
+        $data->content = $resource->get_url()->get_value();
+        $data->displayname = $resource->get_name();
+        return $data;
+    }
+}
diff --git a/admin/tool/moodlenet/classes/local/remote_resource.php b/admin/tool/moodlenet/classes/local/remote_resource.php
new file mode 100644 (file)
index 0000000..e952618
--- /dev/null
@@ -0,0 +1,169 @@
+<?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 the remote_resource class definition.
+ *
+ * @package tool_moodlenet
+ * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_moodlenet\local;
+
+/**
+ * The remote_resource class.
+ *
+ * Objects of type remote_resource provide a means of interacting with resources over HTTP.
+ *
+ * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class remote_resource {
+
+    /** @var \curl $curl the curl http helper.*/
+    protected $curl;
+
+    /** @var url $url the url to the remote resource.*/
+    protected $url;
+
+    /** @var string $filename the name of this remote file.*/
+    protected $filename;
+
+    /** @var string $extension the file extension of this remote file.*/
+    protected $extension;
+
+    /** @var array $headinfo the array of information for the most recent HEAD request.*/
+    protected $headinfo = [];
+
+    /** @var \stdClass $metadata information about the resource. */
+    protected $metadata;
+
+    /**
+     * The remote_resource constructor.
+     *
+     * @param \curl $curl a curl object for HTTP requests.
+     * @param url $url the URL of the remote resource.
+     * @param \stdClass $metadata resource metadata such as name, summary, license, etc.
+     */
+    public function __construct(\curl $curl, url $url, \stdClass $metadata) {
+        $this->curl = $curl;
+        $this->url = $url;
+        $this->filename = pathinfo($this->url->get_path(), PATHINFO_FILENAME);
+        $this->extension = pathinfo($this->url->get_path(), PATHINFO_EXTENSION);
+        $this->metadata = $metadata;
+    }
+
+    /**
+     * Return the URL for this remote resource.
+     *
+     * @return url the url object.
+     */
+    public function get_url(): url {
+        return $this->url;
+    }
+
+    /**
+     * Get the name of the file as taken from the metadata.
+     */
+    public function get_name(): string {
+        return $this->metadata->name ?? '';
+    }
+
+    /**
+     * Get the resource metadata.
+     *
+     * @return \stdClass the metadata.
+     */
+    public function get_metadata(): \stdClass {
+        return$this->metadata;
+    }
+
+    /**
+     * Get the description of the resource as taken from the metadata.
+     *
+     * @return string
+     */
+    public function get_description(): string {
+        return $this->metadata->description ?? '';
+    }
+
+    /**
+     * Return the extension of the file, if found.
+     *
+     * @return string the extension of the file, if found.
+     */
+    public function get_extension(): string {
+        return $this->extension;
+    }
+
+    /**
+     * Returns the file size of the remote file, in bytes, or null if it cannot be determined.
+     *
+     * @return int|null the content length, if able to be determined, otherwise null.
+     */
+    public function get_download_size(): ?int {
+        $this->get_resource_info();
+        return $this->headinfo['download_content_length'] ?? null;
+    }
+
+    /**
+     * Download the remote resource to a local requestdir, returning the path and name of the resulting file.
+     *
+     * @return array an array containing filepath adn filename, e.g. [filepath, filename].
+     * @throws \moodle_exception if the file cannot be downloaded.
+     */
+    public function download_to_requestdir(): array {
+        $filename = sprintf('%s.%s', $this->filename, $this->get_extension());
+        $path = make_request_directory();
+        $fullpathwithname = sprintf('%s/%s', $path, $filename);
+
+        // In future, use a timeout (download and/or connection) controlled by a tool_moodlenet setting.
+        $downloadtimeout = 30;
+
+        $result = $this->curl->download_one($this->url->get_value(), null, ['filepath' => $fullpathwithname,
+            'timeout' => $downloadtimeout]);
+        if ($result !== true) {
+            throw new \moodle_exception('errorduringdownload', 'tool_moodlenet', '', $result);
+        }
+
+        return [$path, $filename];
+    }
+
+    /**
+     * Fetches information about the remote resource via a HEAD request.
+     *
+     * @throws \coding_exception if any connection problems occur.
+     */
+    protected function get_resource_info() {
+        if (!empty($this->headinfo)) {
+            return;
+        }
+        $options['CURLOPT_RETURNTRANSFER'] = 1;
+        $options['CURLOPT_FOLLOWLOCATION'] = 1;
+        $options['CURLOPT_MAXREDIRS'] = 5;
+        $options['CURLOPT_FAILONERROR'] = 1; // We want to consider http error codes as errors to report, not just status codes.
+
+        $this->curl->head($this->url->get_value(), $options);
+        $errorno = $this->curl->get_errno();
+        $this->curl->resetopt();
+
+        if ($errorno !== 0) {
+            $message = 'Problem during HEAD request for remote resource \''.$this->url->get_value().'\'. Curl Errno: ' . $errorno;
+            throw new \coding_exception($message);
+        }
+        $this->headinfo = $this->curl->get_info();
+    }
+
+}
diff --git a/admin/tool/moodlenet/classes/local/url.php b/admin/tool/moodlenet/classes/local/url.php
new file mode 100644 (file)
index 0000000..233b770
--- /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/>.
+/**
+ * Contains the url class, providing a representation of a url and operations on its component parts.
+ *
+ * @package tool_moodlenet
+ * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_moodlenet\local;
+
+/**
+ * The url class, providing a representation of a url and operations on its component parts.
+ *
+ * @copyright 2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class url {
+
+    /** @var string $url the full URL string.*/
+    protected $url;
+
+    /** @var string|null $path the path component of this URL.*/
+    protected $path;
+
+    /** @var host|null $host the host component of this URL.*/
+    protected $host;
+
+    /**
+     * The url constructor.
+     *
+     * @param string $url the URL string.
+     * @throws \coding_exception if the URL does not pass syntax validation.
+     */
+    public function __construct(string $url) {
+        // This object supports URLs as per the spec, so non-ascii chars must be encoded as per IDNA rules.
+        if (!filter_var($url, FILTER_VALIDATE_URL)) {
+            throw new \coding_exception('Malformed URL');
+        }
+        $this->url = $url;
+        $this->path = parse_url($url, PHP_URL_PATH);
+        $this->host = parse_url($url, PHP_URL_HOST);
+    }
+
+    /**
+     * Get the path component of the URL.
+     *
+     * @return string|null the path component of the URL.
+     */
+    public function get_path(): ?string {
+        return $this->path;
+    }
+
+    /**
+     * Return the domain component of the URL.
+     *
+     * @return string|null the domain component of the URL.
+     */
+    public function get_host(): ?string {
+        return $this->host;
+    }
+
+    /**
+     * Return the full URL string.
+     *
+     * @return string the full URL string.
+     */
+    public function get_value() {
+        return  $this->url;
+    }
+}
diff --git a/admin/tool/moodlenet/classes/moodlenet_user_profile.php b/admin/tool/moodlenet/classes/moodlenet_user_profile.php
new file mode 100644 (file)
index 0000000..51e33f9
--- /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/>.
+
+/**
+ * Moodle net user profile class.
+ *
+ * @package    tool_moodlenet
+ * @copyright  2020 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_moodlenet;
+
+/**
+ * A class to represent the moodlenet profile.
+ *
+ * @package    tool_moodlenet
+ * @copyright  2020 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class moodlenet_user_profile {
+
+    /** @var string $profile The full profile name. */
+    protected $profile;
+
+    /** @var int $userid The user ID that this profile belongs to. */
+    protected $userid;
+
+    /** @var string $username The username from $userprofile */
+    protected $username;
+
+    /** @var string $domain The domain from $domain */
+    protected $domain;
+
+    /**
+     * Constructor method.
+     *
+     * @param string $userprofile The moodle net user profile string.
+     * @param int $userid The user ID that this profile belongs to.
+     */
+    public function __construct(string $userprofile, int $userid) {
+        $this->profile = $userprofile;
+        $this->userid = $userid;
+
+        $explodedprofile = explode('@', $this->profile);
+        if (count($explodedprofile) === 2) {
+            // It'll either be an email or WebFinger entry.
+            $this->username = $explodedprofile[0];
+            $this->domain = $explodedprofile[1];
+        } else if (count($explodedprofile) === 3) {
+            // We may have a profile link as MoodleNet gives to the user.
+            $this->username = $explodedprofile[1];
+            $this->domain = $explodedprofile[2];
+        } else {
+            throw new \moodle_exception('invalidmoodlenetprofile', 'tool_moodlenet');
+        }
+    }
+
+    /**
+     * Get the full moodle net profile.
+     *
+     * @return string The moodle net profile.
+     */
+    public function get_profile_name(): string {
+        return $this->profile;
+    }
+
+    /**
+     * Get the user ID that this profile belongs to.
+     *
+     * @return int The user ID.
+     */
+    public function get_userid(): int {
+        return $this->userid;
+    }
+
+    /**
+     * Get the username for this profile.
+     *
+     * @return string The username.
+     */
+    public function get_username(): string {
+        return $this->username;
+    }
+
+    /**
+     * Get the domain for this profile.
+     *
+     * @return string The domain.
+     */
+    public function get_domain(): string {
+        return $this->domain;
+    }
+}
diff --git a/admin/tool/moodlenet/classes/output/renderer.php b/admin/tool/moodlenet/classes/output/renderer.php
new file mode 100644 (file)
index 0000000..98de3e4
--- /dev/null
@@ -0,0 +1,52 @@
+<?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/>.
+
+/**
+ * Renderer.
+ *
+ * @package    tool_moodlenet
+ * @copyright  2020 Mathew May {@link https://mathew.solutions}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_moodlenet\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+use plugin_renderer_base;
+
+/**
+ * Renderer class.
+ *
+ * @package    tool_moodlenet
+ * @copyright  2020 Mathew May {@link https://mathew.solutions}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends plugin_renderer_base {
+
+    /**
+     * Defer to template.
+     *
+     * @param select_page $selectpage
+     * @return string HTML
+     */
+    protected function render_select_page(select_page $selectpage): string {
+
+        $this->page->requires->js_call_amd('tool_moodlenet/select_page', 'init', [$selectpage->get_import_info()->get_id()]);
+        $data = $selectpage->export_for_template($this);
+        return parent::render_from_template('tool_moodlenet/select_page', $data);
+    }
+}
diff --git a/admin/tool/moodlenet/classes/output/select_page.php b/admin/tool/moodlenet/classes/output/select_page.php
new file mode 100644 (file)
index 0000000..60bbde1
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Select page renderable.
+ *
+ * @package    tool_moodlenet
+ * @copyright  2020 Mathew May {@link https://mathew.solutions}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_moodlenet\output;
+
+defined('MOODLE_INTERNAL') || die;
+
+use tool_moodlenet\local\import_info;
+
+/**
+ * Select page renderable.
+ *
+ * @package    tool_moodlenet
+ * @copyright  2020 Mathew May {@link https://mathew.solutions}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class select_page implements \renderable, \templatable {
+
+    /** @var import_info $importinfo resource and config information pertaining to an import. */
+    protected $importinfo;
+
+    /**
+     * Inits the Select page renderable.
+     *
+     * @param import_info $importinfo resource and config information pertaining to an import.
+     */
+    public function __construct(import_info $importinfo) {
+        $this->importinfo = $importinfo;
+    }
+
+    /**
+     * Return the import info.
+     *
+     * @return import_info the import information.
+     */
+    public function get_import_info(): import_info {
+        return $this->importinfo;
+    }
+
+    /**
+     * Export the data.
+     *
+     * @param \renderer_base $output
+     * @return \stdClass
+     */
+    public function export_for_template(\renderer_base $output): \stdClass {
+
+        // Prepare the context object.
+        return (object) [
+            'name' => $this->importinfo->get_resource()->get_name(),
+            'type' => $this->importinfo->get_config()->type,
+            'cancellink' => new \moodle_url('/my'),
+        ];
+    }
+}
diff --git a/admin/tool/moodlenet/classes/privacy/provider.php b/admin/tool/moodlenet/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..a076f4b
--- /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 class for tool_moodlenet.
+ *
+ * @package    tool_moodlenet
+ * @copyright  2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_moodlenet\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy class for tool_moodlenet.
+ *
+ * @copyright  2020 Jake Dallimore <jrhdallimore@gmail.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/admin/tool/moodlenet/classes/profile_manager.php b/admin/tool/moodlenet/classes/profile_manager.php
new file mode 100644 (file)
index 0000000..f1a922a
--- /dev/null
@@ -0,0 +1,350 @@
+<?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/>.
+
+/**
+ * Profile manager class
+ *
+ * @package    tool_moodlenet
+ * @copyright  2020 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_moodlenet;
+
+/**
+ * Class for handling interaction with the moodlenet profile.
+ *
+ * @package    tool_moodlenet
+ * @copyright  2020 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class profile_manager {
+
+    /**
+     * Get the mnet profile for a user.
+     *
+     * @param  int $userid The ID for the user to get the profile form
+     * @return moodlenet_user_profile or null.
+     */
+    public static function get_moodlenet_user_profile(int $userid): ?moodlenet_user_profile {
+        global $CFG;
+        // Check for official profile.
+        if (self::official_profile_exists()) {
+            $user = \core_user::get_user($userid, 'moodlenetprofile');
+            try {
+                $userprofile = $user->moodlenetprofile ? $user->moodlenetprofile : '';
+                return (isset($user)) ? new moodlenet_user_profile($userprofile, $userid) : null;
+            } catch (\moodle_exception $e) {
+                // If an exception is thrown, means there isn't a valid profile set. No need to log exception.
+                return null;
+            }
+        }
+        // Otherwise get hacked in user profile field.
+        require_once($CFG->dirroot . '/user/profile/lib.php');
+        $profilefields = profile_get_user_fields_with_data($userid);
+        foreach ($profilefields as $key => $field) {
+            if ($field->get_category_name() == self::get_category_name()
+                    && $field->inputname == 'profile_field_mnetprofile') {
+                try {
+                    return new moodlenet_user_profile($field->display_data(), $userid);
+                } catch (\moodle_exception $e) {
+                    // If an exception is thrown, means there isn't a valid profile set. No need to log exception.
+                    return null;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Save the moodlenet profile.
+     *
+     * @param moodlenet_user_profile $moodlenetprofile The moodlenet profile to save.
+     */
+    public static function save_moodlenet_user_profile(moodlenet_user_profile $moodlenetprofile): void {
+        global $CFG, $DB;
+        // Do some cursory checks first to see if saving is possible.
+        if (self::official_profile_exists()) {
+            // All good. Let's save.
+            $user = \core_user::get_user($moodlenetprofile->get_userid());
+            $user->moodlenetprofile = $moodlenetprofile->get_profile_name();
+
+            require_once($CFG->dirroot . '/user/lib.php');
+
+            \user_update_user($user, false, true);
+            return;
+        }
+        $fielddata = self::get_user_profile_field();
+        $fielddata = self::validate_and_fix_missing_profile_items($fielddata);
+        // Everything should be back to normal. Let's save.
+        require_once($CFG->dirroot . '/user/profile/lib.php');
+        \profile_save_custom_fields($moodlenetprofile->get_userid(),
+                [$fielddata->shortname => $moodlenetprofile->get_profile_name()]);
+    }
+
+    /**
+     * Checks to see if the required user profile fields and categories are in place. If not it regenerates them.
+     *
+     * @param  stdClass $fielddata The moodlenet profile field.
+     * @return stdClass The same moodlenet profile field, with any necessary updates made.
+     */
+    private static function validate_and_fix_missing_profile_items(\stdClass $fielddata): \stdClass {
+        global $DB;
+
+        if (empty((array) $fielddata)) {
+            // We need to regenerate the category and field to store this data.
+            if (!self::check_profile_category()) {
+                $categoryid = self::create_user_profile_category();
+                self::create_user_profile_text_field($categoryid);
+            } else {
+                // We need the category id.
+                $category = $DB->get_record('user_info_category', ['name' => self::get_category_name()]);
+                self::create_user_profile_text_field($category->id);
+            }
+            $fielddata = self::get_user_profile_field();
+        } else {
+            if (!self::check_profile_category($fielddata->categoryid)) {
+                $categoryid = self::create_user_profile_category();
+                // Update the field to put it back into this category.
+                $fielddata->categoryid = $categoryid;
+                $DB->update_record('user_info_field', $fielddata);
+            }
+        }
+        return $fielddata;
+    }
+
+    /**
+     * Returns the user profile field table object.
+     *
+     * @return stdClass the moodlenet profile table object. False if no record found.
+     */
+    private static function get_user_profile_field(): \stdClass {
+        global $DB;
+        $fieldname = self::get_profile_field_name();
+        $record = $DB->get_record('user_info_field', ['shortname' => $fieldname]);
+        return ($record) ? $record : (object) [];
+    }
+
+    /**
+     * This reports back if the category has been deleted or the config value is different.
+     *
+     * @param  int $categoryid The category id to check against.
+     * @return bool True is the category checks out, otherwise false.
+     */
+    private static function check_profile_category(int $categoryid = null): bool {
+        global $DB;
+        $categoryname = self::get_category_name();
+        $categorydata = $DB->get_record('user_info_category', ['name' => $categoryname]);
+        if (empty($categorydata)) {
+            return false;
+        }
+        if (isset($categoryid) && $categorydata->id != $categoryid) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Are we using the proper user profile field to hold the mnet profile?
+     *
+     * @return bool True if we are using a user table field for the mnet profile. False means we are using costom profile fields.
+     */
+    public static function official_profile_exists(): bool {
+        global $DB;
+
+        $usertablecolumns = $DB->get_columns('user', false);
+        if (isset($usertablecolumns['moodlenetprofile'])) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Gets the category name that is set for this site.
+     *
+     * @return string The category used to hold the moodle net profile field.
+     */
+    public static function get_category_name(): string {
+        return get_config('tool_moodlenet', 'profile_category');
+    }
+
+    /**
+     * Sets the a unique category to hold the moodle net user profile.
+     *
+     * @param string $categoryname The base category name to use.
+     * @return string The actual name of the category to use.
+     */
+    private static function set_category_name(string $categoryname): string {
+        global $DB;
+
+        $attemptname = $categoryname;
+
+        // Check if this category already exists.
+        $foundcategoryname = false;
+        $i = 0;
+        do {
+            $category = $DB->count_records('user_info_category', ['name' => $attemptname]);
+            if ($category > 0) {
+                $i++;
+                $attemptname = $categoryname . $i;
+            } else {
+                set_config('profile_category', $attemptname, 'tool_moodlenet');
+                $foundcategoryname = true;
+            }
+        } while (!$foundcategoryname);
+        return $attemptname;
+    }
+
+    /**
+     * Create a custom user profile category to hold our custom field.
+     *
+     * @return int The id of the created category.
+     */
+    public static function create_user_profile_category(): int {
+        global $DB;
+        // No nice API to do this, so direct DB calls it is.
+        $data = new \stdClass();
+        $data->sortorder = $DB->count_records('user_info_category') + 1;
+        $data->name = self::set_category_name(get_string('pluginname', 'tool_moodlenet'));
+        $data->id = $DB->insert_record('user_info_category', $data, true);
+
+        $createdcategory = $DB->get_record('user_info_category', array('id' => $data->id));
+        \core\event\user_info_category_created::create_from_category($createdcategory)->trigger();
+        return $createdcategory->id;
+    }
+
+    /**
+     * Sets a unique name to be used for the moodle net profile.
+     *
+     * @param string $fieldname The base fieldname to use.
+     * @return string The actual profile field name.
+     */
+    private static function set_profile_field_name(string $fieldname): string {
+        global $DB;
+
+        $attemptname = $fieldname;
+
+        // Check if this profilefield already exists.
+        $foundfieldname = false;
+        $i = 0;
+        do {
+            $profilefield = $DB->count_records('user_info_field', ['shortname' => $attemptname]);
+            if ($profilefield > 0) {
+                $i++;
+                $attemptname = $fieldname . $i;
+            } else {
+                set_config('profile_field_name', $attemptname, 'tool_moodlenet');
+                $foundfieldname = true;
+            }
+        } while (!$foundfieldname);
+        return $attemptname;
+    }
+
+    /**
+     * Gets the unique profile field used to hold the moodle net profile.
+     *
+     * @return string The profile field name being used on this site.
+     */
+    public static function get_profile_field_name(): string {
+        return get_config('tool_moodlenet', 'profile_field_name');
+    }
+
+
+    /**
+     * Create a user profile field to hold the moodlenet profile information.
+     *
+     * @param  int $categoryid The category to put this field into.
+     */
+    public static function create_user_profile_text_field(int $categoryid): void {
+        global $CFG;
+
+        require_once($CFG->dirroot . '/user/profile/definelib.php');
+        require_once($CFG->dirroot . '/user/profile/field/text/define.class.php');
+
+        // Add our moodlenet profile field.
+        $profileclass = new \profile_define_text();
+        $data = (object) [
+            'shortname' => self::set_profile_field_name('mnetprofile'),
+            'name' => get_string('mnetprofile', 'tool_moodlenet'),
+            'datatype' => 'text',
+            'description' => get_string('mnetprofiledesc', 'tool_moodlenet'),
+            'descriptionformat' => 1,
+            'categoryid' => $categoryid,
+            'signup' => 1,
+            'forceunique' => 1,
+            'visible' => 2,
+            'param1' => 30,
+            'param2' => 2048
+        ];
+        $profileclass->define_save($data);
+    }
+
+    /**
+     * Given our $moodlenetprofile let's cURL the domains' WebFinger endpoint
+     *
+     * @param moodlenet_user_profile $moodlenetprofile The moodlenet profile to get info from.
+     * @return array [bool, text, raw]
+     */
+    public static function get_moodlenet_profile_link(moodlenet_user_profile $moodlenetprofile): array {
+        $domain = $moodlenetprofile->get_domain();
+        $username = $moodlenetprofile->get_username();
+
+        // Assumption: All MoodleNet instance's will contain a WebFinger validation script.
+        $url = "https://".$domain."/.well-known/webfinger?resource=acct:".$username."@".$domain;
+
+        $curl = new \curl();
+        $options = [
+            'CURLOPT_HEADER' => 0,
+        ];
+        $content = $curl->get($url, null, $options);
+        $errno   = $curl->get_errno();
+        $info = $curl->get_info();
+
+        // The base cURL seems fine, let's press on.
+        if (!$errno) {
+            // WebFinger gave us a 404 back so the user has no droids here.
+            if ($info['http_code'] >= 400) {
+                if ($info['http_code'] === 404) {
+                    // User not found.
+                    return [
+                        'result' => false,
+                        'message' => get_string('profilevalidationfail', 'tool_moodlenet'),
+                    ];
+                } else {
+                    // There was some other error that was not a missing account.
+                    return [
+                        'result' => false,
+                        'message' => get_string('profilevalidationerror', 'tool_moodlenet'),
+                    ];
+                }
+            }
+
+            // We must have a valid link so give it back to the user.
+            $data = json_decode($content);
+            return [
+                'result' => true,
+                'message' => get_string('profilevalidationpass', 'tool_moodlenet'),
+                'domain' => $data->aliases[0]
+            ];
+        } else {
+            // There was some failure in curl so report it back.
+            return [
+                'result' => false,
+                'message' => get_string('profilevalidationerror', 'tool_moodlenet'),
+            ];
+        }
+    }
+}
diff --git a/admin/tool/moodlenet/db/services.php b/admin/tool/moodlenet/db/services.php
new file mode 100644 (file)
index 0000000..a5b452a
--- /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/>.
+
+/**
+ * Tool Moodle.Net webservice definitions.
+ *
+ * @package    tool_moodlenet
+ * @copyright  2020 Mathew May {@link https://mathew.solutions}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$functions = [
+    'tool_moodlenet_verify_webfinger' => [
+        'classname'   => 'tool_moodlenet\external',
+        'methodname'  => 'verify_webfinger',
+        'description' => 'Verify if the passed information resolves into a WebFinger profile URL',
+        'type'        => 'read',
+        'ajax'        => true,
+        'services'    => [MOODLE_OFFICIAL_MOBILE_SERVICE]
+    ],
+    'tool_moodlenet_search_courses' => [
+        'classname'   => 'tool_moodlenet\external',
+        'methodname'  => 'search_courses',
+        'description' => 'For some given input search for a course that matches',
+        'type'        => 'read',
+        'ajax'        => true,
+        'services'    => [MOODLE_OFFICIAL_MOBILE_SERVICE]
+    ],
+];
diff --git a/admin/tool/moodlenet/db/upgrade.php b/admin/tool/moodlenet/db/upgrade.php
new file mode 100644 (file)
index 0000000..24f6beb
--- /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/>.
+
+/**
+ * Upgrade script for tool_moodlenet.
+ *
+ * @package    tool_moodlenet
+ * @copyright  2020 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Upgrade the plugin.
+ *
+ * @param int $oldversion
+ * @return bool always true
+ */
+function xmldb_tool_moodlenet_upgrade(int $oldversion) {
+    global $CFG, $DB;
+    if ($oldversion < 2020060500) {
+
+        // Grab some of the old settings.
+        $categoryname = get_config('tool_moodlenet', 'profile_category');
+        $profilefield = get_config('tool_moodlenet', 'profile_field_name');
+
+        // Master version only!
+
+        // Find out if we have a custom profile field for moodle.net.
+        $sql = "SELECT f.*
+                  FROM {user_info_field} f
+                  JOIN {user_info_category} c ON c.id = f.categoryid and c.name = :categoryname
+                 WHERE f.shortname = :name";
+
+        $params = [
+            'categoryname' => $categoryname,
+            'name' => $profilefield
+        ];
+
+        $record = $DB->get_record_sql($sql, $params);
+
+        if (!empty($record)) {
+            $userentries = $DB->get_recordset('user_info_data', ['fieldid' => $record->id]);
+            $recordstodelete = [];
+            foreach ($userentries as $userentry) {
+                $data = (object) [
+                    'id' => $userentry->userid,
+                    'moodlenetprofile' => $userentry->data
+                ];
+                $DB->update_record('user', $data, true);
+                $recordstodelete[] = $userentry->id;
+            }
+            $userentries->close();
+
+            // Remove the user profile data, fields, and category.
+            $DB->delete_records_list('user_info_data', 'id', $recordstodelete);
+            $DB->delete_records('user_info_field', ['id' => $record->id]);
+            $DB->delete_records('user_info_category', ['name' => $categoryname]);
+            unset_config('profile_field_name', 'tool_moodlenet');
+            unset_config('profile_category', 'tool_moodlenet');
+        }
+
+        upgrade_plugin_savepoint(true, 2020060500, 'tool', 'moodlenet');
+    }
+
+    return true;
+}
diff --git a/admin/tool/moodlenet/import.php b/admin/tool/moodlenet/import.php
new file mode 100644 (file)
index 0000000..1b57ce9
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This is the main endpoint which MoodleNet instances POST to.
+ *
+ * MoodleNet instances send the user agent to this endpoint via a form POST.
+ * Then:
+ * 1. The POSTed resource information is put in a session store for cross-request access.
+ * 2. This page makes a GET request for admin/tool/moodlenet/index.php (the import confirmation page).
+ * 3. Then, depending on whether the user is authenticated, the user will either:
+ * - If not authenticated, they will be asked to login, after which they will see the confirmation page (leveraging $wantsurl).
+ * - If authenticated, they will see the confirmation page immediately.
+ *
+ * @package     tool_moodlenet
+ * @copyright   2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use tool_moodlenet\local\import_info;
+use tool_moodlenet\local\remote_resource;
+use tool_moodlenet\local\url;
+
+require_once(__DIR__ . '/../../../config.php');
+
+// The integration must be enabled for this import endpoint to be active.
+if (!get_config('tool_moodlenet', 'enablemoodlenet')) {
+    print_error('moodlenetnotenabled', 'tool_moodlenet');
+}
+
+$resourceurl = required_param('resourceurl', PARAM_URL);
+$resourceinfo = required_param('resource_info', PARAM_RAW);
+$resourceinfo = json_decode($resourceinfo);
+$type = optional_param('type', 'link', PARAM_TEXT);
+$course = optional_param('course', 0, PARAM_INT);
+$section = optional_param('section', 0, PARAM_INT);
+// If course isn't provided, course and section are null.
+if (empty($course)) {
+    $course = null;
+    $section = null;
+}
+$name = validate_param($resourceinfo->name, PARAM_TEXT);
+$description = validate_param($resourceinfo->summary, PARAM_TEXT);
+
+// Only accept POSTs.
+if (!empty($_POST)) {
+    // Store information about the import of the resource for the current user.
+    $importconfig = (object) [
+        'course' => $course,
+        'section' => $section,
+        'type' => $type,
+    ];
+    $metadata = (object) [
+        'name' => $name,
+        'description' => $description ?? ''
+    ];
+
+    require_once($CFG->libdir . '/filelib.php');
+    $importinfo = new import_info(
+        $USER->id,
+        new remote_resource(new \curl(), new url($resourceurl), $metadata),
+        $importconfig
+    );
+    $importinfo->save();
+
+    // Redirect to the import confirmation page, detouring via the log in page if required.
+    redirect(new moodle_url('/admin/tool/moodlenet/index.php', ['id' => $importinfo->get_id()]));
+
+}
+
+// Invalid or missing POST data. Show an error to the user.
+print_error('missinginvalidpostdata', 'tool_moodlenet');
diff --git a/admin/tool/moodlenet/index.php b/admin/tool/moodlenet/index.php
new file mode 100644 (file)
index 0000000..5680d19
--- /dev/null
@@ -0,0 +1,136 @@
+<?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/>.
+
+/**
+ * Landing page for all imports from MoodleNet.
+ *
+ * This page asks the user to confirm the import process, and takes them to the relevant next step.
+ *
+ * @package     tool_moodlenet
+ * @copyright   2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use tool_moodlenet\local\import_info;
+use tool_moodlenet\local\import_backup_helper;
+
+require_once(__DIR__ . '/../../../config.php');
+require_once($CFG->dirroot .'/course/lib.php');
+
+$cancel = optional_param('cancel', null, PARAM_TEXT);
+$continue = optional_param('continue', null, PARAM_TEXT);
+$id = required_param('id', PARAM_ALPHANUM);
+
+if (is_null($importinfo = import_info::load($id))) {
+    throw new moodle_exception('missinginvalidpostdata', 'tool_moodlenet');
+}
+
+// Access control.
+require_login($importinfo->get_config()->course, false); // Course may be null here - that's ok.
+if ($importinfo->get_config()->course) {
+    require_capability('moodle/course:manageactivities', context_course::instance($importinfo->get_config()->course));
+}
+if (!get_config('tool_moodlenet', 'enablemoodlenet')) {
+    print_error('moodlenetnotenabled', 'tool_moodlenet');
+}
+
+// Handle the form submits.
+// This page POSTs to self to verify the sesskey for the confirm action.
+// The next page will either be:
+// - 1. The restore process for a course or module, if the file is an mbz file.
+// - 2. The 'select a course' tool page, if course and section are not provided.
+// - 3. The 'select what to do with the content' tool page, provided course and section are present.
+// - 4. The dashboard, if the user decides to cancel and course or section is not found.
+// - 5. The course home, if the user decides to cancel but the course and section are found.
+if ($cancel) {
+    if (!empty($importinfo->get_config()->course)) {
+        $url = new \moodle_url('/course/view.php', ['id' => $importinfo->get_config()->course]);
+    } else {
+        $url = new \moodle_url('/');
+    }
+    redirect($url);
+} else if ($continue) {
+    confirm_sesskey();
+
+    // Handle backups.
+    if (strtolower($importinfo->get_resource()->get_extension()) == 'mbz') {
+        if (empty($importinfo->get_config()->course)) {
+            // Find a course that the user has permission to upload a backup file.
+            // This is likely to be very slow on larger sites.
+            $context = import_backup_helper::get_context_for_user($USER->id);
+
+            if (is_null($context)) {
+                print_error('nopermissions', 'error', '', get_string('restore:uploadfile', 'core_role'));
+            }
+        } else {
+            $context = context_course::instance($importinfo->get_config()->course);
+        }
+
+        $importbackuphelper = new import_backup_helper($importinfo->get_resource(), $USER, $context);
+        $storedfile = $importbackuphelper->get_stored_file();
+
+        $url = new \moodle_url('/backup/restorefile.php', [
+            'component' => $storedfile->get_component(),
+            'filearea' => $storedfile->get_filearea(),
+            'itemid' => $storedfile->get_itemid(),
+            'filepath' => $storedfile->get_filepath(),
+            'filename' => $storedfile->get_filename(),
+            'filecontextid' => $storedfile->get_contextid(),
+            'contextid' => $context->id,
+            'action' => 'choosebackupfile'
+        ]);
+        redirect($url);
+    }
+
+    // Handle adding files to a course.
+    // Course and section data present and confirmed. Redirect to the option select view.
+    if (!is_null($importinfo->get_config()->course) && !is_null($importinfo->get_config()->section)) {
+        redirect(new \moodle_url('/admin/tool/moodlenet/options.php', ['id' => $id]));
+    }
+
+    if (is_null($importinfo->get_config()->course)) {
+        redirect(new \moodle_url('/admin/tool/moodlenet/select.php', ['id' => $id]));
+    }
+}
+
+// Display the page.
+$PAGE->set_context(context_system::instance());
+$PAGE->set_pagelayout('base');
+$PAGE->set_title(get_string('addingaresource', 'tool_moodlenet'));
+$PAGE->set_heading(get_string('addingaresource', 'tool_moodlenet'));
+$url = new moodle_url('/admin/tool/moodlenet/index.php');
+$PAGE->set_url($url);
+$renderer = $PAGE->get_renderer('core');
+
+// Relevant confirmation form.
+$context = $context = [
+    'resourceurl' => $importinfo->get_resource()->get_url()->get_value(),
+    'resourcename' => $importinfo->get_resource()->get_name(),
+    'resourcetype' => $importinfo->get_config()->type,
+    'sesskey' => sesskey()
+];
+if (!is_null($importinfo->get_config()->course) && !is_null($importinfo->get_config()->section)) {
+    $course = get_course($importinfo->get_config()->course);
+    $context = array_merge($context, [
+        'course' => $course->id,
+        'coursename' => $course->shortname,
+        'section' => $importinfo->get_config()->section
+    ]);
+}
+
+echo $OUTPUT->header();
+echo $renderer->render_from_template('tool_moodlenet/import_confirmation', $context);
+echo $OUTPUT->footer();
diff --git a/admin/tool/moodlenet/lang/en/tool_moodlenet.php b/admin/tool/moodlenet/lang/en/tool_moodlenet.php
new file mode 100644 (file)
index 0000000..1ba4e5b
--- /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/>.
+
+/**
+ * Strings for the tool_moodlenet component.
+ *
+ * @package     tool_moodlenet
+ * @category    string
+ * @copyright   2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['addingaresource'] = 'Adding content from MoodleNet';
+$string['aria:enterprofile'] = "Enter your MoodleNet profile URL";
+$string['aria:footermessage'] = "Browse for content on MoodleNet";
+$string['browsecontentmoodlenet'] = "Or browse for content on MoodleNet";
+$string['clearsearch'] = "Clear search";
+$string['connectandbrowse'] = "Connect to and browse:";
+$string['defaultmoodlenet'] = "Default MoodleNet URL";
+$string['defaultmoodlenet_desc'] = "The URL to either Moodle HQ's MoodleNet instance, or your preferred instance.";
+$string['defaultmoodlenetname'] = "MoodleNet instance name";
+$string['defaultmoodlenetname_desc'] = 'The name of either Moodle HQ\'s MoodleNet instance or your preferred MoodleNet instance to browse on.';
+$string['enablemoodlenet'] = 'Enable MoodleNet integration';
+$string['enablemoodlenet_desc'] = 'Enabling the integration allows users with the \'xx\' capability to browse MoodleNet from the
+activity chooser and import MoodleNet resources into their course. It also allows users to push backups from MoodleNet into Moodle.
+';
+$string['errorduringdownload'] = 'An error occurred while downloading the file: {$a}';
+$string['forminfo'] = "It will be automatically saved on your moodle profile.";
+$string['footermessage'] = "Or browse for content on";
+$string['instancedescription'] = "MoodleNet is an open social media platform for educators, with a focus on the collaborative curation of collections of open resources. ";
+$string['instanceplaceholder'] = '@yourprofile@moodle.net';
+$string['inputhelp'] = 'Or if you have a MoodleNet account already, enter your MoodleNet profile:';
+$string['invalidmoodlenetprofile'] = '$userprofile is not correctly formatted';
+$string['importconfirm'] = 'You are about to import the content "{$a->resourcename} ({$a->resourcetype})" into the course "{$a->coursename}". Are you sure you want to continue?';
+$string['importconfirmnocourse'] = 'You are about to import the content "{$a->resourcename} ({$a->resourcetype})" into your site. Are you sure you want to continue?';
+$string['importformatselectguidingtext'] = 'In which format would you like this content "{$a->name} ({$a->type})" to be added to your course?';
+$string['importformatselectheader'] = 'Choose the content display format';
+$string['missinginvalidpostdata'] = 'The resource information from MoodleNet is either missing, or is in an incorrect format.
+If this happens repeatedly, please contact the site administrator.';
+$string['mnetprofile'] = 'MoodleNet profile';
+$string['mnetprofiledesc'] = '<p>Enter in your MoodleNet profile details here to be redirected to your profile while visiting MoodleNet.</p>';
+$string['moodlenetsettings'] = 'MoodleNet settings';
+$string['moodlenetnotenabled'] = 'The MoodleNet integration must be enabled before resource imports can be processed.
+To enable this feature, see the \'enablemoodlenet\' setting.';
+$string['notification'] = 'You are about to import the content "{$a->name} ({$a->type})" into your site. Select the course in which it should be added, or <a href="{$a->cancellink}">cancel</a>.';
+$string['searchcourses'] = "Search courses";
+$string['selectpagetitle'] = 'Select page';
+$string['pluginname'] = 'MoodleNet';
+$string['privacy:metadata'] = "The MoodleNet tool only facilitates communication with MoodleNet. It stores no data.";
+$string['profilevalidationerror'] = 'There was a problem trying to validate your profile';
+$string['profilevalidationfail'] = 'Please enter a valid MoodleNet profile';
+$string['profilevalidationpass'] = 'Looks good!';
+$string['saveandgo'] = "Save and go";
+$string['uploadlimitexceeded'] = 'The file size {$a->filesize} exceeds the user upload limit of {$a->uploadlimit} bytes.';
diff --git a/admin/tool/moodlenet/lib.php b/admin/tool/moodlenet/lib.php
new file mode 100644 (file)
index 0000000..5affcd9
--- /dev/null
@@ -0,0 +1,104 @@
+<?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/>.
+
+/**
+ * This page lists public api for tool_moodlenet plugin.
+ *
+ * @package    tool_moodlenet
+ * @copyright  2020 Peter Dias
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+use \core_course\local\entity\activity_chooser_footer;
+
+/**
+ * The default endpoint to MoodleNet.
+ */
+define('MOODLENET_DEFAULT_ENDPOINT', "lms/moodle/search");
+
+/**
+ * Generate the endpoint url to the user's moodlenet site.
+ *
+ * @param string $profileurl The user's moodlenet profile page
+ * @param int $course The moodle course the mnet resource will be added to
+ * @param int $section The section of the course will be added to. Defaults to the 0th element.
+ * @return string the resulting endpoint
+ * @throws moodle_exception
+ */
+function generate_mnet_endpoint(string $profileurl, int $course, int $section = 0) {
+    global $CFG;
+    $urlportions = explode('@', $profileurl);
+    $domain = end($urlportions);
+    $parsedurl = parse_url($domain);
+    $params = [
+        'site' => $CFG->wwwroot,
+        'course' => $course,
+        'section' => $section
+    ];
+    $endpoint = new moodle_url(MOODLENET_DEFAULT_ENDPOINT, $params);
+    return (isset($parsedurl['scheme']) ? $domain : "https://$domain")."/{$endpoint->out(false)}";
+}
+
+/**
+ * Hooking function to build up the initial Activity Chooser footer information for MoodleNet
+ *
+ * @param int $courseid The course the user is currently in and wants to add resources to
+ * @param int $sectionid The section the user is currently in and wants to add resources to
+ * @return activity_chooser_footer
+ * @throws dml_exception
+ * @throws moodle_exception
+ */
+function tool_moodlenet_custom_chooser_footer(int $courseid, int $sectionid): activity_chooser_footer {
+    global $CFG, $USER, $OUTPUT;
+    $defaultlink = get_config('tool_moodlenet', 'defaultmoodlenet');
+    $enabled = get_config('tool_moodlenet', 'enablemoodlenet');
+
+    $advanced = false;
+    // We are in the MoodleNet lib. It is safe assume we have our own functions here.
+    $mnetprofile = \tool_moodlenet\profile_manager::get_moodlenet_user_profile($USER->id);
+    if ($mnetprofile !== null) {
+        $advanced = $mnetprofile->get_domain() ?? false;
+    }
+
+    $defaultlink = generate_mnet_endpoint($defaultlink, $courseid, $sectionid);
+    if ($advanced !== false) {
+        $advanced = generate_mnet_endpoint($advanced, $courseid, $sectionid);
+    }
+
+    $renderedfooter = $OUTPUT->render_from_template('tool_moodlenet/chooser_footer', (object)[
+        'enabled' => (bool)$enabled,
+        'generic' => $defaultlink,
+        'advanced' => $advanced,
+        'courseID' => $courseid,
+        'sectionID' => $sectionid,
+        'img' => $OUTPUT->image_url('MoodleNet', 'tool_moodlenet')->out(false),
+    ]);
+
+    $renderedcarousel = $OUTPUT->render_from_template('tool_moodlenet/chooser_moodlenet', (object)[
+        'buttonName' => get_config('tool_moodlenet', 'defaultmoodlenetname'),
+        'generic' => $defaultlink,
+        'courseID' => $courseid,
+        'sectionID' => $sectionid,
+        'img' => $OUTPUT->image_url('MoodleNet', 'tool_moodlenet')->out(false),
+    ]);
+    return new activity_chooser_footer(
+        'tool_moodlenet/instance_form',
+        $renderedfooter,
+        $renderedcarousel
+    );
+}
diff --git a/admin/tool/moodlenet/options.php b/admin/tool/moodlenet/options.php
new file mode 100644 (file)
index 0000000..a510f53
--- /dev/null
@@ -0,0 +1,128 @@
+<?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/>.
+
+/**
+ * Page to select WHAT to do with a given resource stored on MoodleNet.
+ *
+ * This collates and presents the same options as a user would see for a drag and drop upload.
+ * That is, it leverages the dndupload_register() hooks and delegates the resource handling to the dndupload_handle hooks.
+ *
+ * This page requires a course, section an resourceurl to be provided via import_info.
+ *
+ * @package     tool_moodlenet
+ * @copyright   2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+use tool_moodlenet\local\import_handler_registry;
+use tool_moodlenet\local\import_processor;
+use tool_moodlenet\local\import_info;
+use tool_moodlenet\local\import_strategy_file;
+use tool_moodlenet\local\import_strategy_link;
+
+require_once(__DIR__ . '/../../../config.php');
+require_once($CFG->dirroot . '/course/lib.php');
+
+$module = optional_param('module', null, PARAM_PLUGIN);
+$import = optional_param('import', null, PARAM_ALPHA);
+$cancel = optional_param('cancel', null, PARAM_ALPHA);
+$id = required_param('id', PARAM_ALPHANUM);
+
+if (is_null($importinfo = import_info::load($id))) {
+    throw new moodle_exception('missinginvalidpostdata', 'tool_moodlenet');
+}
+
+// Resolve course and section params.
+// If course is not already set in the importinfo, we require it in the URL params.
+$config = $importinfo->get_config();
+if (!isset($config->course)) {
+    $course = required_param('course', PARAM_INT);
+    $config->course = $course;
+    $config->section = 0;
+    $importinfo->set_config($config);
+    $importinfo->save();
+}
+
+// Access control.
+require_login($config->course, false);
+require_capability('moodle/course:manageactivities', context_course::instance($config->course));
+if (!get_config('tool_moodlenet', 'enablemoodlenet')) {
+    print_error('moodlenetnotenabled', 'tool_moodlenet');
+}
+
+// If the user cancelled, break early.
+if ($cancel) {
+    redirect(new moodle_url('/course/view.php', ['id' => $config->course]));
+}
+
+// Set up required objects.
+$course = get_course($config->course);
+$handlerregistry = new import_handler_registry($course, $USER);
+switch ($config->type) {
+    case 'file':
+        $strategy = new import_strategy_file();
+        break;
+    case 'link':
+    default:
+        $strategy = new import_strategy_link();
+        break;
+}
+
+if ($import && $module) {
+    confirm_sesskey();
+
+    $handlerinfo = $handlerregistry->get_resource_handler_for_mod_and_strategy($importinfo->get_resource(), $module, $strategy);
+    if (is_null($handlerinfo)) {
+        throw new coding_exception("Invalid handler '$module'. The import handler could not be found.");
+    }
+    $importproc = new import_processor($course, $config->section, $importinfo->get_resource(), $handlerinfo, $handlerregistry);
+    $importproc->process();
+
+    $importinfo->purge(); // We don't need information about the import any more.
+
+    redirect(new moodle_url('/course/view.php', ['id' => $course->id]));
+}
+
+// Setup the page and display the form.
+$PAGE->set_context(context_course::instance($course->id));
+$PAGE->set_pagelayout('base');
+$PAGE->set_title(get_string('coursetitle', 'moodle', array('course' => $course->fullname)));
+$PAGE->set_heading($course->fullname);
+$PAGE->set_url(new moodle_url('/admin/tool/moodlenet/options.php'));
+
+// Fetch the handlers supporting this resource. We'll display each of these as an option in the form.
+$handlercontext = [];
+foreach ($handlerregistry->get_resource_handlers_for_strategy($importinfo->get_resource(), $strategy) as $handler) {
+    $handlercontext[] = [
+        'module' => $handler->get_module_name(),
+        'message' => $handler->get_description(),
+    ];
+}
+
+// Template context.
+$context = [
+    'resourcename' => $importinfo->get_resource()->get_name(),
+    'resourcetype' => $importinfo->get_config()->type,
+    'resourceurl' => urlencode($importinfo->get_resource()->get_url()->get_value()),
+    'course' => $course->id,
+    'section' => $config->section,
+    'sesskey' => sesskey(),
+    'handlers' => $handlercontext,
+    'oneoption' => sizeof($handlercontext) === 1
+];
+
+echo $OUTPUT->header();
+echo $PAGE->get_renderer('core')->render_from_template('tool_moodlenet/import_options_select', $context);
+echo $OUTPUT->footer();
diff --git a/admin/tool/moodlenet/pix/MoodleNet.png b/admin/tool/moodlenet/pix/MoodleNet.png
new file mode 100644 (file)
index 0000000..872756b
Binary files /dev/null and b/admin/tool/moodlenet/pix/MoodleNet.png differ
diff --git a/admin/tool/moodlenet/pix/MoodleNet.svg b/admin/tool/moodlenet/pix/MoodleNet.svg
new file mode 100644 (file)
index 0000000..8f191bf
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 934.36 169.63"><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path d="M762.37,40.25a8.45,8.45,0,0,0-8.44,8.44v88.4l-72-91.84c-1.9-2.41-4.22-4.65-8.28-4.65H671.7A8.56,8.56,0,0,0,663.26,49V160a8.37,8.37,0,0,0,8.27,8.44A8.45,8.45,0,0,0,680,160V69.11l73.63,94.16c2.13,2.48,4.62,4.78,8.42,4.78h.7a7.83,7.83,0,0,0,7.92-8.09V48.69A8.37,8.37,0,0,0,762.37,40.25Z" style="fill:#414443"/><path d="M872.92,119.11c0-23.7-15.06-47.7-43.83-47.7a43.32,43.32,0,0,0-32.44,14.36c-8.25,9.1-12.8,21.37-12.8,34.58v.35c0,27.89,20.21,48.93,47,48.93,14.28,0,25.21-4.37,35.37-14.12a7.4,7.4,0,0,0,2.65-5.59,7.43,7.43,0,0,0-12.5-5.38c-7.52,6.92-15.51,10.15-25.17,10.15-16,0-27.93-11-30.27-27.66H865A7.92,7.92,0,0,0,872.92,119.11ZM828.74,86c11.75,0,24.9,7.35,27.31,27.83H800.93C803.23,97.36,814.46,86,828.74,86Z" style="fill:#414443"/><path d="M926.62,152.4a7.44,7.44,0,0,0-1.88.35,23.35,23.35,0,0,1-6.39.88c-9.38,0-13.75-4.31-13.75-13.57V88.47h22a7.66,7.66,0,0,0,7.74-7.56,7.75,7.75,0,0,0-7.74-7.56h-22V53.44A8.55,8.55,0,0,0,896.16,45a8.27,8.27,0,0,0-8.26,8.44V73.35h-5.82a7.66,7.66,0,0,0-7.56,7.56,7.75,7.75,0,0,0,7.56,7.56h5.82v53.35c0,17.6,9.69,27.28,27.28,27.28a35.66,35.66,0,0,0,13.94-2.57,7.23,7.23,0,0,0,4.89-6.75A7.4,7.4,0,0,0,926.62,152.4Z" style="fill:#414443"/><path d="M153.57,164.26V106.85q0-18-14.87-18t-14.88,18v57.41H94.6V106.85q0-18-14.62-18-14.88,0-14.87,18v57.41H35.88v-60.8q0-18.78,13-28.43Q60.41,66.41,80,66.41q19.83,0,29.23,10.18,8.08-10.19,29.49-10.18,19.56,0,31,8.62,13,9.64,13,28.43v60.8Z" style="fill:#f98012"/><path d="M511.75,164V0H541V164Z" style="fill:#f98012"/><path d="M474.47,164v-9.66q-3.92,5.22-13.32,8.36a49.12,49.12,0,0,1-15.93,2.87q-20.89,0-33.55-14.37T399,115.68c0-13.92,4.11-25.61,12.41-35,7.34-8.3,19.28-14.1,33-14.1,15.49,0,24.54,5.82,30,12.53V0h28.46V164Zm0-54.57q0-7.85-7.44-15t-15.28-7.19A20.73,20.73,0,0,0,434,96.36q-5.75,8.1-5.74,19.84,0,11.49,5.74,19.59,6.54,9.41,17.76,9.4,6.79,0,14.75-6.4t8-13.19Z" style="fill:#f98012"/><path d="M343.91,166.6q-22.2,0-36.69-14.1t-14.5-36.3q0-22.19,14.5-36.29T343.91,65.8q22.18,0,36.82,14.11t14.62,36.29q0,22.2-14.62,36.3T343.91,166.6Zm0-77.29q-10.57,0-16.26,8a32.07,32.07,0,0,0-5.67,19q0,11,5.28,18.63a20.35,20.35,0,0,0,33.3,0q5.54-7.6,5.54-18.63t-5.28-18.63Q354.73,89.31,343.91,89.31Z" style="fill:#f98012"/><path d="M238.15,166.6q-22.2,0-36.69-14.1T187,116.2Q187,94,201.46,79.91T238.15,65.8q22.18,0,36.82,14.11t14.62,36.29q0,22.2-14.62,36.3T238.15,166.6Zm0-77.29q-10.56,0-16.26,8a32.08,32.08,0,0,0-5.68,19q0,11,5.29,18.63a20.35,20.35,0,0,0,33.3,0q5.55-7.6,5.55-18.63t-5.29-18.63Q249,89.31,238.15,89.31Z" style="fill:#f98012"/><path d="M575.64,125.08c.62,7,9.67,21.94,24.55,21.94,14.48,0,21.33-8.36,21.67-11.75l30.81-.27c-3.36,10.28-17,32.13-53,32.13-15,0-28.68-4.66-38.52-14s-14.75-21.46-14.75-36.43q0-23.25,14.75-36.95T599.4,66.07q25.59,0,40,17,13.32,15.67,13.32,42Zm47.78-18a29.09,29.09,0,0,0-7.82-15.4,22,22,0,0,0-15.68-6.53,20.53,20.53,0,0,0-15.28,6.26,31.66,31.66,0,0,0-8.22,15.67Z" style="fill:#f98012"/><path d="M92.65,62l29-21.2-.37-1.29C68.94,45.9,45.11,50.45,0,76.6l.42,1.19,3.59,0a180.84,180.84,0,0,0-.17,26c-5,14.48-.13,24.33,4.45,35,.72-11.15.65-23.34-2.77-35.47a180.08,180.08,0,0,1,.19-25.51l29.91.29a135.7,135.7,0,0,0,.89,17.54c26.72,9.39,53.6,0,67.86-23.19C100.42,68,92.65,62,92.65,62Z" style="fill:#363636"/></g></g></svg>
\ No newline at end of file
diff --git a/admin/tool/moodlenet/pix/courses.svg b/admin/tool/moodlenet/pix/courses.svg
new file mode 100644 (file)
index 0000000..75e59fc
--- /dev/null
@@ -0,0 +1,52 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="157 -1305 148 125" preserveAspectRatio="xMinYMid meet">
+  <defs>
+    <style>
+      .cls-1 {
+        clip-path: url(#clip-Courses);
+      }
+
+      .cls-2 {
+        fill: #eee;
+      }
+
+      .cls-3 {
+        fill: #c4c8cc;
+      }
+
+      .cls-4 {
+        fill: #fff;
+      }
+    </style>
+    <clipPath id="clip-Courses">
+      <rect x="157" y="-1305" width="148" height="125"/>
+    </clipPath>
+  </defs>
+  <g id="Courses" class="cls-1">
+    <g id="Group_44" data-name="Group 44" transform="translate(-268 -1781)">
+      <ellipse id="Ellipse_41" data-name="Ellipse 41" class="cls-2" cx="74" cy="14.785" rx="74" ry="14.785" transform="translate(425 571.43)"/>
+      <rect id="Rectangle_87" data-name="Rectangle 87" class="cls-3" width="95.097" height="110.215" transform="translate(451.909 476)"/>
+      <g id="Group_43" data-name="Group 43" transform="translate(464.04 494)">
+        <rect id="Rectangle_88" data-name="Rectangle 88" class="cls-4" width="31.043" height="34" transform="translate(0)"/>
+        <rect id="Rectangle_89" data-name="Rectangle 89" class="cls-4" width="31.043" height="34" transform="translate(0 42)"/>
+        <rect id="Rectangle_90" data-name="Rectangle 90" class="cls-4" width="31.067" height="34" transform="translate(39.005)"/>
+        <rect id="Rectangle_91" data-name="Rectangle 91" class="cls-4" width="31.067" height="34" transform="translate(39.005 42)"/>
+        <rect id="Rectangle_92" data-name="Rectangle 92" class="cls-3" width="23.023" height="3.18" transform="translate(3.081 16.549)"/>
+        <rect id="Rectangle_93" data-name="Rectangle 93" class="cls-3" width="23.023" height="3.18" transform="translate(3.081 58.549)"/>
+        <rect id="Rectangle_94" data-name="Rectangle 94" class="cls-3" width="23.023" height="3.18" transform="translate(43.122 16.549)"/>
+        <rect id="Rectangle_95" data-name="Rectangle 95" class="cls-3" width="23.023" height="3.18" transform="translate(43.122 58.549)"/>
+        <rect id="Rectangle_96" data-name="Rectangle 96" class="cls-3" width="14.014" height="3.18" transform="translate(3.081 21.825)"/>
+        <rect id="Rectangle_97" data-name="Rectangle 97" class="cls-3" width="18.845" height="3.18" transform="translate(3.081 26.825)"/>
+        <rect id="Rectangle_98" data-name="Rectangle 98" class="cls-3" width="14.014" height="3.18" transform="translate(3.081 63.825)"/>
+        <rect id="Rectangle_99" data-name="Rectangle 99" class="cls-3" width="18.845" height="3.18" transform="translate(3.081 68.825)"/>
+        <rect id="Rectangle_100" data-name="Rectangle 100" class="cls-3" width="14.014" height="3.18" transform="translate(43.122 21.825)"/>
+        <rect id="Rectangle_101" data-name="Rectangle 101" class="cls-3" width="18.845" height="3.18" transform="translate(43.122 26.825)"/>
+        <rect id="Rectangle_102" data-name="Rectangle 102" class="cls-3" width="14.014" height="3.18" transform="translate(43.122 63.825)"/>
+        <rect id="Rectangle_103" data-name="Rectangle 103" class="cls-3" width="18.845" height="3.18" transform="translate(43.122 68.825)"/>
+        <ellipse id="Ellipse_42" data-name="Ellipse 42" class="cls-3" cx="5.658" cy="5.652" rx="5.658" ry="5.652" transform="translate(3.003 3.55)"/>
+        <ellipse id="Ellipse_43" data-name="Ellipse 43" class="cls-3" cx="5.658" cy="5.652" rx="5.658" ry="5.652" transform="translate(3.003 45.55)"/>
+        <ellipse id="Ellipse_44" data-name="Ellipse 44" class="cls-3" cx="5.658" cy="5.652" rx="5.658" ry="5.652" transform="translate(43.044 3.55)"/>
+        <ellipse id="Ellipse_45" data-name="Ellipse 45" class="cls-3" cx="5.658" cy="5.652" rx="5.658" ry="5.652" transform="translate(43.044 45.55)"/>
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/admin/tool/moodlenet/select.php b/admin/tool/moodlenet/select.php
new file mode 100644 (file)
index 0000000..704de2f
--- /dev/null
@@ -0,0 +1,53 @@
+<?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/>.
+
+/**
+ * Select page.
+ *
+ * @package    tool_moodlenet
+ * @copyright  2020 Mathew May {@link https://mathew.solutions}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+use tool_moodlenet\local\import_info;
+use tool_moodlenet\output\select_page;
+
+require_once(__DIR__ . '/../../../config.php');
+
+$id = required_param('id', PARAM_ALPHANUM);
+
+// Access control.
+require_login();
+if (!get_config('tool_moodlenet', 'enablemoodlenet')) {
+    print_error('moodlenetnotenabled', 'tool_moodlenet');
+}
+
+if (is_null($importinfo = import_info::load($id))) {
+    throw new moodle_exception('missinginvalidpostdata', 'tool_moodlenet');
+}
+
+$PAGE->set_url('/admin/tool/moodlenet/select.php');
+$PAGE->set_context(context_system::instance());
+$PAGE->set_pagelayout('standard');
+$PAGE->set_title(get_string('selectpagetitle', 'tool_moodlenet'));
+$PAGE->set_heading(format_string($SITE->fullname));
+
+echo $OUTPUT->header();
+
+$renderable = new select_page($importinfo);
+$renderer = $PAGE->get_renderer('tool_moodlenet');
+echo $renderer->render($renderable);
+
+echo $OUTPUT->footer();
diff --git a/admin/tool/moodlenet/settings.php b/admin/tool/moodlenet/settings.php
new file mode 100644 (file)
index 0000000..4b6fb4f
--- /dev/null
@@ -0,0 +1,46 @@
+<?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/>.
+
+/**
+ * Puts the plugin actions into the admin settings tree.
+ *
+ * @package     tool_moodlenet
+ * @copyright   2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+if ($hassiteconfig) {
+    // Create a MoodleNet category.
+    $ADMIN->add('root', new admin_category('moodlenet', get_string('pluginname', 'tool_moodlenet')));
+    // Our settings page.
+    $settings = new admin_settingpage('tool_moodlenet', get_string('moodlenetsettings', 'tool_moodlenet'));
+    $ADMIN->add('moodlenet', $settings);
+
+    $temp = new admin_setting_configcheckbox('tool_moodlenet/enablemoodlenet', get_string('enablemoodlenet', 'tool_moodlenet'),
+        new lang_string('enablemoodlenet_desc', 'tool_moodlenet'), 1, 1, 0);
+    $settings->add($temp);
+
+    $temp = new admin_setting_configtext('tool_moodlenet/defaultmoodlenetname',
+        get_string('defaultmoodlenetname', 'tool_moodlenet'), new lang_string('defaultmoodlenetname_desc', 'tool_moodlenet'),
+        'Moodle HQ MoodleNet');
+    $settings->add($temp);
+
+    $temp = new admin_setting_configtext('tool_moodlenet/defaultmoodlenet', get_string('defaultmoodlenet', 'tool_moodlenet'),
+        new lang_string('defaultmoodlenet_desc', 'tool_moodlenet'), 'https://home.moodle.net');
+    $settings->add($temp);
+}
diff --git a/admin/tool/moodlenet/templates/chooser_footer.mustache b/admin/tool/moodlenet/templates/chooser_footer.mustache
new file mode 100644 (file)
index 0000000..6748023
--- /dev/null
@@ -0,0 +1,46 @@
+{{!
+    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 tool_moodlenet/chooser_footer
+
+    Chooser favourite template partial.
+
+    Example context (json):
+    {
+    }
+}}
+{{#enabled}}
+    <div class="w-100 d-flex px-2">
+            <a aria-label="{{#str}}aria:footermessage, tool_moodlenet{{/str}}"
+                class="d-inline my-auto mr-1"
+                {{#advanced}}
+                    href="{{advanced}}"
+                    target="_self"
+                {{/advanced}}
+                {{^advanced}}
+                    href="#"
+                    data-action="show-moodlenet"
+                    data-courseid="{{courseID}}"
+                    data-sectionID="{{sectionID}}"
+                {{/advanced}}
+            >
+                {{#str}} footermessage , tool_moodlenet{{/str}}
+
+                <span class="moodlenet-logo" aria-hidden="true">{{#pix}} MoodleNet, tool_moodlenet {{/pix}}</span>
+            </a>
+    </div>
+{{/enabled}}
diff --git a/admin/tool/moodlenet/templates/chooser_footer_close_mnet.mustache b/admin/tool/moodlenet/templates/chooser_footer_close_mnet.mustache
new file mode 100644 (file)
index 0000000..2e6f05e
--- /dev/null
@@ -0,0 +1,28 @@
+{{!
+    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 tool_moodlenet/chooser_footer_close_mnet
+
+    Chooser favourite template partial.
+
+    Example context (json):
+    {
+    }
+}}
+<button data-action="close-chooser-option-summary" class="closeoptionsummary btn btn-secondary mr-auto" tabindex="0">
+    {{#str}} back {{/str}}
+</button>
diff --git a/admin/tool/moodlenet/templates/chooser_moodlenet.mustache b/admin/tool/moodlenet/templates/chooser_moodlenet.mustache
new file mode 100644 (file)
index 0000000..c0c7fc5
--- /dev/null
@@ -0,0 +1,67 @@
+{{!
+    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 tool_moodlenet/chooser_moodlenet
+
+    Chooser favourite template partial.
+
+    Example context (json):
+    {
+    }
+}}
+<div class="optionsummary" tabindex="-1" data-region="chooser-option-summary-container">
+    <div class="content text-left mb-5 px-5 py-4" data-region="chooser-option-summary-content-container">
+        <div data-region="moodle-net">
+            <div class="overlay-icon-container z-index-1 d-none" data-region="spinner"></div>
+            <img class="w-25 mb-4" aria-hidden="true" src="{{{img}}}">
+            <p>{{#str}} instancedescription, tool_moodlenet {{/str}}</p>
+            <p class="w-75 mx-auto mb-1 mt-5">{{#str}} connectandbrowse, tool_moodlenet {{/str}}</p>
+            <a class="btn btn-secondary d-block w-75 mx-auto mb-4"
+               data-action="browse"
+               href="{{{generic}}}"
+            >
+                {{{buttonName}}}
+            </a>
+            <div id="mnet-instance-form-{{uniqid}}" data-region="mnet-form">
+                <input type="hidden" name="sesskey" value="{{sesskey}}">
+                <div class="w-75 mx-auto my-3">
+                    <p class="text-left">{{#str}}inputhelp, tool_moodlenet{{/str}}</p>
+                    <div class="input-group">
+                        <input type="text"
+                               class="form-control"
+                               data-var="mnet-link"
+                               data-courseid="{{courseID}}"
+                               data-sectionid="{{sectionID}}"
+                               placeholder="{{#str}} instanceplaceholder, tool_moodlenet {{/str}}"
+                               aria-label="{{#str}} aria:enterprofile, tool_moodlenet {{/str}}"
+                               autocomplete="off"
+                        >
+                        <div class="input-group-append z-index-0">
+                            <button class="btn btn-secondary"
+                                    data-action="submit" id="button-addon2"
+                            >
+                                {{#str}} saveandgo, tool_moodlenet {{/str}}
+                            </button>
+                        </div>
+                    </div>
+                    <p class="text-left" aria-live="assertive" data-region="validation-area"></p>
+                    <p class="text-left">{{#str}} forminfo, tool_moodlenet {{/str}}</p>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
diff --git a/admin/tool/moodlenet/templates/import_confirmation.mustache b/admin/tool/moodlenet/templates/import_confirmation.mustache
new file mode 100644 (file)
index 0000000..337b22a
--- /dev/null
@@ -0,0 +1,76 @@
+{{!
+    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 tool_moodlenet/import_confirmation
+
+    MoodleNet import confirmation template.
+
+    The purpose of this template is to present the user with a confirm/cancel dialog-like page.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * resourceurl The URL to the remote resource on MoodleNet.
+    * resourcename The name of the remote resource on MoodleNet.
+    * sesskey The CSRF token, as per sesskey()
+
+    Example context (json):
+    {
+        "course": 33,
+        "coursename": "Introduction to quantum physics",
+        "section": 0,
+        "resourceurl": "http://example.com/test.png",
+        "resourcename": "test.png",
+        "sesskey": "abc123"
+    }
+}}
+<div class="generalbox modal modal-dialog modal-in-page show">
+    <div class="box py-3 modal-content">
+        <form action="#" method="post">
+            {{#course}}
+            <input type="hidden" name="course" value="{{course}}">
+            {{/course}}
+
+            <input type="hidden" name="section" value="{{section}}">
+
+            <input type="hidden" name="resourceurl" value="{{resourceurl}}">
+            <input type="hidden" name="sesskey" value="{{sesskey}}">
+            <div class="box py-3 modal-header p-x-1">
+                <h4>{{#str}}confirm, core{{/str}}</h4>
+            </div>
+            <div class="box py-3 modal-body">
+                {{#course}}
+                    {{#str}}importconfirm, tool_moodlenet, {"resourcename": {{#quote}}{{resourcename}}{{/quote}}, "resourcetype": {{#quote}}{{resourcetype}}{{/quote}}, "coursename": {{#quote}}{{coursename}}{{/quote}} }{{/str}}
+                {{/course}}
+                {{^course}}
+                    {{#str}}importconfirmnocourse, tool_moodlenet, {"resourcename": {{#quote}}{{resourcename}}{{/quote}}, "resourcetype": {{#quote}}{{resourcetype}}{{/quote}} }{{/str}}
+                {{/course}}
+
+            </div>
+            <div class="box py-3 modal-footer">
+                <div class="buttons">
+                    <input class="btn btn-secondary" type="submit" name="cancel" value="{{#str}}cancel, core{{/str}}">
+                    <input class="btn btn-primary" type="submit" name="continue" value="{{#str}}confirm, core{{/str}}">
+                </div>
+            </div>
+        </form>
+    </div>
+</div>
diff --git a/admin/tool/moodlenet/templates/import_options_select.mustache b/admin/tool/moodlenet/templates/import_options_select.mustache
new file mode 100644 (file)
index 0000000..9dc42eb
--- /dev/null
@@ -0,0 +1,80 @@
+{{!
+    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 tool_moodlenet/import_options_select
+
+    MoodleNet import options template.
+
+    The purpose of this template is to render an list of import options as radio-button-like controls.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * course The course id.
+    * section The sectio id.
+    * resourceurl The URL to the remote resource on MoodleNet.
+    * resourcename The name of the remote resource on MoodleNet.
+    * sesskey The CSRF token, as per sesskey()
+    * handlers The array of handler options to present the user with
+
+    Example context (json):
+    {
+        "course": 33,
+        "coursename": "Introduction to quantum physics",
+        "section": 0,
+        "resourceurl": "http://example.com/test.png",
+        "resourcename": "A test image",
+        "sesskey": "abc123",
+        "handlers": [
+            {
+                "module": "label",
+                "message": "Add media to the course page"
+            }
+        ]
+    }
+}}
+<div class="generalbox modal modal-dialog modal-in-page show">
+    <div class="box py-3 modal-content">
+        <form action="#" method="post">
+            <input type="hidden" name="course" value="{{course}}">
+            <input type="hidden" name="section" value="{{section}}">
+            <input type="hidden" name="resourceurl" value="{{resourceurl}}">
+            <input type="hidden" name="sesskey" value="{{sesskey}}">
+            <div class="box py-3 modal-header p-x-1">
+                <h4>{{#str}}importformatselectheader, tool_moodlenet{{/str}}</h4>
+            </div>
+            <div class="box py-3 modal-body">
+                {{#str}}importformatselectguidingtext, tool_moodlenet, {"name": {{#quote}}{{resourcename}}{{/quote}}, "type": {{#quote}}{{resourcetype}}{{/quote}} }{{/str}}
+                <br><br>
+                {{#handlers}}
+                    <input id="{{module}}_option" name="module" type="radio" value="{{module}}" {{#oneoption}}checked="checked"{{/oneoption}}>&nbsp;<label for="{{module}}_option">{{message}}</label>
+                    <br>
+                {{/handlers}}
+            </div>
+            <div class="box py-3 modal-footer">
+                <div class="buttons">
+                    <input class="btn btn-secondary" type="submit" name="cancel" value="Cancel">
+                    <input class="btn btn-primary" type="submit" name="import" value="Continue">
+                </div>
+            </div>
+        </form>
+    </div>
+</div>
diff --git a/admin/tool/moodlenet/templates/select_page.mustache b/admin/tool/moodlenet/templates/select_page.mustache
new file mode 100644 (file)
index 0000000..a49903b
--- /dev/null
@@ -0,0 +1,58 @@
+{{!
+    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 tool_moodlenet/select_page
+
+    This template renders the course selection page for the MoodleNet tool.
+
+    Example context (json):
+    {
+        "name": "A cat picture",
+        "cancellink": "https://moodlesite/my"
+    }
+}}
+<div data-region="moodle-net-select">
+    <div class="alert alert-primary" role="alert">
+        {{#str}} notification, tool_moodlenet, {"name": {{#quote}}{{name}}{{/quote}}, "cancellink": {{#quote}}{{cancellink}}{{/quote}}, "type": {{#quote}}{{type}}{{/quote}}  } {{/str}}
+    </div>
+    <h3>{{#str}} selectacourse {{/str}}</h3>
+    <div class="searchbar input-group w-50" role="search">
+        <label for="searchinput">
+            <span class="sr-only">{{#str}} searchcourses, tool_moodlenet {{/str}}</span>
+        </label>
+        <input type="text"
+               data-region="search-input"
+               id="searchinput"
+               class="form-control form-control-lg searchinput border-right-0 px-3 py-2"
+               placeholder="{{#str}} search, core {{/str}}"
+               name="search"
+               autocomplete="off"
+        >
+        <div class="searchbar-append d-flex border border-secondary border-left-0 px-3 py-2">
+            <div data-region="search-icon">
+                {{#pix}} a/search, core {{/pix}}
+            </div>
+            <div class="clear d-none">
+                <button class="btn p-0" data-region="clear-icon">
+                    <span class="d-flex" aria-hidden="true">{{#pix}} e/cancel, core {{/pix}}</span>
+                    <span class="sr-only">{{#str}} clearsearch, tool_moodlenet {{/str}}</span>
+                </button>
+            </div>
+        </div>
+    </div>
+    <div class="my-4" data-region="mnet-courses" aria-live="polite"></div>
+</div>
diff --git a/admin/tool/moodlenet/templates/view-cards.mustache b/admin/tool/moodlenet/templates/view-cards.mustache
new file mode 100644 (file)
index 0000000..da742ff
--- /dev/null
@@ -0,0 +1,54 @@
+{{!
+    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 tool_moodlenet/view-cards
+
+    This template renders the cards view for the MoodleNet tool.
+
+    Example context (json):
+    {
+        "courses": [
+            {
+                "name": "Assignment due 1",
+                "viewurl": "https://moodlesite/course/view.php?id=2",
+                "courseimage": "https://moodlesite/pluginfile/123/course/overviewfiles/123.jpg",
+                "fullname": "course 3",
+                "coursecategory": "Miscellaneous",
+                "visible": true
+            }
+        ]
+    }
+}}
+
+{{< core_course/coursecards }}
+    {{$coursename}}
+        <span class="multiline">
+            {{#shortentext}}50, {{{fullname}}} {{/shortentext}}
+        </span>
+    {{/coursename}}
+    {{$coursecategory}}
+        <span class="sr-only">
+            {{#str}}aria:coursecategory, core_course{{/str}}
+        </span>
+        <span class="categoryname text-truncate">
+            {{{coursecategory}}}
+        </span>
+    {{/coursecategory}}
+    {{$divider}}
+        <div class="pl-1 pr-1">|</div>
+    {{/divider}}
+{{/ core_course/coursecards }}
diff --git a/admin/tool/moodlenet/tests/import_backup_helper_test.php b/admin/tool/moodlenet/tests/import_backup_helper_test.php
new file mode 100644 (file)
index 0000000..81ea96a
--- /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/>.
+
+/**
+ * Unit tests for the import_backup_helper
+ *
+ * @package    tool_moodlenet
+ * @category   test
+ * @copyright  2020 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+/**
+ * Class import_backup_helper tests
+ */
+class tool_moodlenet_import_backup_helper_testcase extends advanced_testcase {
+
+    /**
+     * Test that the first available context with the capability to upload backup files is returned.
+     */
+    public function test_get_context_for_user() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        $user4 = $this->getDataGenerator()->create_user();
+        $user5 = $this->getDataGenerator()->create_user();
+
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'editingteacher');
+        $this->getDataGenerator()->enrol_user($user5->id, $course->id, 'student');
+
+        $category = $this->getDataGenerator()->create_category();
+        $rolerecord = $DB->get_record('role', ['shortname' => 'manager']);
+        $categorycontext = context_coursecat::instance($category->id);
+        $this->getDataGenerator()->role_assign($rolerecord->id, $user3->id, $categorycontext->id);
+        $this->getDataGenerator()->role_assign($rolerecord->id, $user5->id, $categorycontext->id);
+
+        $roleid = $this->getDataGenerator()->create_role();
+        $sitecontext = context_system::instance();
+        assign_capability('moodle/restore:uploadfile', CAP_ALLOW, $roleid, $sitecontext->id, true);
+        accesslib_clear_all_caches_for_unit_testing();
+        $this->getDataGenerator()->role_assign($roleid, $user4->id, $sitecontext->id);
+
+        $result = \tool_moodlenet\local\import_backup_helper::get_context_for_user($user1->id);
+        $this->assertNull($result);
+        $result = \tool_moodlenet\local\import_backup_helper::get_context_for_user($user2->id);
+        $this->assertEquals($result, $coursecontext);
+        $this->assertEquals(CONTEXT_COURSE, $result->contextlevel);
+        $result = \tool_moodlenet\local\import_backup_helper::get_context_for_user($user3->id);
+        $this->assertEquals($result, $categorycontext);
+        $this->assertEquals(CONTEXT_COURSECAT, $result->contextlevel);
+        $result = \tool_moodlenet\local\import_backup_helper::get_context_for_user($user4->id);
+        $this->assertEquals($result, $sitecontext);
+        $this->assertEquals(CONTEXT_SYSTEM, $result->contextlevel);
+        $result = \tool_moodlenet\local\import_backup_helper::get_context_for_user($user5->id);
+        $this->assertEquals($result, $categorycontext);
+        $this->assertEquals(CONTEXT_COURSECAT, $result->contextlevel);
+    }
+
+}
\ No newline at end of file
diff --git a/admin/tool/moodlenet/tests/import_handler_info_test.php b/admin/tool/moodlenet/tests/import_handler_info_test.php
new file mode 100644 (file)
index 0000000..7f308c0
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for the import_handler_info class.
+ *
+ * @package    tool_moodlenet
+ * @category   test
+ * @copyright  2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_moodlenet\local\tests;
+
+use tool_moodlenet\local\import_handler_info;
+use tool_moodlenet\local\import_strategy;
+use tool_moodlenet\local\import_strategy_file;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class tool_moodlenet_import_handler_info_testcase, providing test cases for the import_handler_info class.
+ */
+class tool_moodlenet_import_handler_info_testcase extends \advanced_testcase {
+
+    /**
+     * Test init and the getters.
+     *
+     * @dataProvider handler_info_data_provider
+     * @param string $modname the name of the mod.
+     * @param string $description description of the mod.
+     * @param bool $expectexception whether we expect an exception during init or not.
+     */
+    public function test_initialisation($modname, $description, $expectexception) {
+        $this->resetAfterTest();
+        // Skip those cases we cannot init.
+        if ($expectexception) {
+            $this->expectException(\coding_exception::class);
+            $handlerinfo = new import_handler_info($modname, $description, new import_strategy_file());
+        }
+
+        $handlerinfo = new import_handler_info($modname, $description, new import_strategy_file());
+
+        $this->assertEquals($modname, $handlerinfo->get_module_name());
+        $this->assertEquals($description, $handlerinfo->get_description());
+        $this->assertInstanceOf(import_strategy::class, $handlerinfo->get_strategy());
+    }
+
+
+    /**
+     * Data provider for creation of import_handler_info objects.
+     *
+     * @return array the data for creation of the info object.
+     */
+    public function handler_info_data_provider() {
+        return [
+            'All data present' => ['label', 'Add a label to the course', false],
+            'Empty module name' => ['', 'Add a file resource to the course', true],
+            'Empty description' => ['resource', '', true],
+
+        ];
+    }
+}
diff --git a/admin/tool/moodlenet/tests/import_handler_registry_test.php b/admin/tool/moodlenet/tests/import_handler_registry_test.php
new file mode 100644 (file)
index 0000000..df7399c
--- /dev/null
@@ -0,0 +1,119 @@
+<?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/>.
+
+/**
+ * Unit tests for the import_handler_registry class.
+ *
+ * @package    tool_moodlenet
+ * @category   test
+ * @copyright  2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_moodlenet\local\tests;
+
+use tool_moodlenet\local\import_handler_registry;
+use tool_moodlenet\local\import_handler_info;
+use tool_moodlenet\local\import_strategy_file;
+use tool_moodlenet\local\import_strategy_link;
+use tool_moodlenet\local\remote_resource;
+use tool_moodlenet\local\url;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class tool_moodlenet_import_handler_registry_testcase, providing test cases for the import_handler_registry class.
+ */
+class tool_moodlenet_import_handler_registry_testcase extends \advanced_testcase {
+
+    /**
+     * Test confirming the behaviour of get_resource_handlers_for_strategy with different params.
+     */
+    public function test_get_resource_handlers_for_strategy() {
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $ihr = new import_handler_registry($course, $teacher);
+        $resource = new remote_resource(
+            new \curl(),
+            new url('http://example.org'),
+            (object) [
+                'name' => 'Resource name',
+                'description' => 'Resource description'
+            ]
+        );
+
+        $handlers = $ihr->get_resource_handlers_for_strategy($resource, new import_strategy_file());
+        $this->assertIsArray($handlers);
+        foreach ($handlers as $handler) {
+            $this->assertInstanceOf(import_handler_info::class, $handler);
+        }
+    }
+
+    /**
+     * Test confirming that the results are scoped to the provided user.
+     */
+    public function test_get_resource_handlers_for_strategy_user_scoping() {
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
+
+        $studentihr = new import_handler_registry($course, $student);
+        $teacherihr = new import_handler_registry($course, $teacher);
+        $resource = new remote_resource(
+            new \curl(),
+            new url('http://example.org'),
+            (object) [
+                'name' => 'Resource name',
+                'description' => 'Resource description'
+            ]
+        );
+
+        $this->assertEmpty($studentihr->get_resource_handlers_for_strategy($resource, new import_strategy_file()));
+        $this->assertNotEmpty($teacherihr->get_resource_handlers_for_strategy($resource, new import_strategy_file()));
+    }
+
+    /**
+     * Test confirming that we can find a unique handler based on the module and strategy name.
+     */
+    public function test_get_resource_handler_for_module_and_strategy() {
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $ihr = new import_handler_registry($course, $teacher);
+        $resource = new remote_resource(
+            new \curl(),
+            new url('http://example.org'),
+            (object) [
+                'name' => 'Resource name',
+                'description' => 'Resource description'
+            ]
+        );
+
+        // Resource handles every file type, so we'll always be able to find that unique handler when looking.
+        $handler = $ihr->get_resource_handler_for_mod_and_strategy($resource, 'resource', new import_strategy_file());
+        $this->assertInstanceOf(import_handler_info::class, $handler);
+
+        // URL handles every resource, so we'll always be able to find that unique handler when looking with a link strategy.
+        $handler = $ihr->get_resource_handler_for_mod_and_strategy($resource, 'url', new import_strategy_link());
+        $this->assertInstanceOf(import_handler_info::class, $handler);
+        $this->assertEquals('url', $handler->get_module_name());
+        $this->assertInstanceOf(import_strategy_link::class, $handler->get_strategy());
+    }
+}
diff --git a/admin/tool/moodlenet/tests/import_info_test.php b/admin/tool/moodlenet/tests/import_info_test.php
new file mode 100644 (file)
index 0000000..1151371
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for the import_info class.
+ *
+ * @package    tool_moodlenet
+ * @category   test
+ * @copyright  2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_moodlenet\local\tests;
+
+use tool_moodlenet\local\import_info;
+use tool_moodlenet\local\remote_resource;
+use tool_moodlenet\local\url;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class tool_moodlenet_import_info_testcase, providing test cases for the import_info class.
+ */
+class tool_moodlenet_import_info_testcase extends \advanced_testcase {
+
+    /**
+     * Create some test objects.
+     *
+     * @return array
+     */
+    protected function create_test_info(): array {
+        $user = $this->getDataGenerator()->create_user();
+        $resource = new remote_resource(new \curl(),
+            new url('http://example.org'),
+            (object) [
+                'name' => 'Resource name',
+                'description' => 'Resource summary'
+            ]
+        );
+        $importinfo = new import_info($user->id, $resource, (object)[]);
+
+        return [$user, $resource, $importinfo];
+    }
+
+    /**
+     * Test for creation and getters.
+     */
+    public function test_getters() {
+        $this->resetAfterTest();
+        [$user, $resource, $importinfo] = $this->create_test_info();
+
+        $this->assertEquals($resource, $importinfo->get_resource());
+        $this->assertEquals(new \stdClass(), $importinfo->get_config());
+        $this->assertNotEmpty($importinfo->get_id());
+    }
+
+    /**
+     * Test for setters.
+     */
+    public function test_set_config() {
+        $this->resetAfterTest();
+        [$user, $resource, $importinfo] = $this->create_test_info();
+
+        $config = $importinfo->get_config();
+        $this->assertEquals(new \stdClass(), $config);
+        $config->course = 3;
+        $config->section = 1;
+        $importinfo->set_config($config);
+        $this->assertEquals((object) ['course' => 3, 'section' => 1], $importinfo->get_config());
+    }
+
+    /**
+     * Verify the object can be stored and loaded.
+     */
+    public function test_persistence() {
+        $this->resetAfterTest();
+        [$user, $resource, $importinfo] = $this->create_test_info();
+
+        // Nothing to load initially since nothing has been saved.
+        $loadedinfo = import_info::load($importinfo->get_id());
+        $this->assertNull($loadedinfo);
+
+        // Now, save and confirm we can load the data into a new object.
+        $importinfo->save();
+        $loadedinfo2 = import_info::load($importinfo->get_id());
+        $this->assertEquals($importinfo, $loadedinfo2);
+
+        // Purge and confirm the load returns null now.
+        $importinfo->purge();
+        $loadedinfo3 = import_info::load($importinfo->get_id());
+        $this->assertNull($loadedinfo3);
+    }
+}
diff --git a/admin/tool/moodlenet/tests/import_processor_test.php b/admin/tool/moodlenet/tests/import_processor_test.php
new file mode 100644 (file)
index 0000000..ecfcec4
--- /dev/null
@@ -0,0 +1,156 @@
+<?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/>.
+
+/**
+ * Unit tests for the import_processor class.
+ *
+ * @package    tool_moodlenet
+ * @category   test
+ * @copyright  2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_moodlenet\local\tests;
+
+use tool_moodlenet\local\import_handler_registry;
+use tool_moodlenet\local\import_processor;
+use tool_moodlenet\local\import_strategy_file;
+use tool_moodlenet\local\import_strategy_link;
+use tool_moodlenet\local\remote_resource;
+use tool_moodlenet\local\url;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class tool_moodlenet_import_processor_testcase, providing test cases for the import_processor class.
+ */
+class tool_moodlenet_import_processor_testcase extends \advanced_testcase {
+
+    /**
+     * An integration test, this confirms the ability to construct an import processor and run the import for the current user.
+     */
+    public function test_process_valid_resource() {
+        $this->resetAfterTest();
+
+        // Set up a user as a teacher in a course.
+        $course = $this->getDataGenerator()->create_course();
+        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $section = 0;
+        $this->setUser($teacher);
+
+        // Set up the import, using a mod_resource handler for the html extension.
+        $resourceurl = $this->getExternalTestFileUrl('/test.html');
+        $remoteresource = new remote_resource(
+            new \curl(),
+            new url($resourceurl),
+            (object) [
+                'name' => 'Resource name',
+                'description' => 'Resource description'
+            ]
+        );
+        $handlerregistry = new import_handler_registry($course, $teacher);
+        $handlerinfo = $handlerregistry->get_resource_handler_for_mod_and_strategy($remoteresource, 'resource',
+            new import_strategy_file());
+        $importproc = new import_processor($course, $section, $remoteresource, $handlerinfo, $handlerregistry);
+
+        // Import the file.
+        $importproc->process();
+
+        // Verify there is a new mod_resource created with correct name, description and containing the test.html file.
+        $modinfo = get_fast_modinfo($course, $teacher->id);
+        $cms = $modinfo->get_instances();
+        $this->assertArrayHasKey('resource', $cms);
+        $cminfo = array_shift($cms['resource']);
+        $this->assertEquals('Resource name', $cminfo->get_formatted_name());
+        $cm = get_coursemodule_from_id('', $cminfo->id, 0, false, MUST_EXIST);
+        list($cm, $context, $module, $data, $cw) = get_moduleinfo_data($cminfo, $course);
+        $this->assertEquals($remoteresource->get_description(), $data->intro);
+        $fs = get_file_storage();
+        $files = $fs->get_area_files(\context_module::instance($cminfo->id)->id, 'mod_resource', 'content', false,
+            'sortorder DESC, id ASC', false);
+        $file = reset($files);
+        $this->assertEquals('test.html', $file->get_filename());
+        $this->assertEquals('text/html', $file->get_mimetype());
+    }
+
+    /**
+     * Test confirming that an exception is thrown when trying to process a resource which does not exist.
+     */
+    public function test_process_invalid_resource() {
+        $this->resetAfterTest();
+
+        // Set up a user as a teacher in a course.
+        $course = $this->getDataGenerator()->create_course();
+        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $section = 0;
+        $this->setUser($teacher);
+
+        // Set up the import, using a mod_resource handler for the html extension.
+        $resourceurl = $this->getExternalTestFileUrl('/test.htmlzz');
+        $remoteresource = new remote_resource(
+            new \curl(),
+            new url($resourceurl),
+            (object) [
+                'name' => 'Resource name',
+                'description' => 'Resource description'
+            ]
+        );
+        $handlerregistry = new import_handler_registry($course, $teacher);
+        $handlerinfo = $handlerregistry->get_resource_handler_for_mod_and_strategy($remoteresource, 'resource',
+            new import_strategy_file());
+        $importproc = new import_processor($course, $section, $remoteresource, $handlerinfo, $handlerregistry);
+
+        // Import the file.
+        $this->expectException(\coding_exception::class);
+        $importproc->process();
+    }
+
+    /**
+     * Test confirming that imports can be completed using alternative import strategies.
+     */
+    public function test_process_alternative_import_strategies() {
+        $this->resetAfterTest();
+
+        // Set up a user as a teacher in a course.
+        $course = $this->getDataGenerator()->create_course();
+        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $section = 0;
+        $this->setUser($teacher);
+
+        // Set up the import, using a mod_url handler and the link import strategy.
+        $remoteresource = new remote_resource(
+            new \curl(),
+            new url('http://example.com/cats.pdf'),
+            (object) [
+                'name' => 'Resource name',
+                'description' => 'Resource description'
+            ]
+        );
+        $handlerregistry = new import_handler_registry($course, $teacher);
+        $handlerinfo = $handlerregistry->get_resource_handler_for_mod_and_strategy($remoteresource, 'url',
+            new import_strategy_link());
+        $importproc = new import_processor($course, $section, $remoteresource, $handlerinfo, $handlerregistry);
+
+        // Import the resource as a link.
+        $importproc->process();
+
+        // Verify there is a new mod_url created with name 'cats' and containing the URL of the resource.
+        $modinfo = get_fast_modinfo($course, $teacher->id);
+        $cms = $modinfo->get_instances();
+        $this->assertArrayHasKey('url', $cms);
+        $cminfo = array_shift($cms['url']);
+        $this->assertEquals('Resource name', $cminfo->get_formatted_name());
+    }
+}
diff --git a/admin/tool/moodlenet/tests/lib_test.php b/admin/tool/moodlenet/tests/lib_test.php
new file mode 100644 (file)
index 0000000..d63afe2
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for tool_moodlenet lib
+ *
+ * @package    tool_moodlenet
+ * @copyright  2020 Peter Dias
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/admin/tool/moodlenet/lib.php');
+
+/**
+ * Test moodlenet functions
+ */
+class tool_moodlenet_lib_testcase extends advanced_testcase {
+
+    /**
+     * Test the generate_mnet_endpoint function
+     *
+     * @dataProvider get_endpoints_provider
+     * @param string $profileurl
+     * @param int $course
+     * @param int $section
+     * @param string $expected
+     */
+    public function test_generate_mnet_endpoint($profileurl, $course, $section, $expected) {
+        $endpoint = generate_mnet_endpoint($profileurl, $course, $section);
+        $this->assertEquals($expected, $endpoint);
+    }
+
+    /**
+     * Dataprovider for test_generate_mnet_endpoint
+     *
+     * @return array
+     */
+    public function get_endpoints_provider() {
+        global $CFG;
+        return [
+            [
+                '@name@domain.name',
+                1,
+                2,
+                'https://domain.name/' . MOODLENET_DEFAULT_ENDPOINT . '?site=' . urlencode($CFG->wwwroot)
+                    . '&course=1&section=2'
+            ],
+            [
+                '@profile@name@domain.name',
+                1,
+                2,
+                'https://domain.name/' . MOODLENET_DEFAULT_ENDPOINT . '?site=' . urlencode($CFG->wwwroot)
+                    . '&course=1&section=2'
+            ],
+            [
+                'https://domain.name',
+                1,
+                2,
+                'https://domain.name/' . MOODLENET_DEFAULT_ENDPOINT . '?site=' . urlencode($CFG->wwwroot)
+                    . '&course=1&section=2'
+            ]
+        ];
+    }
+}
diff --git a/admin/tool/moodlenet/tests/profile_manager_test.php b/admin/tool/moodlenet/tests/profile_manager_test.php
new file mode 100644 (file)
index 0000000..9485fbf
--- /dev/null
@@ -0,0 +1,142 @@
+<?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/>.
+
+/**
+ * Unit tests for the profile manager
+ *
+ * @package    tool_moodlenet
+ * @category   test
+ * @copyright  2020 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+/**
+ * Class profile_manager tests
+ */
+class tool_moodlenet_profile_manager_testcase extends advanced_testcase {
+
+    /**
+     * Test that on this site we use the user table to hold moodle net profile information.
+     */
+    public function test_official_profile_exists() {
+        $this->assertTrue(\tool_moodlenet\profile_manager::official_profile_exists());
+    }
+
+    /**
+     * Test a null is returned when the user's mnet profile field is not set.
+     */
+    public function test_get_moodlenet_user_profile_no_profile_set() {
+        $this->resetAfterTest();
+        $user = $this->getDataGenerator()->create_user();
+
+        $result = \tool_moodlenet\profile_manager::get_moodlenet_user_profile($user->id);
+        $this->assertNull($result);
+    }
+
+    /**
+     * Test a null is returned when the user's mnet profile field is not set.
+     */
+    public function test_moodlenet_user_profile_creation_no_profile_set() {
+        $this->resetAfterTest();
+        $user = $this->getDataGenerator()->create_user();
+
+        $this->expectException(moodle_exception::class);
+        $this->expectExceptionMessage(get_string('invalidmoodlenetprofile', 'tool_moodlenet'));
+        $result = new \tool_moodlenet\moodlenet_user_profile("", $user->id);
+    }
+
+    /**
+     * Test the return of a moodle net profile.
+     */
+    public function test_get_moodlenet_user_profile() {
+        $this->resetAfterTest();
+        $user = $this->getDataGenerator()->create_user(['moodlenetprofile' => '@matt@hq.mnet']);
+
+        $result = \tool_moodlenet\profile_manager::get_moodlenet_user_profile($user->id);
+        $this->assertEquals($user->moodlenetprofile, $result->get_profile_name());
+    }
+
+    /**
+     * Test the creation of a user profile category.
+     */
+    public function test_create_user_profile_category() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $basecategoryname = get_string('pluginname', 'tool_moodlenet');
+
+        \tool_moodlenet\profile_manager::create_user_profile_category();
+        $categoryname = \tool_moodlenet\profile_manager::get_category_name();
+        $this->assertEquals($basecategoryname, $categoryname);
+        \tool_moodlenet\profile_manager::create_user_profile_category();
+
+        $recordcount = $DB->count_records('user_info_category', ['name' => $basecategoryname]);
+        $this->assertEquals(1, $recordcount);
+
+        // Test the duplication of categories to ensure a unique name is always used.
+        $categoryname = \tool_moodlenet\profile_manager::get_category_name();
+        $this->assertEquals($basecategoryname . 1, $categoryname);
+        \tool_moodlenet\profile_manager::create_user_profile_category();
+        $categoryname = \tool_moodlenet\profile_manager::get_category_name();
+        $this->assertEquals($basecategoryname . 2, $categoryname);
+    }
+
+    /**
+     * Test the creating of the custom user profile field to hold the moodle net profile.
+     */
+    public function test_create_user_profile_text_field() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $shortname = 'mnetprofile';
+
+        $categoryid = \tool_moodlenet\profile_manager::create_user_profile_category();
+        \tool_moodlenet\profile_manager::create_user_profile_text_field($categoryid);
+
+        $record = $DB->get_record('user_info_field', ['shortname' => $shortname]);
+        $this->assertEquals($shortname, $record->shortname);
+        $this->assertEquals($categoryid, $record->categoryid);
+
+        // Test for a unique name if 'mnetprofile' is already in use.
+        \tool_moodlenet\profile_manager::create_user_profile_text_field($categoryid);
+        $profilename = \tool_moodlenet\profile_manager::get_profile_field_name();
+        $this->assertEquals($shortname . 1, $profilename);
+        \tool_moodlenet\profile_manager::create_user_profile_text_field($categoryid);
+        $profilename = \tool_moodlenet\profile_manager::get_profile_field_name();
+        $this->assertEquals($shortname . 2, $profilename);
+    }
+
+    /**
+     * Test that the user moodlenet profile is saved.
+     */
+    public function test_save_moodlenet_user_profile() {
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        $profilename = '@matt@hq.mnet';
+
+        $moodlenetprofile = new \tool_moodlenet\moodlenet_user_profile($profilename, $user->id);
+
+        \tool_moodlenet\profile_manager::save_moodlenet_user_profile($moodlenetprofile);
+
+        $userdata = \core_user::get_user($user->id);
+        $this->assertEquals($profilename, $userdata->moodlenetprofile);
+    }
+}
diff --git a/admin/tool/moodlenet/tests/remote_resource_test.php b/admin/tool/moodlenet/tests/remote_resource_test.php
new file mode 100644 (file)
index 0000000..2e10874
--- /dev/null
@@ -0,0 +1,115 @@
+<?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/>.
+
+/**
+ * Unit tests for the remote_resource class.
+ *
+ * @package    tool_moodlenet
+ * @category   test
+ * @copyright  2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_moodlenet\local\tests;
+
+use tool_moodlenet\local\remote_resource;
+use tool_moodlenet\local\url;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class tool_moodlenet_remote_resource_testcase, providing test cases for the remote_resource class.
+ */
+class tool_moodlenet_remote_resource_testcase extends \advanced_testcase {
+
+    /**
+     * Test getters.
+     *
+     * @dataProvider remote_resource_data_provider
+     * @param string $url the url of the resource.
+     * @param string $metadata the resource metadata like name, description, etc.
+     * @param string $expectedextension the extension we expect to find when querying the remote resource.
+     */
+    public function test_getters($url, $metadata, $expectedextension) {
+        $this->resetAfterTest();
+
+        $remoteres = new remote_resource(new \curl(), new url($url), $metadata);
+
+        $this->assertEquals(new url($url), $remoteres->get_url());
+        $this->assertEquals($metadata->name, $remoteres->get_name());
+        $this->assertEquals($metadata->description, $remoteres->get_description());
+        $this->assertEquals($expectedextension, $remoteres->get_extension());
+    }
+
+    /**
+     * Data provider generating remote urls.
+     *
+     * @return array
+     */
+    public function remote_resource_data_provider() {
+        return [
+            'With filename and extension' => [
+                $this->getExternalTestFileUrl('/test.html'),
+                (object) [
+                    'name' => 'Test html file',
+                    'description' => 'Full description of the html file'
+                ],
+                'html'
+            ],
+            'With filename only' => [
+                'http://example.com/path/file',
+                (object) [
+                    'name' => 'Test html file',
+                    'description' => 'Full description of the html file'
+                ],
+                ''
+            ]
+        ];
+    }
+
+    /**
+     * Test confirming the network based operations of a remote_resource.
+     */
+    public function test_network_features() {
+        $url = $this->getExternalTestFileUrl('/test.html');
+        $nonexistenturl = $this->getExternalTestFileUrl('/test.htmlzz');
+
+        $remoteres = new remote_resource(
+            new \curl(),
+            new url($url),
+            (object) [
+                'name' => 'Test html file',
+                'description' => 'Some description'
+            ]
+        );
+        $nonexistentremoteres = new remote_resource(
+            new \curl(),
+            new url($nonexistenturl),
+            (object) [
+                'name' => 'Test html file',
+                'description' => 'Some description'
+            ]
+        );
+
+        $this->assertGreaterThan(0, $remoteres->get_download_size());
+        [$path, $name] = $remoteres->download_to_requestdir();
+        $this->assertIsString($path);
+        $this->assertEquals('test.html', $name);
+        $this->assertFileExists($path . '/' . $name);
+
+        $this->expectException(\coding_exception::class);
+        $nonexistentremoteres->get_download_size();
+    }
+}
diff --git a/admin/tool/moodlenet/tests/url_test.php b/admin/tool/moodlenet/tests/url_test.php
new file mode 100644 (file)
index 0000000..74a7349
--- /dev/null
@@ -0,0 +1,103 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for the url class.
+ *
+ * @package    tool_moodlenet
+ * @category   test
+ * @copyright  2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_moodlenet\local\tests;
+
+use tool_moodlenet\local\url;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class tool_moodlenet_url_testcase, providing test cases for the url class.
+ */
+class tool_moodlenet_url_testcase extends \advanced_testcase {
+
+    /**
+     * Test the parsing to host + path components.
+     *
+     * @dataProvider url_provider
+     * @param string $urlstring The full URL string
+     * @param string $host the expected host component of the URL.
+     * @param string $path the expected path component of the URL.
+     * @param bool $exception whether or not an exception is expected during construction.
+     */
+    public function test_parsing($urlstring, $host, $path, $exception) {
+        if ($exception) {
+            $this->expectException(\coding_exception::class);
+            $url = new url($urlstring);
+            return;
+        }
+
+        $url = new url($urlstring);
+        $this->assertEquals($urlstring, $url->get_value());
+        $this->assertEquals($host, $url->get_host());
+        $this->assertEquals($path, $url->get_path());
+    }
+
+    /**
+     * Data provider.
+     *
+     * @return array
+     */
+    public function url_provider() {
+        return [
+            'No path' => [
+                'url' => 'https://example.moodle.net',
+                'host' => 'example.moodle.net',
+                'path' => null,
+                'exception' => false,
+            ],
+            'Slash path' => [
+                'url' => 'https://example.moodle.net/',
+                'host' => 'example.moodle.net',
+                'path' => '/',
+                'exception' => false,
+            ],
+            'Path includes file and extension' => [
+                'url' => 'https://example.moodle.net/uploads/123456789/pic.png',
+                'host' => 'example.moodle.net',
+                'path' => '/uploads/123456789/pic.png',
+                'exception' => false,
+            ],
+            'Path includes file, extension and params' => [
+                'url' => 'https://example.moodle.net/uploads/123456789/pic.png?option=1&option2=test',
+                'host' => 'example.moodle.net',
+                'path' => '/uploads/123456789/pic.png',
+                'exception' => false,
+            ],
+            'Malformed - invalid' => [
+                'url' => 'invalid',
+                'host' => null,
+                'path' => null,
+                'exception' => true,
+            ],
+            'Direct, non-encoded utf8 - invalid' => [
+                'url' => 'http://москва.рф/services/',
+                'host' => 'москва.рф',
+                'path' => '/services/',
+                'exception' => true,
+            ],
+        ];
+    }
+}
diff --git a/admin/tool/moodlenet/version.php b/admin/tool/moodlenet/version.php
new file mode 100644 (file)
index 0000000..d6a7b6e
--- /dev/null
@@ -0,0 +1,30 @@
+<?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 file for tool_moodlenet
+ *
+ * @package     tool_moodlenet
+ * @copyright   2020 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->component  = 'tool_moodlenet';
+$plugin->version    = 2020060500;
+$plugin->requires   = 2020022800.01;
+$plugin->maturity   = MATURITY_ALPHA;
index 7520aa3..49bd8a1 100644 (file)
@@ -175,6 +175,20 @@ abstract class restore_qtype_plugin extends restore_plugin {
                 }
             }
 
+            $rules = restore_course_task::define_decode_rules();
+            $rulesactivity = restore_quiz_activity_task::define_decode_rules();
+            $rules = array_merge($rules, $rulesactivity);
+
+            $decoder = $this->task->get_decoder();
+            foreach ($rules as $rule) {
+                $decoder->add_rule($rule);
+            }
+
+            $contentdecoded = $decoder->decode_content($data->answertext);
+            if ($contentdecoded) {
+                $data->answertext = $contentdecoded;
+            }
+
             if (!isset($this->questionanswercache[$data->answertext])) {
                 // If we haven't found the matching answer, something has gone really wrong, the question in the DB
                 // is missing answers, throw an exception.
diff --git a/backup/tests/quiz_restore_decode_links_test.php b/backup/tests/quiz_restore_decode_links_test.php
new file mode 100644 (file)
index 0000000..19a142d
--- /dev/null
@@ -0,0 +1,97 @@
+<?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/>.
+
+/**
+ * Decode links quiz restore tests.
+ *
+ * @package    core_backup
+ * @copyright  2020 Ilya Tregubov <mattp@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+// Include all the needed stuff.
+global $CFG;
+require_once($CFG->dirroot . '/course/lib.php');
+require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
+
+/**
+ * restore_decode tests (both rule and content)
+ */
+class restore_quiz_decode_testcase extends \core_privacy\tests\provider_testcase {
+
+    /**
+     * Test restore_decode_rule class
+     */
+    public function test_restore_quiz_decode_links() {
+        global $DB, $CFG, $USER;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course(
+            array('format' => 'topics', 'numsections' => 3,
+                'enablecompletion' => COMPLETION_ENABLED),
+            array('createsections' => true));
+        $quiz = $generator->create_module('quiz', array(
+            'course' => $course->id));
+
+        // Create questions.
+
+        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
+        $context = context_course::instance($course->id);
+        $cat = $questiongenerator->create_question_category(array('contextid' => $context->id));
+        $question = $questiongenerator->create_question('multichoice', null, array('category' => $cat->id));
+
+        // Add to the quiz.
+        quiz_add_quiz_question($question->id, $quiz);
+
+        $questiondata = question_bank::load_question_data($question->id);
+
+        $firstanswer = array_shift($questiondata->options->answers);
+        $DB->set_field('question_answers', 'answer', $CFG->wwwroot . '/course/view.php?id=' . $course->id,
+            ['id' => $firstanswer->id]);
+
+        $secondanswer = array_shift($questiondata->options->answers);
+        $DB->set_field('question_answers', 'answer', $CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->cmid,
+            ['id' => $secondanswer->id]);
+
+        $thirdanswer = array_shift($questiondata->options->answers);
+        $DB->set_field('question_answers', 'answer', $CFG->wwwroot . '/grade/report/index.php?id=' . $quiz->cmid,
+            ['id' => $thirdanswer->id]);
+
+        $fourthanswer = array_shift($questiondata->options->answers);
+        $DB->set_field('question_answers', 'answer', $CFG->wwwroot . '/mod/quiz/index.php?id=' . $quiz->cmid,
+            ['id' => $fourthanswer->id]);
+
+        $newcm = duplicate_module($course, get_fast_modinfo($course)->get_cm($quiz->cmid));
+
+        $sql = "SELECT qa.answer
+                  FROM {quiz} q
+             LEFT JOIN {quiz_slots} qs ON qs.quizid = q.id
+             LEFT JOIN {question_answers} qa ON qa.question = qs.questionid
+                 WHERE q.id = :quizid";
+        $params = array('quizid' => $newcm->instance);
+        $answers = $DB->get_fieldset_sql($sql, $params);
+        $this->assertEquals($CFG->wwwroot . '/course/view.php?id=' . $course->id, $answers[0]);
+        $this->assertEquals($CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->cmid, $answers[1]);
+        $this->assertEquals($CFG->wwwroot . '/grade/report/index.php?id=' . $quiz->cmid, $answers[2]);
+        $this->assertEquals($CFG->wwwroot . '/mod/quiz/index.php?id=' . $quiz->cmid, $answers[3]);
+    }
+}
index 2307d0d..d52a7c8 100644 (file)
@@ -100,9 +100,11 @@ if ($form->is_cancelled()) {
         $badge->imageauthorurl = $data->imageauthorurl;
         $badge->imagecaption = $data->imagecaption;
         $badge->usermodified = $USER->id;
-        $badge->issuername = $data->issuername;
-        $badge->issuerurl = $data->issuerurl;
-        $badge->issuercontact = $data->issuercontact;
+        if (badges_open_badges_backpack_api() == OPEN_BADGES_V1) {
+            $badge->issuername = $data->issuername;
+            $badge->issuerurl = $data->issuerurl;
+            $badge->issuercontact = $data->issuercontact;
+        }
         $badge->expiredate = ($data->expiry == 1) ? $data->expiredate : null;
         $badge->expireperiod = ($data->expiry == 2) ? $data->expireperiod : null;
 
index 09db662..bc7695d 100644 (file)
@@ -166,3 +166,32 @@ Feature: Add badges to the system
     And I click on "Site badges" "link" in the "Navigation" "block"
     Then I should see "Manage badges"
     And I should see "Add a new badge"
+
+  @javascript @_file_upload
+  Scenario: Edit a badge
+    Given I navigate to "Badges > Badges settings" in site administration
+    And I set the field "Badge issuer name" to "Test Badge Site"
+    And I set the field "Badge issuer email address" to "testuser@example.com"
+    And I press "Save changes"
+    And I navigate to "Badges > Add a new badge" in site administration
+    And I set the following fields to these values:
+      | Name | Test badge with 'apostrophe' and other friends (<>&@#) |
+      | Version | firstversion |
+      | Language | English |
+      | Description | Test badge description |
+      | Image author | http://author.example.com |
+      | Image caption | Test caption image |
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
+    And I press "Create badge"
+    When I follow "Edit details"
+    And I should see "Test badge with 'apostrophe' and other friends (&@#)"
+    And I should not see "Issuer details"
+    And I set the following fields to these values:
+      | Name | Test badge renamed |
+      | Version | secondversion |
+    And I press "Save changes"
+    And I follow "Overview"
+    Then I should not see "Test badge with 'apostrophe' and other friends (&@#)"
+    And I should not see "firstversion"
+    And I should see "Test badge renamed"
+    And I should see "secondversion"
index bd815e6..ee1b61b 100644 (file)
@@ -267,9 +267,11 @@ class core_block_externallib_testcase extends externallib_advanced_testcase {
             $returnedblocks[] = $block['name'];
             // Check the configuration returned for this default block.
             if ($block['name'] == 'recentlyaccessedcourses') {
-                $this->assertEquals('displaycategories', $block['configs'][0]['name']);
-                $this->assertEquals(json_encode('0'), $block['configs'][0]['value']);
-                $this->assertEquals('plugin', $block['configs'][0]['type']);
+                // Convert config to associative array to avoid DB sorting randomness.
+                $config = array_column($block['configs'], null, 'name');
+                $this->assertArrayHasKey('displaycategories', $config);
+                $this->assertEquals(json_encode('0'), $config['displaycategories']['value']);
+                $this->assertEquals('plugin', $config['displaycategories']['type']);
             }
         }
         // Remove lp block.
index ccc760a..995e0df 100644 (file)
@@ -1055,6 +1055,15 @@ $CFG->admin = 'admin';
 //      $CFG->alternative_file_system_class = '\\local_myfilestorage\\file_system';
 //
 //=========================================================================
+// 15. CAMPAIGN CONTENT
+//=========================================================================
+//
+// We have added a campaign content to the notifications page, in case you want to hide that from your site you just
+// need to set showcampaigncontent setting to false.
+//
+//      $CFG->showcampaigncontent = true;
+//
+//=========================================================================
 // ALL DONE!  To continue installation, visit your main page with a browser
 //=========================================================================
 
index 5d0babf..bd655b7 100644 (file)
Binary files a/contentbank/amd/build/sort.min.js and b/contentbank/amd/build/sort.min.js differ
index 4609a9e..663a438 100644 (file)
Binary files a/contentbank/amd/build/sort.min.js.map and b/contentbank/amd/build/sort.min.js.map differ
index bfa61c8..24f4f79 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-import selectors from 'core_contentbank/selectors';
+import selectors from './selectors';
 import {get_string as getString} from 'core/str';
 import Prefetch from 'core/prefetch';
+import Ajax from 'core/ajax';
+import Notification from 'core/notification';
 
 /**
  * Set up the contentbank views.
@@ -59,6 +61,7 @@ const registerListenerEvents = (contentBank) => {
         contentBank.classList.add('view-grid');
         viewGrid.classList.add('active');
         viewList.classList.remove('active');
+        setViewListPreference(false);
     });
 
     viewList.addEventListener('click', () => {
@@ -66,6 +69,7 @@ const registerListenerEvents = (contentBank) => {
         contentBank.classList.add('view-list');
         viewList.classList.add('active');
         viewGrid.classList.remove('active');
+        setViewListPreference(true);
     });
 
     // Sort by file name alphabetical
@@ -97,6 +101,35 @@ const registerListenerEvents = (contentBank) => {
     });
 };
 
+
+/**
+ * Set the contentbank user preference in list view
+ *
+ * @param  {Bool} viewList view ContentBank as list.
+ * @return {Promise} Repository promise.
+ */
+const setViewListPreference = function(viewList) {
+
+    // If the given status is not hidden, the preference has to be deleted with a null value.
+    if (viewList === false) {
+        viewList = null;
+    }
+
+    const request = {
+        methodname: 'core_user_update_user_preferences',
+        args: {
+            preferences: [
+                {
+                    type: 'core_contentbank_view_list',
+                    value: viewList
+                }
+            ]
+        }
+    };
+
+    return Ajax.call([request])[0].catch(Notification.exception);
+};
+
 /**
  * Update the sort button view.
  *
index b851222..5549392 100644 (file)
@@ -97,11 +97,12 @@ class bankcontent implements renderable, templatable {
                 'type' => $mimetype
             );
         }
+        $data->viewlist = get_user_preferences('core_contentbank_view_list');
         $data->contents = $contentdata;
         // The tools are displayed in the action bar on the index page.
         foreach ($this->toolbar as $tool) {
             // Customize the output of a tool, like dropdowns.
-            $method = 'export_tool_'.$tool['name'];
+            $method = 'export_tool_'.$tool['action'];
             if (method_exists($this, $method)) {
                 $this->$method($tool);
             }
index d654ab3..c0c49c5 100644 (file)
@@ -44,7 +44,8 @@ use context_course;
 class provider implements
     \core_privacy\local\metadata\provider,
     \core_privacy\local\request\core_userlist_provider,
-    \core_privacy\local\request\plugin\provider {
+    \core_privacy\local\request\plugin\provider,
+    \core_privacy\local\request\user_preference_provider {
 
     /**
      * Returns meta data about this system.
@@ -65,6 +66,26 @@ class provider implements
         return $collection;
     }
 
+    /**
+     * Export all user preferences for the contentbank
+     *
+     * @param int $userid The userid of the user whose data is to be exported.
+     */
+    public static function export_user_preferences(int $userid) {
+        $preference = get_user_preferences('core_contentbank_view_list', null, $userid);
+        if (isset($preference)) {
+            writer::export_user_preference(
+                    'core_contentbank',
+                    'core_contentbank_view_list',
+                    $preference,
+                    get_string('privacy:request:preference:set', 'core_contentbank', (object) [
+                            'name' => 'core_contentbank_view_list',
+                            'value' => $preference,
+                    ])
+            );
+        }
+    }
+
     /**
      * Get the list of contexts that contain user information for the specified user.
      *
index b7229b9..2db4714 100644 (file)
@@ -30,6 +30,7 @@ use core_contentbank\form\edit_content;
 use core_h5p\api;
 use core_h5p\editor as h5peditor;
 use core_h5p\factory;
+use core_h5p\helper;
 use stdClass;
 
 /**
@@ -53,6 +54,8 @@ class editor extends edit_content {
         global $DB;
 
         $mform = $this->_form;
+        $errors = [];
+        $notifications = [];
 
         // Id of the content to edit.
         $id = $this->_customdata['id'];
@@ -73,9 +76,22 @@ class editor extends edit_content {
             $file = $this->content->get_file();
 
             $h5p = api::get_content_from_pathnamehash($file->get_pathnamehash());
-            $mform->addElement('hidden', 'h5pid', $h5p->id);
-            $mform->setType('h5pid', PARAM_INT);
-            $this->h5peditor->set_content($h5p->id);
+            if (!$h5p) {
+                // H5P content has not been deployed yet. Let's check why.
+                $factory = new \core_h5p\factory();
+                $factory->get_framework()->set_file($file);
+
+                $h5pid = helper::save_h5p($factory, $file, new stdClass());
+                $errors = $factory->get_framework()->getMessages('error');
+                $notifications = $factory->get_framework()->getMessages('info');
+            } else {
+                $h5pid = $h5p->id;
+            }
+            if ($h5pid) {
+                $mform->addElement('hidden', 'h5pid', $h5pid);
+                $mform->setType('h5pid', PARAM_INT);
+                $this->h5peditor->set_content($h5pid);
+            }
         } else {
             // The H5P editor needs the H5P content type library name for a new content.
             $mform->addElement('hidden', 'library', $library);
@@ -86,11 +102,20 @@ class editor extends edit_content {
         $mformid = 'coolh5peditor';
         $mform->setAttributes(array('id' => $mformid) + $mform->getAttributes());
 
-        $this->add_action_buttons();
-
-        $this->h5peditor->add_editor_to_form($mform);
-
-        $this->add_action_buttons();
+        if ($errors || $notifications) {
+            // Show the error messages and a Cancel button.
+            foreach ($errors as $error) {
+                $mform->addElement('warning', $error->code, 'notify', $error->message);
+            }
+            foreach ($notifications as $key => $notification) {
+                $mform->addElement('warning', 'notification_'.$key, 'notify', $notification);
+            }
+            $mform->addElement('cancel', 'cancel', get_string('back'));
+        } else {
+            $this->add_action_buttons();
+            $this->h5peditor->add_editor_to_form($mform);
+            $this->add_action_buttons();
+        }
     }
 
     /**
index 8639d35..5e3bcbe 100644 (file)
@@ -70,3 +70,23 @@ Feature: H5P file upload to content bank for admins
     And I expand "Site pages" node
     And I click on "Content bank" "link"
     And I should not see "filltheblanks.h5p"
+
+  Scenario: Admins can upload and deployed content types when libraries are not installed
+    Given I navigate to "H5P > Manage H5P content types" in site administration
+    And I should not see "Fill in the Blanks"
+    And I follow "Dashboard" in the user menu
+    And I expand "Site pages" node
+    And I click on "Content bank" "link"
+    And 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 switch to "h5p-player" class iframe
+    And I switch to "h5p-iframe" class iframe
+    Then I should see "Of which countries"
+    And I switch to the main frame
+    And I navigate to "H5P > Manage H5P content types" in site administration
+    And I should see "Fill in the Blanks"
index 9c25ac9..48f6e22 100644 (file)
@@ -71,3 +71,75 @@ Feature: H5P file upload to content bank for non admins
     And I expand "Site pages" node
     And I click on "Content bank" "link"
     Then I should see "filltheblanks.h5p"
+
+  Scenario: Teachers can not upload and deployed content types when libraries are not installed
+    Given I log out
+    And I log in as "admin"
+    And I navigate to "H5P > Manage H5P content types" in site administration
+    And I should not see "Fill in the Blanks"
+    And I log out
+    And I log in as "teacher1"
+    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"
+    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 switch to "h5p-player" class iframe
+    Then I should not see "Of which countries"
+    And I should see "missing-required-library"
+    And I switch to the main frame
+    And I log out
+    And I log in as "admin"
+    And I navigate to "H5P > Manage H5P content types" in site administration
+    And I should not see "Fill in the Blanks"
+
+  Scenario: Teachers can not see existing contents when libraries are not installed
+    Given I log out
+    And 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 navigate to "H5P > Manage H5P content types" in site administration
+    And I should not see "Fill in the Blanks"
+    When I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "H5P content type" filemanager
+    And I click on "Upload H5P content types" "button" in the "#fitem_id_uploadlibraries" "css_element"
+    And I wait until the page is ready
+    And I should see "Fill in the Blanks"
+    And I log out
+    And I log in as "teacher1"
+    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 switch to "h5p-player" class iframe
+    And I switch to "h5p-iframe" class iframe
+    Then I should see "Of which countries"
+    Then I should not see "missing-required-library"
+    And I switch to the main frame
+    Given I log out
+    And I log in as "admin"
+    And I navigate to "H5P > Manage H5P content types" in site administration
+    When I click on "Delete version" "link" in the "Fill in the Blanks" "table_row"
+    And I press "Continue"
+    Then I should not see "Fill in the Blanks"
+    And I log out
+    And I log in as "teacher1"
+    Given I am on "Course 1" course homepage
+    When I expand "Site pages" node
+    And I click on "Content bank" "link"
+    And I should see "filltheblanks.h5p"
+    And I click on "filltheblanks.h5p" "link"
+    And I switch to "h5p-player" class iframe
+    Then I should not see "Of which countries"
+    Then I should see "missing-required-library"
index e4bb6d6..5608a9a 100644 (file)
@@ -70,7 +70,12 @@ if (has_capability('moodle/contentbank:useeditor', $context)) {
     if (!empty($editabletypes)) {
         // Editor base URL.
         $editbaseurl = new moodle_url('/contentbank/edit.php', ['contextid' => $contextid]);
-        $toolbar[] = ['name' => get_string('add'), 'link' => $editbaseurl, 'dropdown' => true, 'contenttypes' => $editabletypes];
+        $toolbar[] = [
+            'name' => get_string('add'),
+            'link' => $editbaseurl, 'dropdown' => true,
+            'contenttypes' => $editabletypes,
+            'action' => 'add'
+        ];
     }
 }
 
@@ -80,7 +85,12 @@ if (has_capability('moodle/contentbank:upload', $context)) {
     $accepted = $cb->get_supported_extensions_as_string($context);
     if (!empty($accepted)) {
         $importurl = new moodle_url('/contentbank/upload.php', ['contextid' => $contextid]);
-        $toolbar[] = array('name' => get_string('upload', 'contentbank'), 'link' => $importurl, 'icon' => 'i/upload');
+        $toolbar[] = [
+            'name' => get_string('upload', 'contentbank'),
+            'link' => $importurl,
+            'icon' => 'i/upload',
+            'action' => 'upload'
+        ];
     }
 }
 
diff --git a/contentbank/lib.php b/contentbank/lib.php
new file mode 100644 (file)
index 0000000..e709df7
--- /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/>.
+
+/**
+ * Library functions for contentbank
+ *
+ * @package   core_contentbank
+ * @copyright 2020 Bas Brands
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Get the current user preferences that are available
+ *
+ * @return Array preferences configuration
+ */
+function core_contentbank_user_preferences() {
+    return [
+        'core_contentbank_view_list' => [
+            'choices' => array(0, 1),
+            'type' => PARAM_INT,
+            'null' => NULL_NOT_ALLOWED,
+            'default' => 'none'
+        ],
+    ];
+}
index 020b905..4801a7f 100644 (file)
@@ -74,7 +74,8 @@
     }
 
 }}
-<div class="content-bank-container view-grid" data-region="contentbank">
+<div class="content-bank-container {{#viewlist}}view-list{{/viewlist}} {{^viewlist}}view-grid{{/viewlist}}"
+data-region="contentbank">
     <div class="d-flex justify-content-between flex-column flex-sm-row">
         <div class="cb-search-container mb-2">
             {{>core_contentbank/bankcontent/search}}
                         <div class="cb-thumbnail" role="img" aria-label="{{{ name }}}"
                         style="background-image: url('{{{ icon }}}');">
                         </div>
-                        <a href="{{{ link }}}" class="cb-link stretched-link">
+                        <a href="{{{ link }}}" class="cb-link stretched-link" title="{{{ name }}}">
                             <span class="cb-name word-break-all clamp-2" data-region="cb-content-name">
                                 {{{ name }}}
                             </span>
index 242fa1a..4d590ce 100644 (file)
         </a>
     {{/dropdown}}
 {{/tools}}
-<button class="icon-no-margin btn btn-secondary active ml-2"
+<button class="icon-no-margin btn btn-secondary {{^viewlist}}active{{/viewlist}} ml-2"
 title="{{#str}}  displayicons, contentbank  {{/str}}"
 data-action="viewgrid">
     {{#pix}}a/view_icon_active, core, {{#str}} displayicons, contentbank {{/str}} {{/pix}}
 </button>
-<button class="icon-no-margin btn btn-secondary"
+<button class="icon-no-margin btn btn-secondary {{#viewlist}}active{{/viewlist}}"
 title="{{#str}} displaydetails, contentbank {{/str}}"
 data-action="viewlist">
     {{#pix}}t/viewdetails, core, {{#str}} displaydetails, contentbank {{/str}} {{/pix}}
index 7a2fbf5..f49dbd6 100644 (file)
                 {{/typeeditorparams}}
                 {{#typeeditorparams}}
                     <a class="dropdown-item icon-size-4" href="{{{ baseurl }}}&{{{ typeeditorparams }}}">
-                        <img alt="" class="icon" src="{{{ typeicon }}}"> {{ typename }}
+                        {{#typeicon}}
+                            <img alt="" class="icon" src="{{{ typeicon }}}">
+                        {{/typeicon}}
+                        {{^typeicon}}
+                            {{#pix}} b/h5p_library, core {{/pix}}
+                        {{/typeicon}} {{ typename }}
                     </a>
                 {{/typeeditorparams}}
             {{/types}}
index 713768c..aef6eab 100644 (file)
@@ -97,3 +97,21 @@ Feature: Content bank use editor feature
       | moodle/contentbank:useeditor     | Prohibit   | editingteacher | System       |           |
     And I reload the page
     Then "[data-action=Add-content]" "css_element" should not exist
+
+  Scenario: Users can edit content and save changes
+    Given the following "contentbank content" exist:
+      | contextlevel | reference | contenttype     | user  | contentname             | filepath                                    |
+      | System       |           | contenttype_h5p | admin | filltheblanks.h5p       | /h5p/tests/fixtures/filltheblanks.h5p       |
+    And I click on "Site pages" "list_item" in the "Navigation" "block"
+    And I click on "Content bank" "link" in the "Navigation" "block"
+    And I click on "filltheblanks.h5p" "link"
+    And I click on "Edit" "link"
+    And I switch to "h5p-editor-iframe" class iframe
+    And the field "Title" matches value "Geography"
+    And I set the field "Title" to "New title"
+    And I switch to the main frame
+    When I click on "Save" "button"
+    And I should see "filltheblanks.h5p" in the "h1" "css_element"
+    And I click on "Edit" "link"
+    And I switch to "h5p-editor-iframe" class iframe
+    Then the field "Title" matches value "New title"
diff --git a/contentbank/tests/behat/view_preferences.feature b/contentbank/tests/behat/view_preferences.feature
new file mode 100644 (file)
index 0000000..44fa8bf
--- /dev/null
@@ -0,0 +1,28 @@
+@core @core_contentbank @contentbank_h5p @javascript
+Feature: Store the content bank view preference
+  In order to consistantly view the content bank in icons or details view
+  As an admin
+  I need to be able to store my view preference
+
+  Background:
+    Given the following "contentbank content" exist:
+        | contextlevel | reference | contenttype       | user  | contentname          |
+        | System       |           | contenttype_h5p   | admin | filltheblanks.h5p    |
+        | System       |           | contenttype_h5p   | admin | mathsbook.h5p        |
+
+  Scenario: Admins can order content in the content bank
+    Given I log in as "admin"
+    And I am on site homepage
+    And I turn 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"
+    When I click on "Display content bank with file details" "button"
+    And I should see "Last modified"
+    And I follow "filltheblanks.h5p"
+    And I click on "Content bank" "link"
+    And I should see "Last modified"
+    And I click on "Display content bank with icons" "button"
+    And I follow "filltheblanks.h5p"
+    And I click on "Content bank" "link"
+    And I should not see "Last modified"
index e737117..3717d40 100644 (file)
@@ -29,6 +29,7 @@ use stdClass;
 use context_system;
 use context_coursecat;
 use context_course;
+use context_user;
 use core_contentbank\privacy\provider;
 use core_privacy\local\request\approved_contextlist;
 use core_privacy\local\request\writer;
@@ -361,4 +362,50 @@ class core_contentbank_privacy_testcase extends provider_testcase {
 
         return $scenario;
     }
+
+    /**
+     * Ensure that export_user_preferences returns no data if the user has not visited any content bank.
+     */
+    public function test_export_user_preferences_no_pref() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $user = $this->getDataGenerator()->create_user();
+        $managerroleid = $DB->get_field('role', 'id', ['shortname' => 'manager']);
+        $this->getDataGenerator()->role_assign($managerroleid, $user->id);
+
+        provider::export_user_preferences($user->id);
+        $writer = writer::with_context(context_system::instance());
+        $this->assertFalse($writer->has_any_data());
+    }
+
+    /**
+     * Test for provider::test_export_user_preferences().
+     */
+    public function test_export_user_preferences() {
+        global $DB;
+
+        // Test setup.
+        $this->resetAfterTest(true);
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        set_user_preference('core_contentbank_view_list', 1);
+        // Test the user preferences export contains 1 user preference record for the User.
+        provider::export_user_preferences($user->id);
+        $contextuser = context_user::instance($user->id);
+        $writer = writer::with_context($contextuser);
+        $this->assertTrue($writer->has_any_data());
+
+        $prefs = $writer->get_user_preferences('core_contentbank');
+        $this->assertCount(1, (array) $prefs);
+        $this->assertEquals(1, $prefs->core_contentbank_view_list->value);
+        $this->assertEquals(
+                get_string('privacy:request:preference:set', 'core_contentbank', (object) [
+                        'name' => 'core_contentbank_view_list',
+                        'value' => $prefs->core_contentbank_view_list->value,
+                ]),
+                $prefs->core_contentbank_view_list->description
+        );
+    }
 }
index da936c1..27aebcf 100644 (file)
Binary files a/course/amd/build/activitychooser.min.js and b/course/amd/build/activitychooser.min.js differ
index 83b317b..055d5ac 100644 (file)
Binary files a/course/amd/build/activitychooser.min.js.map and b/course/amd/build/activitychooser.min.js.map differ
index dde9eec..dbb33f4 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js and b/course/amd/build/local/activitychooser/dialogue.min.js differ
index 6f8a885..5c2bb8f 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js.map and b/course/amd/build/local/activitychooser/dialogue.min.js.map differ
index e7e7505..44e5ee1 100644 (file)
Binary files a/course/amd/build/local/activitychooser/repository.min.js and b/course/amd/build/local/activitychooser/repository.min.js differ
index e7bb504..0469db6 100644 (file)
Binary files a/course/amd/build/local/activitychooser/repository.min.js.map and b/course/amd/build/local/activitychooser/repository.min.js.map differ
index 0aa10d5..176ed99 100644 (file)
@@ -85,19 +85,43 @@ const registerListenerEvents = (courseId, chooserConfig) => {
         };
     })();
 
+    const fetchFooterData = (() => {
+        let footerInnerPromise = null;
+
+        return (sectionId) => {
+            if (!footerInnerPromise) {
+                footerInnerPromise = new Promise((resolve) => {
+                    resolve(Repository.fetchFooterData(courseId, sectionId));
+                });
+            }
+
+            return footerInnerPromise;
+        };
+    })();
+
     CustomEvents.define(document, events);
 
     // Display module chooser event listeners.
     events.forEach((event) => {
         document.addEventListener(event, async(e) => {
             if (e.target.closest(selectors.elements.sectionmodchooser)) {
+                let caller;
                 // We need to know who called this.
                 // Standard courses use the ID in the main section info.
                 const sectionDiv = e.target.closest(selectors.elements.section);
                 // Front page courses need some special handling.
                 const button = e.target.closest(selectors.elements.sectionmodchooser);
+
                 // If we don't have a section ID use the fallback ID.
-                const caller = sectionDiv || button;
+                // We always want the sectionDiv caller first as it keeps track of section ID's after DnD changes.
+                // The button attribute is always just a fallback for us as the section div is not always available.
+                // A YUI change could be done maybe to only update the button attribute but we are going for minimal change here.
+                if (sectionDiv !== null && sectionDiv.hasAttribute('data-sectionid')) {
+                    // We check for attributes just in case of outdated contrib course formats.
+                    caller = sectionDiv;
+                } else {
+                    caller = button;
+                }
 
                 // We want to show the modal instantly but loading whilst waiting for our data.
                 let bodyPromiseResolver;
@@ -105,7 +129,8 @@ const registerListenerEvents = (courseId, chooserConfig) => {
                     bodyPromiseResolver = resolve;
                 });
 
-                const sectionModal = buildModal(bodyPromise);
+                const footerData = await fetchFooterData(caller.dataset.sectionid);
+                const sectionModal = buildModal(bodyPromise, footerData);
 
                 // Now we have a modal we should start fetching data.
                 const data = await fetchModuleData();
@@