MDL-67883 tool_moodlenet: Add MoodleNet to core
authorJake Dallimore <jake@moodle.com>
Fri, 5 Jun 2020 02:09:37 +0000 (10:09 +0800)
committerMathew May <mathewm@hotmail.co.nz>
Fri, 5 Jun 2020 03:47:42 +0000 (11:47 +0800)
61 files changed:
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 [moved from pix/MoodleNet.png with 100% similarity]
admin/tool/moodlenet/pix/MoodleNet.svg [moved from pix/MoodleNet.svg with 100% similarity]
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]
course/templates/local/activitychooser/help.mustache
theme/boost/scss/moodle/core.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css

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/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 9ddacd4..471dffc 100644 (file)
@@ -45,7 +45,7 @@
         </div>
     </div>
     {{^showFooter}}
-        <div class="fixed-bottom position-absolute py-3 px-4 border-top">
+        <div class="fixed-bottom position-absolute py-3 px-4 border-top modal-footer">
             {{>core_course/local/activitychooser/footer_partial}}
         </div>
     {{/showFooter}}
index 411f762..32a17d6 100644 (file)
@@ -1639,6 +1639,7 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
 
 .modchooser .modal-footer {
     height: 70px;
+    background: $modal-content-bg;
     .moodlenet-logo {
         .icon {
             height: 2.5rem;
index b2d48fe..f0ec236 100644 (file)
@@ -11002,7 +11002,8 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
     margin: 1em auto; }
 
 .modchooser .modal-footer {
-  height: 70px; }
+  height: 70px;
+  background: #fff; }
   .modchooser .modal-footer .moodlenet-logo .icon {
     height: 2.5rem;
     width: 6rem;
index e2c098d..316f966 100644 (file)
@@ -11209,7 +11209,8 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
     margin: 1em auto; }
 
 .modchooser .modal-footer {
-  height: 70px; }
+  height: 70px;
+  background: #fff; }
   .modchooser .modal-footer .moodlenet-logo .icon {
     height: 2.5rem;
     width: 6rem;