Merge branch 'MDL-75316-master-4' of https://github.com/mickhawkins/moodle

This commit is contained in:
Jake Dallimore 2023-04-18 17:24:55 +08:00
commit c9e8aa0406
60 changed files with 3330 additions and 12 deletions

View File

@ -0,0 +1,57 @@
<?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/>.
/**
* MoodleNet callback.
*
* @package core
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core\moodlenet\moodlenet_client;
use core\oauth2\api;
require_once(__DIR__ . '/../config.php');
require_login();
// Parameters.
$issuerid = required_param('issuerid', PARAM_INT);
$error = optional_param('error', '', PARAM_RAW);
$message = optional_param('error_description', null, PARAM_RAW);
// Headers to make it not cacheable.
header('Cache-Control: no-cache, must-revalidate');
header('Expires: Sat, 26 Jul 1997 05:00:00 GMT');
$PAGE->set_context(context_system::instance());
$PAGE->set_url('/admin/moodlenet_oauth2_callback.php');
$PAGE->set_pagelayout('popup');
// Wait as long as it takes for this script to finish.
core_php_time_limit::raise();
$issuer = api::get_issuer($issuerid);
$returnurl = new moodle_url('/admin/moodlenet_oauth2_callback.php');
$returnurl->param('issuerid', $issuerid);
$returnurl->param('callback', 'yes');
$returnurl->param('sesskey', sesskey());
$oauthclient = api::get_user_oauth_client($issuer, $returnurl, moodlenet_client::API_SCOPE_CREATE_RESOURCE, true);
$oauthclient->is_logged_in(); // Will upgrade the auth code to a token.
echo $OUTPUT->header();
$PAGE->requires->js_call_amd('core/moodlenet/oauth2callback', 'init', [$error, $message]);
echo $OUTPUT->footer();

View File

@ -33,6 +33,11 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
new lang_string('enablecourserelativedates', 'core_admin'),
new lang_string('enablecourserelativedates_desc', 'core_admin'), 0));
// Sharing to MoodleNet setting.
$temp->add(new admin_setting_configcheckbox('enablesharingtomoodlenet',
new lang_string('enablesharingtomoodlenet', 'core_admin'),
new lang_string('enablesharingtomoodlenet_desc', 'core_admin'), 0));
$ADMIN->add('experimental', $temp);
// "debugging" settingpage

View File

@ -0,0 +1,55 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file gives information about MoodleNet.
*
* @package core
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
if ($hassiteconfig) {
if (!empty($CFG->enablesharingtomoodlenet)) {
if (!$ADMIN->locate('moodlenet')) {
$ADMIN->add('root', new admin_category('moodlenet', get_string('pluginname', 'tool_moodlenet')));
}
// Outbound settings page.
$settings = new admin_settingpage('moodlenetoutbound', new lang_string('moodlenet:outboundsettings', 'moodle'));
$ADMIN->add('moodlenet', $settings);
// Get all the issuers.
$issuers = \core\oauth2\api::get_all_issuers();
$oauth2services = [
'' => new lang_string('none', 'admin'),
];
foreach ($issuers as $issuer) {
// Get the enabled issuer with the service type is MoodleNet only.
if ($issuer->get('servicetype') == 'moodlenet' && $issuer->get('enabled')) {
$oauth2services[$issuer->get('id')] = s($issuer->get('name'));
}
}
$url = new \moodle_url('/admin/tool/oauth2/issuers.php');
$settings->add(new admin_setting_configselect('moodlenet/oauthservice', new lang_string('issuer', 'auth_oauth2'),
new lang_string('moodlenet:configoauthservice', 'moodle', $url->out()), '', $oauth2services));
}
}

View File

@ -0,0 +1,52 @@
@core @core_admin
Feature: MoodleNet outbound configuration
In order to send activity/resource to MoodleNet
As a Moodle administrator
I need to set outbound configuration
Background:
Given I log in as "admin"
Scenario: Share to MoodleNet experimental flag
Given I navigate to "Development > Experimental" in site administration
Then "Enable sharing to MoodleNet" "field" should exist
And the field "Enable sharing to MoodleNet" matches value "0"
Scenario: Outbound configuration without experimental flag enable yet
Given I navigate to "MoodleNet" in site administration
Then I should not see "MoodleNet outbound settings"
Scenario: Outbound configuration without OAuth 2 service setup yet
Given the following config values are set as admin:
| enablesharingtomoodlenet | 1 |
When I navigate to "MoodleNet" in site administration
Then I should see "MoodleNet outbound settings"
And I click on "MoodleNet outbound settings" "link"
And the field "OAuth 2 service" matches value "None"
And I should see "Select a MoodleNet OAuth 2 service to enable sharing to that MoodleNet site. If the service doesn't exist yet, you will need to create it."
And I click on "create" "link"
And I should see "OAuth 2 services"
Scenario: Outbound configuration with OAuth 2 service setup
Given a MoodleNet mock server is configured
And the following config values are set as admin:
| enablesharingtomoodlenet | 1 |
And I navigate to "Server > OAuth 2 services" in site administration
And I press "Custom"
And I should see "Create new service: Custom"
And I set the following fields to these values:
| Name | Testing custom service |
| Client ID | thisistheclientid |
| Client secret | supersecret |
And I press "Save changes"
When I navigate to "MoodleNet > MoodleNet outbound settings" in site administration
Then the field "OAuth 2 service" matches value "None"
And I navigate to "Server > OAuth 2 services" in site administration
And I press "MoodleNet"
And I should see "Create new service: MoodleNet"
And I change the MoodleNet field "Service base URL" to mock server
And I press "Save changes"
And I navigate to "MoodleNet > MoodleNet outbound settings" in site administration
And the "OAuth 2 service" "field" should be enabled
And I should see "MoodleNet" in the "OAuth 2 service" "select"
And I should not see "Testing custom service" in the "OAuth 2 service" "select"

View File

@ -25,7 +25,7 @@
defined('MOODLE_INTERNAL') || die();
$string['autoenablenotification'] = '<p>In Moodle 4.0 onwards, the <a href="https://moodle.net/">MoodleNet</a> integration is enabled by default in Advanced features. Users with the capability to create and manage activities can browse MoodleNet via the activity chooser and import MoodleNet resources into their courses.</p><p>If desired, an alternative MoodleNet instance may be specified in the <a href="{$a->settingslink}">MoodleNet settings</a>.</p>';
$string['autoenablenotification'] = '<p>In Moodle 4.0 onwards, the <a href="https://moodle.net/">MoodleNet</a> integration is enabled by default in Advanced features. Users with the capability to create and manage activities can browse MoodleNet via the activity chooser and import MoodleNet resources into their courses.</p><p>If desired, an alternative MoodleNet instance may be specified in the <a href="{$a->settingslink}">MoodleNet inbound settings</a>.</p>';
$string['autoenablenotification_subject'] = 'Default MoodleNet setting changed.';
$string['addingaresource'] = 'Adding content from MoodleNet';
$string['aria:enterprofile'] = "Enter your MoodleNet profile ID";
@ -38,7 +38,7 @@ $string['defaultmoodlenet_desc'] = 'The URL of the MoodleNet instance available
$string['defaultmoodlenetname'] = "MoodleNet instance name";
$string['defaultmoodlenetnamevalue'] = 'MoodleNet Central';
$string['defaultmoodlenetname_desc'] = 'The name of the MoodleNet instance available via the activity chooser.';
$string['enablemoodlenet'] = 'Enable MoodleNet integration';
$string['enablemoodlenet'] = 'Enable MoodleNet integration (inbound)';
$string['enablemoodlenet_desc'] = 'If enabled, a user with the capability to create and manage activities can browse MoodleNet via the activity chooser and import MoodleNet resources into their course. In addition, a user with the capability to restore backups can select a backup file on MoodleNet and restore it into Moodle.';
$string['errorduringdownload'] = 'An error occurred while downloading the file: {$a}';
$string['forminfo'] = 'Your MoodleNet profile ID will be automatically saved in your profile on this site.';
@ -55,7 +55,7 @@ $string['missinginvalidpostdata'] = 'The resource information from MoodleNet is
If this happens repeatedly, please contact the site administrator.';
$string['mnetprofile'] = 'MoodleNet profile';
$string['mnetprofiledesc'] = '<p>Enter your MoodleNet profile details here to be redirected to your profile while visiting MoodleNet.</p>';
$string['moodlenetsettings'] = 'MoodleNet settings';
$string['moodlenetsettings'] = 'MoodleNet inbound settings';
$string['moodlenetnotenabled'] = 'The MoodleNet integration must be enabled in Site administration / MoodleNet before resource imports can be processed.';
$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['removedmnetprofilenotification'] = 'Due to recent changes on the MoodleNet platform, any users who previously saved their MoodleNet profile ID on the site will need to enter a MoodleNet profile ID in the new format in order to authenticate on the MoodleNet platform.';

View File

@ -35,7 +35,9 @@ if ($hassiteconfig) {
// Create a MoodleNet category.
if (get_config('tool_moodlenet', 'enablemoodlenet')) {
$ADMIN->add('root', new admin_category('moodlenet', get_string('pluginname', 'tool_moodlenet')));
if (!$ADMIN->locate('moodlenet')) {
$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);

View File

@ -607,6 +607,8 @@ $string['enableplugin'] = 'Enable {$a}';
$string['enablerecordcache'] = 'Enable record cache';
$string['enablerssfeeds'] = 'Enable RSS feeds';
$string['enablesearchareas'] = 'Enable search areas';
$string['enablesharingtomoodlenet'] = 'Enable sharing to MoodleNet (outbound)';
$string['enablesharingtomoodlenet_desc'] = 'Enable users to share course content to a configured MoodleNet instance if they have the relevant capability.';
$string['enablestats'] = 'Enable statistics';
$string['enabletrusttext'] = 'Enable trusted content';
$string['enableuserfeedback'] = 'Enable feedback about this software';

View File

@ -426,6 +426,8 @@ $string['moduledisable'] = 'This module ({$a}) has been disabled for this partic
$string['moduledoesnotexist'] = 'This module does not exist';
$string['moduleinstancedoesnotexist'] = 'The instance of this module does not exist';
$string['modulemissingcode'] = 'Module {$a} is missing the code needed to perform this function';
$string['moodlenet:invalidshareformat'] = 'Invalid MoodleNet share format';
$string['moodlenet:usernotconfigured'] = 'You do not have permission to share content to MoodleNet, or your account is incorrectly configured.';
$string['movecatcontentstoroot'] = 'Moving the category content to root is not allowed. You must move the contents to an existing category!';
$string['movecatcontentstoselected'] = 'Some category content cannot be moved into the selected category.';
$string['movecategorynotpossible'] = 'You cannot move category \'{$a}\' into the selected category.';

View File

@ -1365,6 +1365,27 @@ $string['moodledocslink'] = 'Help and documentation';
$string['moodleversion'] = 'Moodle version';
$string['moodlerelease'] = 'Moodle release';
$string['moodleservicesandsupport'] = 'Services and support';
$string['moodlenet:cannotconnecttoserver'] = 'Cannot connect to MoodleNet server';
$string['moodlenet:configoauthservice'] = 'Select a MoodleNet OAuth 2 service to enable sharing to that MoodleNet site. If the service doesn\'t exist yet, you will need to <a href="{$a}">create</a> it.';
$string['moodlenet:eventresourceexported'] = 'Resource exported';
$string['moodlenet:gotomoodlenet'] = 'Go to MoodleNet drafts';
$string['moodlenet:issuerisnotauthorized'] = 'MoodleNet issuer is not authorized';
$string['moodlenet:issuerisnotenabled'] = 'MoodleNet issuer is not enabled';
$string['moodlenet:issuerisnotset'] = 'MoodleNet issuer is not set at site administration';
$string['moodlenet:outboundsettings'] = 'MoodleNet outbound settings';
$string['moodlenet:sharenotice'] = 'You are sharing this to MoodleNet as a {$a}';
$string['moodlenet:sharefailtitle'] = 'Something went wrong';
$string['moodlenet:sharefailtext'] = 'There was an error sharing your content to MoodleNet.<br>Please try again later.';
$string['moodlenet:sharefailtextwithsitesupport'] = 'There was an error sharing your content to MoodleNet.<br>Please try again later or <a href="{$a}">contact site support</a>.';
$string['moodlenet:sharefilesizelimitexceeded'] = 'The size of the resource being shared ({$a->filesize} bytes) exceeds the limit of {$a->filesizelimit} bytes.';
$string['moodlenet:sharesuccesstitle'] = 'Saved to MoodleNet drafts';
$string['moodlenet:sharesuccesstext'] = "Almost done! Visit your drafts in MoodleNet to finish sharing your content.";
$string['moodlenet:sharetomoodlenet'] = 'Share to MoodleNet';
$string['moodlenet:sharetyperesource'] = 'resource';
$string['moodlenet:sharingstatus'] = 'Sharing to MoodleNet';
$string['moodlenet:sharinglargefile'] = "Large files can take some time.";
$string['moodlenet:sharingto'] = 'Sharing to: ';
$string['moodlenet:packagingandsending'] = 'Packaging your file and sending to MoodleNet...';
$string['more'] = 'more';
$string['morehelp'] = 'More help';
$string['morehelpaboutmodule'] = 'More help about the {$a} activity';
@ -1970,6 +1991,7 @@ $string['setmode'] = 'Set mode';
$string['setpassword'] = 'Set password';
$string['setpasswordinstructions'] = 'Please enter your new password below, then save changes.';
$string['settings'] = 'Settings';
$string['share'] = 'Share';
$string['shortname'] = 'Short name'; /* @deprecated MDL-34652 - Use shortnamecourse or shortnameuser or some own context specific string. */
$string['shortnamecollisionwarning'] = '[*] = This shortname is already in use by a course and will need to be changed upon approval';
$string['shortnamecourse'] = 'Course short name';

View File

@ -309,6 +309,7 @@ $string['manager'] = 'Manager';
$string['managerdescription'] = 'Managers can access courses and modify them, but usually do not participate in them.';
$string['manageroles'] = 'Manage roles';
$string['maybeassignedin'] = 'Context types where this role may be assigned';
$string['moodlenet:shareactivity'] = 'Share activities to MoodleNet';
$string['morethan'] = 'More than {$a}';
$string['multipleroles'] = 'Multiple roles';
$string['my:manageblocks'] = 'Manage Dashboard page blocks';

View File

@ -0,0 +1,11 @@
define("core/moodlenet/oauth2callback",["exports","core/prefetch","core/notification","core/str"],(function(_exports,_prefetch,_notification,_str){var obj;
/**
* A module to handle the OAuth2 callback for MoodleNet.
*
* @module core/moodlenet/oauth2callback
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 4.2
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_prefetch=(obj=_prefetch)&&obj.__esModule?obj:{default:obj};_exports.init=(error,errorDescription)=>{_prefetch.default.prefetchStrings("moodle",["moodlenet:sharefailtitle","error"]),((error,errorDescription)=>{window.opener?(window.opener.moodleNetAuthorize(error,errorDescription),setTimeout((()=>{window.close()}),300)):(0,_notification.alert)((0,_str.get_string)("error","moodle"),(0,_str.get_string)("moodlenet:sharefailtitle","moodle"))})(error,errorDescription)}}));
//# sourceMappingURL=oauth2callback.min.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"oauth2callback.min.js","sources":["../../src/moodlenet/oauth2callback.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * A module to handle the OAuth2 callback for MoodleNet.\n *\n * @module core/moodlenet/oauth2callback\n * @copyright 2023 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.2\n */\n\nimport Prefetch from \"core/prefetch\";\nimport {alert} from 'core/notification';\nimport {get_string as getString} from 'core/str';\n\n/**\n * Handle the OAuth2 callback for MoodleNet.\n *\n * @param {String} error Error\n * @param {String} errorDescription Error description\n */\nconst handleCallback = (error, errorDescription) => {\n if (window.opener) {\n // Call the MoodleNet Authorization again in the opener window.\n window.opener.moodleNetAuthorize(error, errorDescription);\n // Close the authorization popup.\n // We need to use setTimeout here because the Behat 'I press \"x\" and switch to main window' step expects the popup to still\n // be visible after clicking the button. Otherwise, it will throw a webdriver error.\n setTimeout(() => {\n // Close the authorization popup.\n window.close();\n }, 300);\n } else {\n alert(getString('error', 'moodle'), getString('moodlenet:sharefailtitle', 'moodle'));\n }\n};\n\n/**\n * Initialize.\n *\n * @param {String} error Error\n * @param {String} errorDescription Error description\n */\nexport const init = (error, errorDescription) => {\n Prefetch.prefetchStrings('moodle', ['moodlenet:sharefailtitle', 'error']);\n handleCallback(error, errorDescription);\n};\n"],"names":["error","errorDescription","prefetchStrings","window","opener","moodleNetAuthorize","setTimeout","close","handleCallback"],"mappings":";;;;;;;;4JAwDoB,CAACA,MAAOC,sCACfC,gBAAgB,SAAU,CAAC,2BAA4B,UAvB7C,EAACF,MAAOC,oBACvBE,OAAOC,QAEPD,OAAOC,OAAOC,mBAAmBL,MAAOC,kBAIxCK,YAAW,KAEPH,OAAOI,UACR,+BAEG,mBAAU,QAAS,WAAW,mBAAU,2BAA4B,YAY9EC,CAAeR,MAAOC"}

View File

@ -0,0 +1,3 @@
define("core/moodlenet/send_activity_modal",["exports","core/modal","core/modal_registry"],(function(_exports,_modal,_modal_registry){var _class;function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=_interopRequireDefault(_modal),_modal_registry=_interopRequireDefault(_modal_registry);const SendActivityModal=(_defineProperty(_class=class extends _modal.default{registerEventListeners(){super.registerEventListeners(),this.registerCloseOnSave(),this.registerCloseOnCancel()}},"TYPE","core/moodlenet/send_activity_modal"),_defineProperty(_class,"TEMPLATE","core/moodlenet/send_activity_modal_base"),_class);_modal_registry.default.register(SendActivityModal.TYPE,SendActivityModal,SendActivityModal.TEMPLATE);var _default=SendActivityModal;return _exports.default=_default,_exports.default}));
//# sourceMappingURL=send_activity_modal.min.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"send_activity_modal.min.js","sources":["../../src/moodlenet/send_activity_modal.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Send activity modal for MoodleNet.\n *\n * @module core/moodlenet/send_activity_modal\n * @copyright 2023 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.2\n */\n\nimport Modal from 'core/modal';\nimport ModalRegistry from 'core/modal_registry';\n\nconst SendActivityModal = class extends Modal {\n static TYPE = 'core/moodlenet/send_activity_modal';\n static TEMPLATE = 'core/moodlenet/send_activity_modal_base';\n\n registerEventListeners() {\n // Call the parent registration.\n super.registerEventListeners();\n\n // Register to close on save/cancel.\n this.registerCloseOnSave();\n this.registerCloseOnCancel();\n }\n};\n\nModalRegistry.register(SendActivityModal.TYPE, SendActivityModal, SendActivityModal.TEMPLATE);\n\nexport default SendActivityModal;\n"],"names":["SendActivityModal","Modal","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","register","TYPE","TEMPLATE"],"mappings":"2jBA2BMA,0CAAoB,cAAcC,eAIpCC,+BAEUA,8BAGDC,2BACAC,iCATK,wEACI,2EAYRC,SAASL,kBAAkBM,KAAMN,kBAAmBA,kBAAkBO,uBAErEP"}

View File

@ -0,0 +1,11 @@
define("core/moodlenet/send_resource",["exports","core/config","core/modal_factory","core/notification","core/str","core/prefetch","core/templates","core/moodlenet/service","core/moodlenet/send_activity_modal"],(function(_exports,_config,_modal_factory,_notification,_str,_prefetch,Templates,MoodleNetService,_send_activity_modal){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* A module to handle Share operations of the MoodleNet.
*
* @module core/moodlenet/send_resource
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 4.2
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_config=_interopRequireDefault(_config),_modal_factory=_interopRequireDefault(_modal_factory),_prefetch=_interopRequireDefault(_prefetch),Templates=_interopRequireWildcard(Templates),MoodleNetService=_interopRequireWildcard(MoodleNetService),_send_activity_modal=_interopRequireDefault(_send_activity_modal);let currentModal,siteSupportUrl,issuerId,courseId,cmId,shareFormat;const responseFromMoodleNet=function(status){let resourceUrl=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";const $modal=currentModal.getModal(),modal=$modal[0];modal.querySelector(".modal-header").classList.add("no-border"),currentModal.setBody(Templates.render("core/moodlenet/send_activity_modal_done",{success:status,sitesupporturl:siteSupportUrl})),status&&(currentModal.setFooter(Templates.render("core/moodlenet/send_activity_modal_footer_view",{resourseurl:resourceUrl})),currentModal.showFooter())},handleAuthorization=issuerId=>MoodleNetService.authorizationCheck(issuerId,courseId).then((async data=>data.status?((issuerId,cmId,shareFormat)=>{const modal=currentModal.getModal()[0];modal.querySelector(".modal-header").classList.remove("no-border"),modal.querySelector(".modal-header").classList.add("no-header-text"),currentModal.setBody(Templates.render("core/moodlenet/send_activity_modal_packaging",{})),currentModal.hideFooter(),MoodleNetService.sendActivity(issuerId,cmId,shareFormat).then((async data=>{const status=data.status,resourceUrl=data.resourceurl;return responseFromMoodleNet(status,resourceUrl)})).catch(_notification.exception)})(issuerId,cmId,shareFormat):(window.moodleNetAuthorize=(error,errorDescription)=>{""==error?handleAuthorization(issuerId):"access_denied"!==error&&(0,_notification.alert)("Authorization error","Error: "+error+"<br><br>Error description: "+errorDescription,"Cancel")},window.open(data.loginurl,"moodlenet_auth","location=0,status=0,width=".concat(550,",height=").concat(550,",scrollbars=yes"))))).catch(_notification.exception);_exports.init=()=>{_prefetch.default.prefetchTemplates(["core/moodlenet/send_activity_modal_base","core/moodlenet/send_activity_modal_packaging","core/moodlenet/send_activity_modal_done","core/moodlenet/send_activity_modal_footer_view"]),document.addEventListener("click",(e=>{const shareAction=e.target.closest('[data-action="sendtomoodlenet"]'),sendAction=e.target.closest('.moodlenet-action-buttons [data-action="share"]');if(shareAction){e.preventDefault();const type=shareAction.getAttribute("data-type"),shareType=shareAction.getAttribute("data-sharetype"),cmId=_config.default.contextInstanceId;"activity"==type&&MoodleNetService.getActivityInformation(cmId).then((async data=>data.status?(siteSupportUrl=data.supportpageurl,issuerId=data.issuerid,_modal_factory.default.create({type:_send_activity_modal.default.TYPE,large:!0,templateContext:{activitytype:data.type,activityname:data.name,sharetype:await(0,_str.get_string)("moodlenet:sharetype"+shareType,"moodle"),server:data.server}}).then((modal=>(currentModal=modal,modal.show(),modal))).catch(_notification.exception)):(0,_notification.addNotification)({message:data.warnings[0].message,type:"error"}))).catch(_notification.exception)}sendAction&&(e.preventDefault(),courseId=_config.default.courseId,cmId=_config.default.contextInstanceId,shareFormat=0,handleAuthorization(issuerId))}))}}));
//# sourceMappingURL=send_resource.min.js.map

File diff suppressed because one or more lines are too long

11
lib/amd/build/moodlenet/service.min.js vendored Normal file
View File

@ -0,0 +1,11 @@
define("core/moodlenet/service",["exports","core/ajax"],(function(_exports,_ajax){var obj;
/**
* A javascript module to handle MoodleNet ajax actions.
*
* @module core/moodlenet/service
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 4.2
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.sendActivity=_exports.getActivityInformation=_exports.authorizationCheck=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.getActivityInformation=cmId=>{const request={methodname:"core_moodlenet_get_share_info_activity",args:{cmid:cmId}};return _ajax.default.call([request])[0]};_exports.sendActivity=(issuerId,cmId,shareFormat)=>{const request={methodname:"core_moodlenet_send_activity",args:{issuerid:issuerId,cmid:cmId,shareformat:shareFormat}};return _ajax.default.call([request])[0]};_exports.authorizationCheck=(issuerId,courseId)=>{const request={methodname:"core_moodlenet_auth_check",args:{issuerid:issuerId,courseid:courseId}};return _ajax.default.call([request])[0]}}));
//# sourceMappingURL=service.min.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"service.min.js","sources":["../../src/moodlenet/service.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * A javascript module to handle MoodleNet ajax actions.\n *\n * @module core/moodlenet/service\n * @copyright 2023 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.2\n */\n\nimport Ajax from 'core/ajax';\n\n/**\n * Get the activity information by course module id.\n *\n * @param {Integer} cmId The course module id.\n * @return {promise}\n */\nexport const getActivityInformation = (cmId) => {\n const request = {\n methodname: 'core_moodlenet_get_share_info_activity',\n args: {\n cmid: cmId\n }\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Send the activity to Moodlenet.\n *\n * @param {Integer} issuerId The OAuth 2 issuer ID.\n * @param {Integer} cmId The course module ID.\n * @param {Integer} shareFormat The share format.\n * @return {promise}\n */\nexport const sendActivity = (issuerId, cmId, shareFormat) => {\n const request = {\n methodname: 'core_moodlenet_send_activity',\n args: {\n issuerid: issuerId,\n cmid: cmId,\n shareformat: shareFormat,\n }\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Check if the user is already authorized with MoodleNet.\n *\n * @param {Integer} issuerId The OAuth 2 issuer ID.\n * @param {Integer} courseId The course ID.\n * @return {promise}\n */\nexport const authorizationCheck = (issuerId, courseId) => {\n const request = {\n methodname: 'core_moodlenet_auth_check',\n args: {\n issuerid: issuerId,\n courseid: courseId,\n }\n };\n\n return Ajax.call([request])[0];\n};\n"],"names":["cmId","request","methodname","args","cmid","Ajax","call","issuerId","shareFormat","issuerid","shareformat","courseId","courseid"],"mappings":";;;;;;;;0OAgCuCA,aAC7BC,QAAU,CACZC,WAAY,yCACZC,KAAM,CACFC,KAAMJ,cAIPK,cAAKC,KAAK,CAACL,UAAU,0BAWJ,CAACM,SAAUP,KAAMQ,qBACnCP,QAAU,CACZC,WAAY,+BACZC,KAAM,CACFM,SAAUF,SACVH,KAAMJ,KACNU,YAAaF,qBAIdH,cAAKC,KAAK,CAACL,UAAU,gCAUE,CAACM,SAAUI,kBACnCV,QAAU,CACZC,WAAY,4BACZC,KAAM,CACFM,SAAUF,SACVK,SAAUD,kBAIXN,cAAKC,KAAK,CAACL,UAAU"}

View File

@ -0,0 +1,60 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A module to handle the OAuth2 callback for MoodleNet.
*
* @module core/moodlenet/oauth2callback
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 4.2
*/
import Prefetch from "core/prefetch";
import {alert} from 'core/notification';
import {get_string as getString} from 'core/str';
/**
* Handle the OAuth2 callback for MoodleNet.
*
* @param {String} error Error
* @param {String} errorDescription Error description
*/
const handleCallback = (error, errorDescription) => {
if (window.opener) {
// Call the MoodleNet Authorization again in the opener window.
window.opener.moodleNetAuthorize(error, errorDescription);
// Close the authorization popup.
// We need to use setTimeout here because the Behat 'I press "x" and switch to main window' step expects the popup to still
// be visible after clicking the button. Otherwise, it will throw a webdriver error.
setTimeout(() => {
// Close the authorization popup.
window.close();
}, 300);
} else {
alert(getString('error', 'moodle'), getString('moodlenet:sharefailtitle', 'moodle'));
}
};
/**
* Initialize.
*
* @param {String} error Error
* @param {String} errorDescription Error description
*/
export const init = (error, errorDescription) => {
Prefetch.prefetchStrings('moodle', ['moodlenet:sharefailtitle', 'error']);
handleCallback(error, errorDescription);
};

View File

@ -0,0 +1,44 @@
// 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/>.
/**
* Send activity modal for MoodleNet.
*
* @module core/moodlenet/send_activity_modal
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 4.2
*/
import Modal from 'core/modal';
import ModalRegistry from 'core/modal_registry';
const SendActivityModal = class extends Modal {
static TYPE = 'core/moodlenet/send_activity_modal';
static TEMPLATE = 'core/moodlenet/send_activity_modal_base';
registerEventListeners() {
// Call the parent registration.
super.registerEventListeners();
// Register to close on save/cancel.
this.registerCloseOnSave();
this.registerCloseOnCancel();
}
};
ModalRegistry.register(SendActivityModal.TYPE, SendActivityModal, SendActivityModal.TEMPLATE);
export default SendActivityModal;

View File

@ -0,0 +1,189 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A module to handle Share operations of the MoodleNet.
*
* @module core/moodlenet/send_resource
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 4.2
*/
import Config from 'core/config';
import ModalFactory from 'core/modal_factory';
import {alert as displayAlert, addNotification, exception as displayException} from 'core/notification';
import {get_string as getString} from 'core/str';
import Prefetch from "core/prefetch";
import * as Templates from 'core/templates';
import * as MoodleNetService from 'core/moodlenet/service';
import SendActivityModal from 'core/moodlenet/send_activity_modal';
const TYPE_ACTIVITY = "activity";
let currentModal;
let siteSupportUrl;
let issuerId;
let courseId;
let cmId;
let shareFormat;
/**
* Handle send to MoodleNet.
*
* @param {int} issuerId The OAuth 2 issuer ID.
* @param {int} cmId The course module ID.
* @param {int} shareFormat The share format.
*/
const sendToMoodleNet = (issuerId, cmId, shareFormat) => {
const $modal = currentModal.getModal();
const modal = $modal[0];
modal.querySelector('.modal-header').classList.remove('no-border');
modal.querySelector('.modal-header').classList.add('no-header-text');
currentModal.setBody(Templates.render('core/moodlenet/send_activity_modal_packaging', {}));
currentModal.hideFooter();
MoodleNetService.sendActivity(issuerId, cmId, shareFormat).then(async(data) => {
const status = data.status;
const resourceUrl = data.resourceurl;
return responseFromMoodleNet(status, resourceUrl);
}).catch(displayException);
};
/**
* Handle response from MoodleNet.
*
* @param {boolean} status Response status. True if successful.
* @param {String} resourceUrl Resource URL.
*/
const responseFromMoodleNet = (status, resourceUrl = '') => {
const $modal = currentModal.getModal();
const modal = $modal[0];
modal.querySelector('.modal-header').classList.add('no-border');
currentModal.setBody(Templates.render('core/moodlenet/send_activity_modal_done', {
success: status,
sitesupporturl: siteSupportUrl,
}));
if (status) {
currentModal.setFooter(Templates.render('core/moodlenet/send_activity_modal_footer_view', {
resourseurl: resourceUrl,
}));
currentModal.showFooter();
}
};
/**
* Handle authorization with MoodleNet server.
*
* @param {int} issuerId The OAuth 2 issuer ID.
* @return {promise}
*/
const handleAuthorization = (issuerId) => {
const windowsizewidth = 550;
const windowsizeheight = 550;
// Check if the user is authorized with MoodleNet or not.
return MoodleNetService.authorizationCheck(issuerId, courseId).then(async(data) => {
if (!data.status) {
// Not yet authorized.
// Declare moodleNetAuthorize variable, so we can call it later in the callback.
window.moodleNetAuthorize = (error, errorDescription) => {
// This will be called by the callback after the authorization is successful.
if (error == '') {
handleAuthorization(issuerId);
} else if (error !== 'access_denied') {
displayAlert(
'Authorization error',
'Error: ' + error + '<br><br>Error description: ' + errorDescription,
'Cancel'
);
}
};
// Open the login url of the OAuth 2 issuer for user to login into MoodleNet and authorize.
return window.open(data.loginurl, 'moodlenet_auth',
`location=0,status=0,width=${windowsizewidth},height=${windowsizeheight},scrollbars=yes`);
} else {
// Already authorized.
return sendToMoodleNet(issuerId, cmId, shareFormat);
}
}).catch(displayException);
};
/**
* Register events.
*/
const registerEventListeners = () => {
document.addEventListener('click', e => {
const shareAction = e.target.closest('[data-action="sendtomoodlenet"]');
const sendAction = e.target.closest('.moodlenet-action-buttons [data-action="share"]');
if (shareAction) {
e.preventDefault();
const type = shareAction.getAttribute('data-type');
const shareType = shareAction.getAttribute('data-sharetype');
const cmId = Config.contextInstanceId;
if (type == TYPE_ACTIVITY) {
MoodleNetService.getActivityInformation(cmId).then(async(data) => {
if (data.status) {
siteSupportUrl = data.supportpageurl;
issuerId = data.issuerid;
const modalPromise = ModalFactory.create({
type: SendActivityModal.TYPE,
large: true,
templateContext: {
'activitytype': data.type,
'activityname': data.name,
'sharetype': await getString('moodlenet:sharetype' + shareType, 'moodle'),
'server': data.server,
}
});
return modalPromise.then(modal => {
currentModal = modal;
modal.show();
return modal;
}).catch(displayException);
} else {
return addNotification({
message: data.warnings[0].message,
type: 'error'
});
}
}).catch(displayException);
}
}
if (sendAction) {
e.preventDefault();
courseId = Config.courseId;
cmId = Config.contextInstanceId;
shareFormat = 0;
handleAuthorization(issuerId);
}
});
};
/**
* Initialize.
*/
export const init = () => {
Prefetch.prefetchTemplates([
'core/moodlenet/send_activity_modal_base',
'core/moodlenet/send_activity_modal_packaging',
'core/moodlenet/send_activity_modal_done',
'core/moodlenet/send_activity_modal_footer_view',
]);
registerEventListeners();
};

View File

@ -0,0 +1,82 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A javascript module to handle MoodleNet ajax actions.
*
* @module core/moodlenet/service
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 4.2
*/
import Ajax from 'core/ajax';
/**
* Get the activity information by course module id.
*
* @param {Integer} cmId The course module id.
* @return {promise}
*/
export const getActivityInformation = (cmId) => {
const request = {
methodname: 'core_moodlenet_get_share_info_activity',
args: {
cmid: cmId
}
};
return Ajax.call([request])[0];
};
/**
* Send the activity to Moodlenet.
*
* @param {Integer} issuerId The OAuth 2 issuer ID.
* @param {Integer} cmId The course module ID.
* @param {Integer} shareFormat The share format.
* @return {promise}
*/
export const sendActivity = (issuerId, cmId, shareFormat) => {
const request = {
methodname: 'core_moodlenet_send_activity',
args: {
issuerid: issuerId,
cmid: cmId,
shareformat: shareFormat,
}
};
return Ajax.call([request])[0];
};
/**
* Check if the user is already authorized with MoodleNet.
*
* @param {Integer} issuerId The OAuth 2 issuer ID.
* @param {Integer} courseId The course ID.
* @return {promise}
*/
export const authorizationCheck = (issuerId, courseId) => {
const request = {
methodname: 'core_moodlenet_auth_check',
args: {
issuerid: issuerId,
courseid: courseId,
}
};
return Ajax.call([request])[0];
};

View File

@ -154,6 +154,11 @@
"allowedlevel2": true,
"allowedspread": true
},
"moodlenet": {
"component": "core",
"allowedlevel2": false,
"allowedspread": false
},
"navigation": {
"component": "core",
"allowedlevel2": true,

View File

@ -0,0 +1,72 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\event;
/**
* MoodleNet send attempt event.
*
* @package core
* @copyright 2023 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class moodlenet_resource_exported extends \core\event\base {
/**
* Set basic properties for the event.
*
* @return void
*/
protected function init(): void {
$this->data['crud'] = 'c';
// Used by teachers, but not for direct educational value to their students.
$this->data['edulevel'] = self::LEVEL_OTHER;
}
/**
* Fetch the localised general event name.
*
* @return string
*/
public static function get_name(): string {
return get_string('moodlenet:eventresourceexported');
}
/**
* Fetch the non-localised event description.
* This description format is designed to work for both single activity and course sharing.
*
* @return string
*/
public function get_description() {
$outcome = $this->other['success'] ? 'successfully shared' : 'failed to share';
$cmids = implode("', '", $this->other['cmids']);
$description = "The user with id '{$this->userid}' {$outcome} activities to MoodleNet with the " .
"following course module ids, from context with id '{$this->data['contextid']}': '{$cmids}'.";
return rtrim($description, ", '");
}
/**
* Returns relevant URL.
* @return \moodle_url
*/
public function get_url() {
return new \moodle_url($this->other['resourceurl']);
}
}

View File

@ -0,0 +1,147 @@
<?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/>.
namespace core\external;
use context_course;
use core\moodlenet\moodlenet_client;
use core\moodlenet\utilities;
use core\oauth2\api;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use core_external\external_warnings;
use moodle_url;
/**
* The external API to check whether a user has authorized for a given MoodleNet OAuth 2 issuer.
*
* @package core
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class moodlenet_auth_check extends external_api {
/**
* Returns description of parameters.
*
* @return external_function_parameters
* @since Moodle 4.2
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'issuerid' => new external_value(PARAM_INT, 'OAuth 2 issuer ID', VALUE_REQUIRED),
'courseid' => new external_value(PARAM_INT, 'Course ID', VALUE_REQUIRED),
]);
}
/**
* External function to check if the user is already authorized with MoodleNet.
*
* @param int $issuerid Issuer Id.
* @param int $courseid The course ID that contains the activity which being shared
* @return array
* @since Moodle 4.2
*/
public static function execute(int $issuerid, int $courseid): array {
global $USER;
[
'issuerid' => $issuerid,
'courseid' => $courseid,
] = self::validate_parameters(self::execute_parameters(), [
'issuerid' => $issuerid,
'courseid' => $courseid,
]);
// Check capability.
$coursecontext = context_course::instance($courseid);
$usercanshare = utilities::can_user_share($coursecontext, $USER->id);
if (!$usercanshare) {
return self::return_errors($courseid, 'errorpermission',
get_string('nopermissions', 'error', get_string('moodlenet:sharetomoodlenet', 'moodle')));
}
// Get the issuer.
$issuer = api::get_issuer($issuerid);
// Validate the issuer and check if it is enabled or not.
if (!utilities::is_valid_instance($issuer)) {
return self::return_errors($issuerid, 'errorissuernotenabled', get_string('invalidparameter', 'debug'));
}
$returnurl = new moodle_url('/admin/moodlenet_oauth2_callback.php');
$returnurl->param('issuerid', $issuerid);
$returnurl->param('callback', 'yes');
$returnurl->param('sesskey', sesskey());
// Get the OAuth Client.
if (!$oauthclient = api::get_user_oauth_client($issuer, $returnurl, moodlenet_client::API_SCOPE_CREATE_RESOURCE, true)) {
return self::return_errors($issuerid, 'erroroauthclient', get_string('invalidparameter', 'debug'));
}
$status = false;
$warnings = [];
$loginurl = '';
if (!$oauthclient->is_logged_in()) {
$loginurl = $oauthclient->get_login_url()->out(false);
} else {
$status = true;
}
return [
'status' => $status,
'loginurl' => $loginurl,
'warnings' => $warnings,
];
}
/**
* Describes the data returned from the external function.
*
* @return external_single_structure
* @since Moodle 4.2
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'loginurl' => new external_value(PARAM_RAW, 'Login url'),
'status' => new external_value(PARAM_BOOL, 'status: true if success'),
'warnings' => new external_warnings(),
]);
}
/**
* Handle return error.
*
* @param int $itemid Item id
* @param string $warningcode Warning code
* @param string $message Message
* @return array
*/
protected static function return_errors(int $itemid, string $warningcode, string $message): array {
$warnings[] = [
'item' => $itemid,
'warningcode' => $warningcode,
'message' => $message,
];
return [
'status' => false,
'loginurl' => '',
'warnings' => $warnings,
];
}
}

View File

@ -0,0 +1,160 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\external;
use context_course;
use core\moodlenet\utilities;
use core\oauth2\api;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use core_external\external_warnings;
/**
* The external API to het the activity information for MoodleNet sharing.
*
* @package core
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class moodlenet_get_share_info_activity extends external_api {
/**
* Returns description of parameters.
*
* @return external_function_parameters
* @since Moodle 4.2
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'cmid' => new external_value(PARAM_INT, 'The cmid of the activity', VALUE_REQUIRED),
]);
}
/**
* External function to get the activity information.
*
* @param int $cmid The course module id.
* @return array
* @since Moodle 4.2
*/
public static function execute(int $cmid): array {
global $CFG, $USER;
[
'cmid' => $cmid
] = self::validate_parameters(self::execute_parameters(), [
'cmid' => $cmid
]);
// Get course module.
$coursemodule = get_coursemodule_from_id(false, $cmid);
if (!$coursemodule) {
return self::return_errors($cmid, 'errorgettingactivityinformation', get_string('invalidcoursemodule', 'error'));
}
// Get course.
$course = get_course($coursemodule->course);
// Check capability.
$coursecontext = context_course::instance($course->id);
$usercanshare = utilities::can_user_share($coursecontext, $USER->id);
if (!$usercanshare) {
return self::return_errors($cmid, 'errorpermission',
get_string('nopermissions', 'error', get_string('moodlenet:sharetomoodlenet', 'moodle')));
}
$warnings = [];
$supporturl = '';
$issuerid = get_config('moodlenet', 'oauthservice');
if (empty($issuerid)) {
return self::return_errors(0, 'errorissuernotset', get_string('moodlenet:issuerisnotset', 'moodle'));
}
if ($CFG->supportavailability && $CFG->supportavailability != CONTACT_SUPPORT_DISABLED) {
if (!empty($CFG->supportpage)) {
$supporturl = $CFG->supportpage;
} else {
$supporturl = $CFG->wwwroot . '/user/contactsitesupport.php';
}
}
// Get the issuer.
$issuer = api::get_issuer($issuerid);
// Validate the issuer and check if it is enabled or not.
if (!utilities::is_valid_instance($issuer)) {
return self::return_errors($issuerid, 'errorissuernotenabled', get_string('moodlenet:issuerisnotenabled', 'moodle'));
}
return [
'status' => true,
'name' => $coursemodule->name,
'type' => get_string('modulename', $coursemodule->modname),
'server' => $issuer->get_display_name(),
'supportpageurl' => $supporturl,
'issuerid' => $issuerid,
'warnings' => $warnings
];
}
/**
* Describes the data returned from the external function.
*
* @return external_single_structure
* @since Moodle 4.2
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'name' => new external_value(PARAM_TEXT, 'Activity name'),
'type' => new external_value(PARAM_TEXT, 'Activity type'),
'server' => new external_value(PARAM_TEXT, 'MoodleNet server'),
'supportpageurl' => new external_value(PARAM_URL, 'Support page URL'),
'issuerid' => new external_value(PARAM_INT, 'MoodleNet issuer id'),
'status' => new external_value(PARAM_BOOL, 'status: true if success'),
'warnings' => new external_warnings()
]);
}
/**
* Handle return error.
*
* @param int $itemid Item id.
* @param string $warningcode Warning code.
* @param string $message Message.
* @param int $issuerid Issuer id.
* @return array
*/
protected static function return_errors(int $itemid, string $warningcode, string $message, int $issuerid = 0): array {
$warnings[] = [
'item' => $itemid,
'warningcode' => $warningcode,
'message' => $message
];
return [
'status' => false,
'name' => '',
'type' => '',
'server' => '',
'supportpageurl' => '',
'issuerid' => $issuerid,
'warnings' => $warnings
];
}
}

View File

@ -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/>.
namespace core\external;
use context_course;
use core\http_client;
use core\moodlenet\activity_sender;
use core\moodlenet\moodlenet_client;
use core\moodlenet\utilities;
use core\oauth2\api;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use core_external\external_warnings;
use moodle_url;
/**
* The external API to send activity to MoodleNet.
*
* @package core
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class moodlenet_send_activity extends external_api {
/**
* Describes the parameters for sending the activity.
*
* @return external_function_parameters
* @since Moodle 4.2
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'issuerid' => new external_value(PARAM_INT, 'OAuth 2 issuer ID', VALUE_REQUIRED),
'cmid' => new external_value(PARAM_INT, 'Course module ID', VALUE_REQUIRED),
'shareformat' => new external_value(PARAM_INT, 'Share format', VALUE_REQUIRED),
]);
}
/**
* External function to send the activity to MoodleNet.
*
* @param int $issuerid The MoodleNet OAuth 2 issuer ID
* @param int $cmid The course module ID of the activity that being shared
* @param int $shareformat The share format being used, as defined by \core\moodlenet\activity_sender
* @return array
* @since Moodle 4.2
*/
public static function execute(int $issuerid, int $cmid, int $shareformat): array {
global $CFG, $USER;
[
'issuerid' => $issuerid,
'cmid' => $cmid,
'shareformat' => $shareformat,
] = self::validate_parameters(self::execute_parameters(), [
'issuerid' => $issuerid,
'cmid' => $cmid,
'shareformat' => $shareformat,
]);
// Check capability.
[$course] = get_course_and_cm_from_cmid($cmid);
$coursecontext = context_course::instance($course->id);
$usercanshare = utilities::can_user_share($coursecontext, $USER->id);
if (!$usercanshare) {
return self::return_errors($cmid, 'errorpermission',
get_string('nopermissions', 'error', get_string('moodlenet:sharetomoodlenet', 'moodle')));
}
// Check format.
if (!in_array($shareformat, [activity_sender::SHARE_FORMAT_BACKUP])) {
return self::return_errors($shareformat, 'errorinvalidformat', get_string('invalidparameter', 'debug'));
}
// Get the issuer.
$issuer = api::get_issuer($issuerid);
// Validate the issuer and check if it is enabled or not.
if (!utilities::is_valid_instance($issuer)) {
return self::return_errors($issuerid, 'errorissuernotenabled', get_string('invalidparameter', 'debug'));
}
// Get the OAuth Client.
if (!$oauthclient = api::get_user_oauth_client(
$issuer,
new moodle_url($CFG->wwwroot),
moodlenet_client::API_SCOPE_CREATE_RESOURCE
)) {
return self::return_errors($issuerid, 'erroroauthclient', get_string('invalidparameter', 'debug'));
}
// Check login state.
if (!$oauthclient->is_logged_in()) {
return self::return_errors($issuerid, 'erroroauthclient', get_string('moodlenet:issuerisnotauthorized', 'moodle'));
}
// Get the HTTP Client.
$client = new http_client();
// Share activity.
try {
$moodlenetclient = new moodlenet_client($client, $oauthclient);
$activitysender = new activity_sender($cmid, $USER->id, $moodlenetclient, $oauthclient, $shareformat);
$result = $activitysender->share_activity();
if (empty($result['drafturl'])) {
return self::return_errors($result['responsecode'], 'errorsendingactivity',
get_string('moodlenet:cannotconnecttoserver', 'moodle'));
}
} catch (\moodle_exception $e) {
return self::return_errors(0, 'errorsendingactivity', $e->getMessage());
}
return [
'status' => true,
'resourceurl' => $result['drafturl'],
'warnings' => [],
];
}
/**
* Describes the data returned from the external function.
*
* @return external_single_structure
* @since Moodle 4.2
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'status' => new external_value(PARAM_BOOL, 'Status: true if success'),
'resourceurl' => new external_value(PARAM_URL, 'Resource URL from MoodleNet'),
'warnings' => new external_warnings(),
]);
}
/**
* Handle return error.
*
* @param int $itemid Item id
* @param string $warningcode Warning code
* @param string $message Message
* @return array
*/
protected static function return_errors(int $itemid, string $warningcode, string $message): array {
$warnings[] = [
'item' => $itemid,
'warningcode' => $warningcode,
'message' => $message,
];
return [
'status' => false,
'resourceurl' => '',
'warnings' => $warnings,
];
}
}

View File

@ -0,0 +1,182 @@
<?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/>.
namespace core\moodlenet;
use backup;
use backup_controller;
use backup_root_task;
use cm_info;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
/**
* Packager to prepare appropriate backup of an activity to share to MoodleNet.
*
* @package core
* @copyright 2023 Raquel Ortega <raquel.ortega@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_packager {
/** @var backup_controller $controller */
protected $controller;
/**
* Constructor.
*
* @param cm_info $cminfo context module information about the resource being packaged.
* @param int $userid The ID of the user performing the packaging.
*/
public function __construct(
protected cm_info $cminfo,
protected int $userid
) {
// Check backup/restore support.
if (!plugin_supports('mod', $cminfo->modname , FEATURE_BACKUP_MOODLE2)) {
throw new \coding_exception("Cannot backup module $cminfo->modname. This module doesn't support the backup feature.");
}
$this->cminfo = $cminfo;
$this->controller = new backup_controller (
backup::TYPE_1ACTIVITY,
$cminfo->id,
backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO,
backup::MODE_GENERAL,
$userid
);
}
/**
* Prepare the backup file using appropriate setting overrides and return relevant information.
*
* @return array Array containing packaged file and stored_file object describing it.
* Array in the format [storedfile => stored_file object, filecontents => raw file].
*/
public function get_package(): array {
$alltasksettings = $this->get_all_task_settings();
// Override relevant settings to remove user data when packaging to share to MoodleNet.
$this->override_task_setting($alltasksettings, 'setting_root_users', 0);
$this->override_task_setting($alltasksettings, 'setting_root_role_assignments', 0);
$this->override_task_setting($alltasksettings, 'setting_root_blocks', 0);
$this->override_task_setting($alltasksettings, 'setting_root_comments', 0);
$this->override_task_setting($alltasksettings, 'setting_root_badges', 0);
$this->override_task_setting($alltasksettings, 'setting_root_userscompletion', 0);
$this->override_task_setting($alltasksettings, 'setting_root_logs', 0);
$this->override_task_setting($alltasksettings, 'setting_root_grade_histories', 0);
$this->override_task_setting($alltasksettings, 'setting_root_groups', 0);
return $this->package();
}
/**
* Get all backup settings available for override.
*
* @return array the associative array of taskclass => settings instances.
*/
protected function get_all_task_settings(): array {
$tasksettings = [];
foreach ($this->controller->get_plan()->get_tasks() as $task) {
$taskclass = get_class($task);
$tasksettings[$taskclass] = $task->get_settings();
}
return $tasksettings;
}
/**
* Override a backup task setting with a given value.
*
* @param array $alltasksettings All task settings.
* @param string $settingname The name of the setting to be overridden (task class name format).
* @param int $settingvalue Value to be given to the setting.
* @return void
*/
protected function override_task_setting(array $alltasksettings, string $settingname, int $settingvalue): void {
if (empty($rootsettings = $alltasksettings[backup_root_task::class])) {
return;
}
foreach ($rootsettings as $setting) {
$name = $setting->get_ui_name();
if ($name == $settingname && $settingvalue != $setting->get_value()) {
$setting->set_value($settingvalue);
return;
}
}
}
/**
* Package the activity identified by CMID.
*
* @return array Array containing packaged file and stored_file object describing it.
* Array in the format [storedfile => stored_file object, filecontents => raw file].
* @throws \moodle_exception.
*/
protected function package(): array {
// Execute the backup and fetch the result.
$this->controller->execute_plan();
$result = $this->controller->get_results();
// Controller no longer required.
$this->controller->destroy();
if (!isset($result['backup_destination'])) {
throw new \moodle_exception('Failed to package activity.');
}
$backupfile = $result['backup_destination'];
if (!$backupfile->get_contenthash()) {
throw new \moodle_exception('Failed to package activity (invalid file).');
}
// Create the location we want to copy this file to.
$time = time();
$fr = [
'contextid' => \context_course::instance($this->cminfo->course)->id,
'component' => 'core',
'filearea' => 'moodlenet_resource',
'filename' => $this->cminfo->modname . '_backup.mbz',
// Add timestamp to itemid to make it unique, to avoid any collisions.
'itemid' => $this->cminfo->id . $time,
'timemodified' => $time,
];
// Create the local file based on the backup.
$fs = get_file_storage();
$packagedfiledata = [
'storedfile' => $fs->create_file_from_storedfile($fr, $backupfile),
];
// Delete the backup now it has been created in the file area.
$backupfile->delete();
if (!$packagedfiledata['storedfile']) {
throw new \moodle_exception("Failed to copy backup file to moodlenet_activity area.");
}
// Ensure we can handle files at the upper end of the limit supported by MoodleNet.
raise_memory_limit(activity_sender::MAX_FILESIZE);
// Get the actual file content.
$packagedfiledata['filecontents'] = $packagedfiledata['storedfile']->get_content();
return $packagedfiledata;
}
}

View File

@ -0,0 +1,211 @@
<?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/>.
namespace core\moodlenet;
use cm_info;
use core\event\moodlenet_resource_exported;
use core\oauth2\client;
use moodle_exception;
use stdClass;
/**
* API for sharing Moodle LMS activities to MoodleNet instances.
*
* @package core
* @copyright 2023 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_sender {
/**
* @var int Backup share format - the content is being shared as a Moodle backup file.
*/
public const SHARE_FORMAT_BACKUP = 0;
/**
* @var int Maximum upload file size (1.07 GB).
*/
public const MAX_FILESIZE = 1070000000;
/**
* @var cm_info The context module info object for the activity being shared.
*/
protected cm_info $cminfo;
/**
* @var stdClass The course where the activity is located.
*/
protected stdClass $course;
/**
* Class constructor.
*
* @param int $cmid The course module ID of the activity being shared.
* @param int $userid The user ID who is sharing the activity.
* @param moodlenet_client $moodlenetclient The moodlenet_client object used to perform the share.
* @param client $oauthclient The OAuth 2 client for the MoodleNet instance.
* @param int $shareformat The data format to share in. Defaults to a Moodle backup (SHARE_FORMAT_BACKUP).
* @throws moodle_exception
*/
public function __construct(
int $cmid,
protected int $userid,
protected moodlenet_client $moodlenetclient,
protected client $oauthclient,
protected int $shareformat = self::SHARE_FORMAT_BACKUP
) {
[$this->course, $this->cminfo] = get_course_and_cm_from_cmid($cmid);
if (!in_array($shareformat, $this->get_allowed_share_formats())) {
throw new moodle_exception('moodlenet:invalidshareformat');
}
}
/**
* Share an activity/resource to MoodleNet.
*
* @return array The HTTP response code from MoodleNet and the MoodleNet draft resource URL (URL empty string on fail).
* Format: ['responsecode' => 201, 'drafturl' => 'https://draft.mnurl/here']
* @throws moodle_exception
*/
public function share_activity(): array {
global $DB;
$accesstoken = '';
$resourceurl = '';
$issuer = $this->oauthclient->get_issuer();
// Check user can share to the requested MoodleNet instance.
$coursecontext = \context_course::instance($this->course->id);
$usercanshare = utilities::can_user_share($coursecontext, $this->userid);
if ($usercanshare && utilities::is_valid_instance($issuer) && $this->oauthclient->is_logged_in()) {
$accesstoken = $this->oauthclient->get_accesstoken()->token;
}
// Throw an exception if the user is not currently set up to be able to share to MoodleNet.
if (!$accesstoken) {
throw new moodle_exception('moodlenet:usernotconfigured');
}
// Attempt to prepare and send the resource if validation has passed and we have an OAuth 2 token.
// Prepare file in requested format.
$filedata = $this->prepare_share_contents();
// If we have successfully prepared a file to share of permitted size, share it to MoodleNet.
if (!empty($filedata['storedfile'])) {
// Avoid sending a file larger than the defined limit.
$filesize = $filedata['storedfile']->get_filesize();
if ($filesize > self::MAX_FILESIZE) {
$filedata['storedfile']->delete();
throw new moodle_exception('moodlenet:sharefilesizelimitexceeded', 'core', '',
[
'filesize' => $filesize,
'filesizelimit' => self::MAX_FILESIZE,
]);
}
// MoodleNet only accept plaintext descriptions.
$resourcedescription = $DB->get_field($this->cminfo->modname, 'intro', ['id' => $this->cminfo->instance]);
$resourcedescription = strip_tags($resourcedescription);
$resourcedescription = format_text(
$resourcedescription,
FORMAT_PLAIN,
['context' => $coursecontext]
);
$response = $this->moodlenetclient->create_resource_from_file($filedata, $this->cminfo->name, $resourcedescription);
$responsecode = $response->getStatusCode();
$responsebody = json_decode($response->getBody());
$resourceurl = $responsebody->homepage ?? '';
// TODO: Store consumable information about completed share - to be completed in MDL-77296.
// Delete the generated file now it is no longer required.
// (It has either been sent, or failed - retries not currently supported).
$filedata['storedfile']->delete();
}
// Log every attempt to share (and whether or not it was successful).
$this->log_event($coursecontext, $this->cminfo->id, $resourceurl, $responsecode);
return [
'responsecode' => $responsecode,
'drafturl' => $resourceurl,
];
}
/**
* Prepare the data for sharing, in the format specified.
*
* @return array Array of metadata about the file, as well as a stored_file object for the file.
*/
protected function prepare_share_contents(): array {
switch ($this->shareformat) {
case self::SHARE_FORMAT_BACKUP:
default:
// If sharing the activity as a backup, prepare the packaged backup.
$packager = new activity_packager($this->cminfo, $this->userid);
$filedata = $packager->get_package();
break;
};
return $filedata;
}
/**
* Log an event to the admin logs for an outbound share attempt.
*
* @param \context $coursecontext The course context being shared from.
* @param int $cmid The CMID of the activity being shared.
* @param string $resourceurl The URL of the draft resource if it was created.
* @param int $responsecode The HTTP response code describing the outcome of the attempt.
* @return void
*/
protected function log_event(
\context $coursecontext,
int $cmid,
string $resourceurl,
int $responsecode
): void {
$event = moodlenet_resource_exported::create([
'context' => $coursecontext,
'other' => [
'cmids' => [$cmid],
'resourceurl' => $resourceurl,
'success' => ($responsecode == 201),
],
]);
$event->trigger();
}
/**
* Return the list of supported share formats.
*
* @return array Array of supported share format values.
*/
protected function get_allowed_share_formats(): array {
return [
self::SHARE_FORMAT_BACKUP,
];
}
}

View File

@ -0,0 +1,113 @@
<?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/>.
namespace core\moodlenet;
use core\http_client;
use core\oauth2\client;
/**
* MoodleNet client which handles direct outbound communication with MoodleNet instances.
*
* @package core
* @copyright 2023 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class moodlenet_client {
/**
* @var string MoodleNet resource creation endpoint URI.
*/
protected const API_CREATE_RESOURCE_URI = '/.pkg/@moodlenet/ed-resource/basic/v1/create';
/**
* @var string MoodleNet scope for creating resources.
*/
public const API_SCOPE_CREATE_RESOURCE = '@moodlenet/ed-resource:write.own';
/**
* Constructor.
*
* @param http_client $httpclient The httpclient object being used to perform the share.
* @param client $oauthclient The OAuth 2 client for the MoodleNet site being shared to.
*/
public function __construct(
protected http_client $httpclient,
protected client $oauthclient,
) {
// All properties promoted, nothing further required.
}
/**
* Create a resource on MoodleNet which includes a file.
*
* @param array $filedata The file data in the format [storedfile => stored_file object, filecontents => raw file].
* @param string $resourcename The name of the resource being shared.
* @param string $resourcedescription A description of the resource being shared.
* @return Psr\Http\Message\ResponseInterface The HTTP client response from MoodleNet.
*/
public function create_resource_from_file(array $filedata, string $resourcename, $resourcedescription) {
// This may take a long time if a lot of data is being shared.
\core_php_time_limit::raise();
$moodleneturl = $this->oauthclient->get_issuer()->get('baseurl');
$apiurl = rtrim($moodleneturl, '/') . self::API_CREATE_RESOURCE_URI;
$requestdata = $this->prepare_file_share_request_data($filedata, $resourcename, $resourcedescription);
return $this->httpclient->request('POST', $apiurl, $requestdata);
}
/**
* Prepare the request data required for sharing a file to MoodleNet.
* This creates an array in the format used by \core\httpclient options to send a multipart request.
*
* @param array $filedata An array of data relating to the file being shared (as prepared by ::prepare_share_contents).
* @param string $resourcename The name of the resource being shared.
* @param string $resourcedescription A description of the resource being shared.
* @return array Data in the format required to send a file to MoodleNet using \core\httpclient.
*/
protected function prepare_file_share_request_data(array $filedata, string $resourcename, $resourcedescription): array {
return [
'headers' => [
'Authorization' => 'Bearer ' . $this->oauthclient->get_accesstoken()->token,
],
'multipart' => [
[
'name' => 'metadata',
'contents' => json_encode([
'name' => $resourcename,
'description' => $resourcedescription,
]),
'headers' => [
'Content-Disposition' => 'form-data; name="."',
],
],
[
'name' => 'filecontents',
'contents' => $filedata['filecontents'],
'headers' => [
'Content-Disposition' => 'form-data; name=".resource"; filename="' .
$filedata['storedfile']->get_filename() . '"',
'Content-Type' => $filedata['storedfile']->get_mimetype(),
'Content-Transfer-Encoding' => 'binary',
],
],
],
];
}
}

View File

@ -0,0 +1,57 @@
<?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/>.
namespace core\moodlenet;
use context_course;
use core\oauth2\issuer;
/**
* Class containing static utilities (such as various checks) required by the MoodleNet API.
*
* @package core
* @copyright 2023 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class utilities {
/**
* Check whether the specified issuer is configured as a MoodleNet instance that can be shared to.
*
* @param issuer $issuer The OAuth 2 issuer being validated.
* @return bool true if the issuer is enabled and available to share to.
*/
public static function is_valid_instance(issuer $issuer): bool {
global $CFG;
$issuerid = $issuer->get('id');
$allowedissuer = get_config('moodlenet', 'oauthservice');
return ($CFG->enablesharingtomoodlenet && $issuerid == $allowedissuer && $issuer->get('enabled') &&
$issuer->get('servicetype') == 'moodlenet');
}
/**
* Check whether a user has the capabilities required to share activities from a given course to MoodleNet.
*
* @param context_course $coursecontext Course context where the activity would be shared from.
* @param int $userid The user ID being checked.
* @return boolean
*/
public static function can_user_share(context_course $coursecontext, int $userid): bool {
return (has_capability('moodle/moodlenet:shareactivity', $coursecontext, $userid) &&
has_capability('moodle/backup:backupactivity', $coursecontext, $userid));
}
}

View File

@ -122,6 +122,7 @@ class secondary extends view {
'backup' => 9,
'restore' => 10,
'competencybreakdown' => 11,
'sendtomoodlenet' => 16,
],
self::TYPE_CUSTOM => [
'advgrading' => 2,

View File

@ -76,8 +76,8 @@ class api {
/**
* Create one of the standard issuers.
*
* @param string $type One of google, facebook, microsoft, nextcloud or imsobv2p1
* @param string|false $baseurl Baseurl (only required for nextcloud and imsobv2p1)
* @param string $type One of google, facebook, microsoft, MoodleNet, nextcloud or imsobv2p1
* @param string|false $baseurl Baseurl (only required for nextcloud, imsobv2p1 and moodlenet)
* @return \core\oauth2\issuer
*/
public static function create_standard_issuer($type, $baseurl = false) {
@ -92,6 +92,10 @@ class api {
if (!$baseurl) {
throw new moodle_exception('Nextcloud service type requires the baseurl parameter.');
}
case 'moodlenet':
if (!$baseurl) {
throw new moodle_exception('MoodleNet service type requires the baseurl parameter.');
}
case 'google':
case 'facebook':
case 'microsoft':

View File

@ -76,7 +76,11 @@ class moodlenet implements issuer_interface {
}
$endpointscreated = 0;
$configreader = new auth_server_config_reader(new http_client());
$config = [];
if (defined('BEHAT_SITE_RUNNING')) {
$config['verify'] = false;
}
$configreader = new auth_server_config_reader(new http_client($config));
try {
$config = $configreader->read_configuration(new \moodle_url($baseurl));
@ -150,7 +154,11 @@ class moodlenet implements issuer_interface {
'scope' => $scopes
];
$client = new http_client();
$config = [];
if (defined('BEHAT_SITE_RUNNING')) {
$config['verify'] = false;
}
$client = new http_client($config);
$request = new Request(
'POST',
$url,

View File

@ -43,6 +43,9 @@ class user_field_mapping extends persistent {
* @return array
*/
private static function get_user_fields() {
global $CFG;
require_once($CFG->dirroot . '/user/profile/lib.php');
return array_merge(\core_user::AUTHSYNCFIELDS, ['picture', 'username'], get_profile_field_names());
}

View File

@ -2681,4 +2681,14 @@ $capabilities = array(
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => [],
],
// Allow users to share activities to MoodleNet.
'moodle/moodlenet:shareactivity' => [
'captype' => 'read',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => [
'editingteacher' => CAP_ALLOW,
'manager' => CAP_ALLOW,
]
],
);

View File

@ -3042,6 +3042,24 @@ $functions = array(
'type' => 'write',
'ajax' => true,
],
'core_moodlenet_send_activity' => [
'classname' => 'core\external\moodlenet_send_activity',
'description' => 'Send activity to MoodleNet',
'type' => 'read',
'ajax' => true,
],
'core_moodlenet_get_share_info_activity' => [
'classname' => 'core\external\moodlenet_get_share_info_activity',
'description' => 'Get information about an activity being shared',
'type' => 'read',
'ajax' => true,
],
'core_moodlenet_auth_check' => [
'classname' => 'core\external\moodlenet_auth_check',
'description' => 'Check a user has authorized for a given MoodleNet site',
'type' => 'write',
'ajax' => true,
],
);
$services = array(

View File

@ -23,6 +23,7 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core\moodlenet\utilities;
use core_contentbank\contentbank;
defined('MOODLE_INTERNAL') || die();
@ -4799,7 +4800,7 @@ class settings_navigation extends navigation_node {
* @return navigation_node|false
*/
protected function load_module_settings() {
global $CFG;
global $CFG, $USER;
if (!$this->page->cm && $this->context->contextlevel == CONTEXT_MODULE && $this->context->instanceid) {
$cm = get_coursemodule_from_id(false, $this->context->instanceid, 0, false, MUST_EXIST);
@ -4880,6 +4881,22 @@ class settings_navigation extends navigation_node {
$function($this, $modulenode);
}
// Send to MoodleNet.
$usercanshare = utilities::can_user_share($this->context->get_course_context(), $USER->id);
$issuerid = get_config('moodlenet', 'oauthservice');
$issuer = \core\oauth2\api::get_issuer($issuerid);
$isvalidinstance = utilities::is_valid_instance($issuer);
if ($usercanshare && $isvalidinstance) {
$this->page->requires->js_call_amd('core/moodlenet/send_resource', 'init');
$action = new action_link(new moodle_url(''), '', null, [
'data-action' => 'sendtomoodlenet',
'data-type' => 'activity',
'data-sharetype' => 'resource',
]);
$modulenode->add(get_string('moodlenet:sharetomoodlenet', 'moodle'),
$action, self::TYPE_SETTING, null, 'exportmoodlenet')->set_force_into_more_menu(true);
}
// Remove the module node if there are no children.
if ($modulenode->children->count() <= 0) {
$modulenode->remove();

View File

@ -726,11 +726,11 @@ abstract class oauth2_client extends curl {
}
/**
* Get access token.
* Get access token object.
*
* This is just a getter to read the private property.
*
* @return string
* @return stdClass
*/
public function get_accesstoken() {
return $this->accesstoken;

View File

@ -0,0 +1,57 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core/moodlenet/send_activity_modal_base
Modal to send activity to MoodleNet
Example context (json):
{
"uniqid": 123456,
"activitytype": "assignment",
"activityname": "Test assignment",
"sharetype": "resource"
}
}}
<div class="modal moodle-has-zindex" data-region="modal-container" aria-hidden="true" role="dialog">
<div class="modal-dialog moodlenet-share-dialog" role="document" data-region="modal" aria-labelledby="{{uniqid}}-moodlenet-modal-title" tabindex="0">
<div class="modal-content">
<div class="modal-header no-border" data-region="header">
<div id="{{uniqid}}-moodlenet-modal-title" class="modal-title" data-region="title">
<h5 class="sr-only modal-title">{{#str}} moodlenet:sharetomoodlenet, moodle {{/str}}</h5>
<div class="d-flex moodlenet-share-moodlenetinfo">
<span class="moodlenet-logo">{{#pix}} moodlenet, moodle, {{#str}} moodlenet:sharetomoodlenet, moodle {{/str}} {{/pix}}</span>
<span class="moodlenet-title">{{#str}} moodlenet:sharetomoodlenet, moodle {{/str}}</span>
</div>
</div>
<button type="button" class="close" data-action="hide" aria-label="{{#cleanstr}}closebuttontitle{{/cleanstr}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" data-region="body">
<div class="moodlenet-share-modal-content">
<div class="moodlenet-share-activity-info">
<span class="moodlenet-activity-type text-uppercase">{{activitytype}}</span>
<span class="moodlenet-activity-name">{{activityname}}</span>
</div>
<hr class="moodlenet-share-activity-info-hr">
<div class="moodlenet-share-notice">
{{#pix}} e/help, core, {{#str}} moodlenet:sharenotice, moodle, {{sharetype}} {{/str}}{{/pix}} {{#str}} moodlenet:sharenotice, moodle, {{sharetype}} {{/str}}
</div>
</div>
</div>
<div class="modal-footer" data-region="footer">
{{> core/moodlenet/send_activity_modal_footer_share }}
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,57 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core/moodlenet/send_activity_modal_done
This template renders done state for share modal.
Example context (json):
{
"success": true,
"sitesupporturl": "https://moodle.test/contactsitesupport.php"
}
}}
<div class="moodlenet-share-modal-content text-center">
<div class="moodlenet-circle-status{{#success}} success{{/success}}{{^success}} fail{{/success}}">
<span class="status-icon pt-5{{#success}} text-success{{/success}}{{^success}} text-danger{{/success}}">
{{#success}}
{{#pix}} t/check, core {{/pix}}
{{/success}}
{{^success}}
{{#pix}} fp/cross, theme {{/pix}}
{{/success}}
</span>
<span class="status-text font-weight-bold h5 pt-5">
{{#success}}
{{#str}} moodlenet:sharesuccesstitle, moodle {{/str}}
{{/success}}
{{^success}}
{{#str}} moodlenet:sharefailtitle, moodle {{/str}}
{{/success}}
</span>
</div>
<div class="result pt-2">
<span class="d-block h6 font-weight-normal pt-1 result-text">
{{#success}}
{{#str}} moodlenet:sharesuccesstext, moodle {{/str}}
{{/success}}
{{^success}}
{{#sitesupporturl}}
{{#str}} moodlenet:sharefailtextwithsitesupport, moodle, {{sitesupporturl}} {{/str}}
{{/sitesupporturl}}
{{^sitesupporturl}}
{{#str}} moodlenet:sharefailtext, moodle {{/str}}
{{/sitesupporturl}}
{{/success}}
</span>
</div>
</div>

View File

@ -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 core/moodlenet/send_activity_modal_footer_share
Render the footer for sharing activity
Example context (json):
{
"server": "MoodleNet Local"
}
}}
<div class="moodlenet-share-to">
{{#str}} moodlenet:sharingto, moodle {{/str}}<span class="font-weight-bold">{{server}}</span>
</div>
<div class="moodlenet-action-buttons">
<button type="button" class="btn btn-secondary" data-action="cancel">{{#str}} cancel, moodle {{/str}}</button>
<button type="button" class="btn btn-primary" data-action="share">{{#str}} share, moodle {{/str}}</button>
</div>

View File

@ -0,0 +1,26 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core/moodlenet/send_activity_modal_footer_view
Render the footer for sharing activity
Example context (json):
{
"resourseurl": "https://moodlenet.test/draft/activity.mbz"
}
}}
<div class="moodlenet-action-buttons">
<a class="btn btn-primary" href="{{resourseurl}}" target="_blank">
{{#str}} moodlenet:gotomoodlenet, moodle {{/str}}
</a>
</div>

View File

@ -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 core/moodlenet/send_activity_modal_packaging
This template renders the packaging stage for MoodleNet.
Example context (json):
{
}
}}
<div class="moodlenet-share-modal-content text-center p-5">
<div class="font-weight-bold h3 p-2">{{#str}} moodlenet:sharingstatus, moodle {{/str}}</div>
<div class="h5 font-weight-normal p-2 test-share-large">{{#str}} moodlenet:sharinglargefile, moodle {{/str}}</div>
<div class="p-5">
{{> core/loading }}
</div>
<div class="h5 font-weight-normal p-2">{{#str}} moodlenet:packagingandsending, moodle {{/str}}</div>
</div>

View File

@ -0,0 +1,56 @@
<?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/>.
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
use Moodle\BehatExtension\Exception\SkippedException;
require_once(__DIR__ . '/../../behat/behat_base.php');
/**
* Steps definitions related to MoodleNet.
*
* @package core
* @category test
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_moodlenet extends behat_base {
/**
* Check that the TEST_MOODLENET_MOCK_SERVER is defined, so we can connect to the mock server.
*
* @Given /^a MoodleNet mock server is configured$/
*/
public function mock_is_configured(): void {
if (!defined('TEST_MOODLENET_MOCK_SERVER')) {
throw new SkippedException(
'The TEST_MOODLENET_MOCK_SERVER constant must be defined to run MoodleNet tests'
);
}
}
/**
* Change the service base url to the TEST_MOODLENET_MOCK_SERVER url.
*
* @Given /^I change the MoodleNet field "(?P<field_string>(?:[^"]|\\")*)" to mock server$/
* @param string $field Field name
*/
public function change_service_base_url_to_mock_url(string $field): void {
$field = behat_field_manager::get_form_field_from_label($field, $this);
$field->set_value(TEST_MOODLENET_MOCK_SERVER);
}
}

View File

@ -0,0 +1,70 @@
@core
Feature: MoodleNet outbound send activity
In order to send activity to MoodleNet server
As a teacher
I need to be able package the activity and share to MoodleNet
Background:
Given I log in as "admin"
And a MoodleNet mock server is configured
And the following config values are set as admin:
| enablesharingtomoodlenet | 1 |
And I navigate to "Server > OAuth 2 services" in site administration
And I press "MoodleNet"
And I should see "Create new service: MoodleNet"
And I change the MoodleNet field "Service base URL" to mock server
And I press "Save changes"
And I navigate to "MoodleNet > MoodleNet outbound settings" in site administration
And I set the field "Auth 2 service" to "MoodleNet"
And I press "Save changes"
And the following course exists:
| name | Test course |
| shortname | C1 |
And the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
| teacher1 | Teacher | 1 | teacher1@example.com |
| manager1 | Manager | 1 | manager1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| manager1 | C1 | manager |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "activities" exist:
| activity | course | idnumber | name | intro |
| assign | C1 | assign1 | Test Assignment 1 | Test Assignment 1 |
Scenario: Share to MoodleNet menu only be available for teachers and managers
Given I am on the "Test Assignment 1" "assign activity" page logged in as student1
Then "Share to MoodleNet" "link" should not exist in current page administration
And I am on the "Test Assignment 1" "assign activity" page logged in as teacher1
And "Share to MoodleNet" "link" should exist in current page administration
And I am on the "Test Assignment 1" "assign activity" page logged in as manager1
And "Share to MoodleNet" "link" should exist in current page administration
Scenario: Share to MoodleNet menu only be available for user that has capability only
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/moodlenet:shareactivity | Prohibit | editingteacher | Course | C1 |
When I am on the "Test Assignment 1" "assign activity" page logged in as teacher1
Then "Share to MoodleNet" "link" should not exist in current page administration
And I am on the "Test Assignment 1" "assign activity" page logged in as manager1
And "Share to MoodleNet" "link" should exist in current page administration
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/moodlenet:shareactivity | Prohibit | manager | Course | C1 |
And I am on the "Test Assignment 1" "assign activity" page logged in as manager1
And "Share to MoodleNet" "link" should not exist in current page administration
@javascript
Scenario: User can share activity to MoodleNet
Given I am on the "Test Assignment 1" "assign activity" page logged in as teacher1
When I navigate to "Share to MoodleNet" in current page administration
Then I should see "Assignment" in the "Share to MoodleNet" "dialogue"
And I should see "Test Assignment 1" in the "Share to MoodleNet" "dialogue"
And I should see "You are sharing this to MoodleNet as a resource" in the "Share to MoodleNet" "dialogue"
And I click on "Share" "button" in the "Share to MoodleNet" "dialogue"
And I switch to "moodlenet_auth" window
And I press "Allow" and switch to main window
And I should see "Saved to MoodleNet drafts"
And "Go to MoodleNet drafts" "link" should exist in the "Share to MoodleNet" "dialogue"

View File

@ -0,0 +1,87 @@
<?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/>.
namespace core\external;
defined('MOODLE_INTERNAL') || die();
use core\oauth2\api;
use core_external\external_api;
use externallib_advanced_testcase;
global $CFG;
require_once($CFG->dirroot . '/lib/tests/moodlenet/helpers.php');
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
/**
* External functions test for moodlenet_auth_check.
*
* @package core
* @category test
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core\external\moodlenet_auth_check
*/
class moodlenet_auth_check_test extends externallib_advanced_testcase {
/**
* Test the behaviour of moodlenet_auth_check().
*
* @covers ::execute
*/
public function test_moodlenet_auth_check() {
global $CFG;
$this->resetAfterTest();
$this->setAdminUser();
$CFG->enablesharingtomoodlenet = true;
// Generate data.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$user = $generator->create_user();
$generator->enrol_user($user->id, $course->id, 'student');
// Create dummy issuer.
$issuer = \core\moodlenet\helpers::get_mock_issuer(0);
// Test with the user does not have permission.
$this->setUser($user);
$result = moodlenet_auth_check::execute($issuer->get('id'), $course->id);
$result = external_api::clean_returnvalue(moodlenet_auth_check::execute_returns(), $result);
$this->assertFalse($result['status']);
$this->assertNotEmpty($result['warnings']);
$this->assertEquals('errorpermission', $result['warnings'][0]['warningcode']);
// Test with the issuer is not enabled.
$this->setAdminUser();
$result = moodlenet_auth_check::execute($issuer->get('id'), $course->id);
$result = external_api::clean_returnvalue(moodlenet_auth_check::execute_returns(), $result);
$this->assertFalse($result['status']);
$this->assertNotEmpty($result['warnings']);
$this->assertEquals('errorissuernotenabled', $result['warnings'][0]['warningcode']);
// Test with the issuer is enabled and not logged in.
$issuer->set('enabled', 1);
$irecord = $issuer->to_record();
api::update_issuer($irecord);
set_config('oauthservice', $issuer->get('id'), 'moodlenet');
$result = moodlenet_auth_check::execute($issuer->get('id'), $course->id);
$result = external_api::clean_returnvalue(moodlenet_auth_check::execute_returns(), $result);
$this->assertFalse($result['status']);
$this->assertNotEmpty($result['loginurl']);
}
}

View File

@ -0,0 +1,140 @@
<?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/>.
namespace core\external;
use core\oauth2\api;
use core_external\external_api;
use externallib_advanced_testcase;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/lib/tests/moodlenet/helpers.php');
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
/**
* External functions test for moodlenet_get_share_info_activity.
*
* @package core
* @category test
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core\external\moodlenet_get_share_info_activity
*/
class moodlenet_get_share_info_activity_test extends externallib_advanced_testcase {
/**
* Test the behaviour of moodlenet_get_share_info_activity().
* @covers ::execute
*/
public function test_moodlenet_get_share_info_activity() {
global $CFG;
$this->resetAfterTest();
$this->setAdminUser();
$CFG->enablesharingtomoodlenet = true;
// Generate course and activities.
$course = $this->getDataGenerator()->create_course();
$activity1 = $this->getDataGenerator()->create_module('chat', ['course' => $course->id, 'name' => 'Chat activity']);
$activity2 = $this->getDataGenerator()->create_module('assign', ['course' => $course->id, 'name' => 'Assign activity']);
$activity3 = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id, 'name' => 'Quiz activity']);
// Create dummy enabled issuer.
$issuer = \core\moodlenet\helpers::get_mock_issuer(1);
// Test the 1st activity with no OAuth2 setup yet.
$result = moodlenet_get_share_info_activity::execute($activity1->cmid);
$result = external_api::clean_returnvalue(moodlenet_get_share_info_activity::execute_returns(), $result);
$this->assertFalse($result['status']);
$this->assertEmpty($result['name']);
$this->assertEmpty($result['type']);
$this->assertEmpty($result['server']);
$this->assertEmpty($result['supportpageurl']);
$this->assertNotEmpty($result['warnings']);
$this->assertEquals(0, $result['warnings'][0]['item']);
$this->assertEquals('errorissuernotset', $result['warnings'][0]['warningcode']);
$this->assertEquals(get_string('moodlenet:issuerisnotset', 'moodle'), $result['warnings'][0]['message']);
// Test the 1st activity with OAuth2 disabled.
set_config('oauthservice', $issuer->get('id'), 'moodlenet');
$issuer->set('enabled', 0);
$irecord = $issuer->to_record();
api::update_issuer($irecord);
$result = moodlenet_get_share_info_activity::execute($activity1->cmid);
$result = external_api::clean_returnvalue(moodlenet_get_share_info_activity::execute_returns(), $result);
$this->assertFalse($result['status']);
$this->assertEmpty($result['name']);
$this->assertEmpty($result['type']);
$this->assertEmpty($result['server']);
$this->assertEmpty($result['supportpageurl']);
$this->assertNotEmpty($result['warnings']);
$this->assertEquals($issuer->get('id'), $result['warnings'][0]['item']);
$this->assertEquals('errorissuernotenabled', $result['warnings'][0]['warningcode']);
$this->assertEquals(get_string('moodlenet:issuerisnotenabled', 'moodle'), $result['warnings'][0]['message']);
// Test the 1st activity with support url is set to the internal contact site support page.
$issuer->set('enabled', 1);
$irecord = $issuer->to_record();
api::update_issuer($irecord);
$expectedsupporturl = $CFG->wwwroot . '/user/contactsitesupport.php';
$result = moodlenet_get_share_info_activity::execute($activity1->cmid);
$result = external_api::clean_returnvalue(moodlenet_get_share_info_activity::execute_returns(), $result);
$this->assertTrue($result['status']);
$this->assertEquals($activity1->name, $result['name']);
$this->assertEquals(get_string('modulename', 'mod_chat'), $result['type']);
$this->assertEquals($issuer->get_display_name(), $result['server']);
$this->assertEquals($expectedsupporturl, $result['supportpageurl']);
// Test the 2nd activity with support url is set to the external contact site support page.
$expectedsupporturl = 'https://moodle.org/';
$CFG->supportpage = $expectedsupporturl;
$result = moodlenet_get_share_info_activity::execute($activity2->cmid);
$result = external_api::clean_returnvalue(moodlenet_get_share_info_activity::execute_returns(), $result);
$this->assertTrue($result['status']);
$this->assertEquals($activity2->name, $result['name']);
$this->assertEquals(get_string('modulename', 'mod_assign'), $result['type']);
$this->assertEquals($expectedsupporturl, $result['supportpageurl']);
// Test the 3rd activity with contact site support is disabled.
$CFG->supportavailability = CONTACT_SUPPORT_DISABLED;
$result = moodlenet_get_share_info_activity::execute($activity3->cmid);
$result = external_api::clean_returnvalue(moodlenet_get_share_info_activity::execute_returns(), $result);
$this->assertTrue($result['status']);
$this->assertEquals($activity3->name, $result['name']);
$this->assertEquals(get_string('modulename', 'mod_quiz'), $result['type']);
$this->assertEmpty($result['supportpageurl']);
// Test with an invalid activity.
// Get a random cmid that not in the created activity list.
$cmids = [$activity1->cmid, $activity2->cmid, $activity3->cmid];
do {
$randomcmid = random_int(5, 25);
} while (in_array($randomcmid, $cmids));
$result = moodlenet_get_share_info_activity::execute($randomcmid);
$result = external_api::clean_returnvalue(moodlenet_get_share_info_activity::execute_returns(), $result);
$this->assertFalse($result['status']);
$this->assertEmpty($result['name']);
$this->assertEmpty($result['type']);
$this->assertNotEmpty($result['warnings']);
$this->assertEquals($randomcmid, $result['warnings'][0]['item']);
$this->assertEquals('errorgettingactivityinformation', $result['warnings'][0]['warningcode']);
$this->assertEquals(get_string('invalidcoursemodule', 'error'), $result['warnings'][0]['message']);
}
}

View File

@ -0,0 +1,114 @@
<?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/>.
namespace core\external;
use core\oauth2\api;
use core_external\external_api;
use externallib_advanced_testcase;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/lib/tests/moodlenet/helpers.php');
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
/**
* External functions test for moodlenet_send_activity.
*
* @package core
* @category test
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core\external\moodlenet_send_activity
*/
class moodlenet_send_activity_test extends externallib_advanced_testcase {
/**
* Test the behaviour of moodlenet_send_activity().
*
* @covers ::execute
*/
public function test_moodlenet_send_activity() {
global $CFG;
$this->resetAfterTest();
$this->setAdminUser();
// Generate data.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$moduleinstance = $generator->create_module('assign', ['course' => $course->id]);
$user = $generator->create_user();
$generator->enrol_user($user->id, $course->id, 'student');
// Create dummy issuer.
$issuer = \core\moodlenet\helpers::get_mock_issuer(0);
// Test with the experimental flag off.
$result = moodlenet_send_activity::execute($issuer->get('id'), $moduleinstance->cmid, 0);
$result = external_api::clean_returnvalue(moodlenet_send_activity::execute_returns(), $result);
$this->assertFalse($result['status']);
$this->assertNotEmpty($result['warnings']);
$this->assertEquals('errorissuernotenabled', $result['warnings'][0]['warningcode']);
$CFG->enablesharingtomoodlenet = true;
// Test with invalid format.
$result = moodlenet_send_activity::execute($issuer->get('id'), $moduleinstance->cmid, 5);
$result = external_api::clean_returnvalue(moodlenet_send_activity::execute_returns(), $result);
$this->assertFalse($result['status']);
$this->assertNotEmpty($result['warnings']);
$this->assertEquals('errorinvalidformat', $result['warnings'][0]['warningcode']);
// Test with the user does not have permission.
$this->setUser($user);
$result = moodlenet_send_activity::execute($issuer->get('id'), $moduleinstance->cmid, 0);
$result = external_api::clean_returnvalue(moodlenet_send_activity::execute_returns(), $result);
$this->assertFalse($result['status']);
$this->assertNotEmpty($result['warnings']);
$this->assertEquals('errorpermission', $result['warnings'][0]['warningcode']);
$this->setAdminUser();
// Test with the issuer is not enabled.
$result = moodlenet_send_activity::execute($issuer->get('id'), $moduleinstance->cmid, 0);
$result = external_api::clean_returnvalue(moodlenet_send_activity::execute_returns(), $result);
$this->assertFalse($result['status']);
$this->assertNotEmpty($result['warnings']);
$this->assertEquals('errorissuernotenabled', $result['warnings'][0]['warningcode']);
// Test with the issuer is enabled but not set in the MN Outbound setting.
$issuer->set('enabled', 1);
$irecord = $issuer->to_record();
api::update_issuer($irecord);
$result = moodlenet_send_activity::execute($issuer->get('id'), $moduleinstance->cmid, 0);
$result = external_api::clean_returnvalue(moodlenet_send_activity::execute_returns(), $result);
$this->assertFalse($result['status']);
$this->assertNotEmpty($result['warnings']);
$this->assertEquals('errorissuernotenabled', $result['warnings'][0]['warningcode']);
set_config('oauthservice', $issuer->get('id'), 'moodlenet');
// Test with the issuer not yet authorized.
$result = moodlenet_send_activity::execute($issuer->get('id'), $moduleinstance->cmid, 0);
$result = external_api::clean_returnvalue(moodlenet_send_activity::execute_returns(), $result);
$this->assertFalse($result['status']);
$this->assertNotEmpty($result['warnings']);
$this->assertEquals('erroroauthclient', $result['warnings'][0]['warningcode']);
$this->assertEquals($issuer->get('id'), $result['warnings'][0]['item']);
$this->assertEquals(get_string('moodlenet:issuerisnotauthorized', 'moodle'), $result['warnings'][0]['message']);
}
}

View File

@ -0,0 +1,143 @@
<?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/>.
namespace core\moodlenet;
/**
* Unit tests for {@see activity_packager}.
*
* @coversDefaultClass \core\moodlenet\activity_packager
* @package core
* @copyright 2023 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_packager_test extends \advanced_testcase {
/**
* Test fetching and overriding a backup task setting.
*
* @covers ::override_task_setting
* @covers ::get_all_task_settings
* @return void
*/
public function test_override_task_setting() {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$assigndata = [
'course' => $course->id,
'name' => 'Extremely interesting assignment',
'intro' => 'A great assignment to share',
];
$assign = $generator->create_module('assign', $assigndata);
$cminfo = get_fast_modinfo($course->id)->get_cm($assign->cmid);
$packager = new activity_packager($cminfo, $USER->id);
// Fetch all backup task settings.
$rc = new \ReflectionClass(activity_packager::class);
$rcmgetall = $rc->getMethod('get_all_task_settings');
$rcmgetall->setAccessible(true);
$tasksettings = $rcmgetall->invoke($packager);
// Fetch the default settings and grab an example value (setting_root_users).
$rootsettings = $tasksettings[\backup_root_task::class];
$testsettingname = 'setting_root_users';
$oldvalue = 99;
foreach ($rootsettings as $setting) {
$name = $setting->get_ui_name();
if ($name == $testsettingname) {
$oldvalue = $setting->get_value();
break;
}
}
// Check we found the setting value (either 0 or 1 are valid).
$this->assertNotEquals(99, $oldvalue);
$this->assertLessThanOrEqual(1, $oldvalue);
// Override the setting_root_users value, then re-fetch the settings to check the change is reflected.
$overridevalue = ($oldvalue == 1) ? 0 : 1;
$rcmoverridesetting = $rc->getMethod('override_task_setting');
$rcmoverridesetting->setAccessible(true);
$rcmoverridesetting->invoke($packager, $tasksettings, $testsettingname, $overridevalue);
$tasksettings = $rcmgetall->invoke($packager);
$rootsettings = $tasksettings[\backup_root_task::class];
$newvalue = 99;
foreach ($rootsettings as $setting) {
$name = $setting->get_ui_name();
if ($name == $testsettingname) {
$newvalue = $setting->get_value();
break;
}
}
$this->assertEquals($overridevalue, $newvalue);
}
/**
* Test overriding a backup task setting.
*
* @covers ::get_package
* @covers ::package
* @return void
*/
public function test_get_package() {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
$currenttime = time();
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$coursecontext = \context_course::instance($course->id);
$assigndata = [
'course' => $course->id,
'name' => 'Extremely interesting assignment',
'intro' => 'A great assignment to share',
];
$assign = $generator->create_module('assign', $assigndata);
$cminfo = get_fast_modinfo($course->id)->get_cm($assign->cmid);
$packager = new activity_packager($cminfo, $USER->id);
$package = $packager->get_package();
$this->assertEquals(2, count($package));
// Confirm there are backup file contents returned.
$this->assertTrue(array_key_exists('filecontents', $package));
$this->assertNotEmpty($package['filecontents']);
// Confirm the expected stored_file object is returned.
$this->assertTrue(array_key_exists('storedfile', $package));
$this->assertInstanceOf(\stored_file::class, $package['storedfile']);
// Check some known values in the returned stored_file object to confirm they match the file we have packaged.
$this->assertNotEmpty($package['storedfile']->get_contenthash());
$this->assertEquals($coursecontext->id, $package['storedfile']->get_contextid());
$this->assertEquals('core', $package['storedfile']->get_component());
$this->assertEquals('moodlenet_resource', $package['storedfile']->get_filearea());
$this->assertEquals('assign_backup.mbz', $package['storedfile']->get_filename());
$this->assertGreaterThan(0, $package['storedfile']->get_filesize());
$timecreated = $package['storedfile']->get_timecreated();
$this->assertGreaterThanOrEqual($currenttime, $timecreated);
$this->assertEquals($timecreated, $package['storedfile']->get_timemodified());
}
}

View File

@ -0,0 +1,273 @@
<?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/>.
namespace core\moodlenet;
use context_course;
use core\http_client;
use core\oauth2\issuer;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Http\Message\ResponseInterface;
use ReflectionMethod;
use stdClass;
use testing_data_generator;
defined('MOODLE_INTERNAL') || die();
require_once('helpers.php');
/**
* Unit tests for {@see activity_sender}.
*
* @coversDefaultClass \core\moodlenet\activity_sender
* @package core
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity_sender_test extends \advanced_testcase {
/** @var testing_data_generator Data generator. */
private testing_data_generator $generator;
/** @var stdClass Course object. */
private stdClass $course;
/** @var stdClass Activity object, */
private stdClass $moduleinstance;
/** @var context_course Course context instance. */
private context_course $coursecontext;
/** @var issuer $issuer Dummy issuer. */
private issuer $issuer;
/** @var MockObject $mockoauthclient Mock OAuth client. */
private MockObject $mockoauthclient;
/**
* Set up function for tests.
*/
protected function setUp(): void {
parent::setUp();
$this->resetAfterTest();
// Get data generator.
$this->generator = $this->getDataGenerator();
// Create course.
$this->course = $this->generator->create_course();
$this->moduleinstance = $this->generator->create_module('assign', ['course' => $this->course->id]);
$this->coursecontext = context_course::instance($this->course->id);
// Create mock issuer.
$this->issuer = helpers::get_mock_issuer(1);
// Create mock builder for OAuth2 client.
$mockbuilder = $this->getMockBuilder('core\oauth2\client');
$mockbuilder->onlyMethods(['get_issuer', 'is_logged_in', 'get_accesstoken']);
$mockbuilder->setConstructorArgs([$this->issuer, '', '']);
// Get the OAuth2 client mock.
$this->mockoauthclient = $mockbuilder->getMock();
}
/**
* Test prepare_share_contents method.
*
* @covers ::prepare_share_contents
* @return void
*/
public function test_prepare_share_contents() {
global $USER;
$this->setAdminUser();
$httpclient = new http_client();
$moodlenetclient = new moodlenet_client($httpclient, $this->mockoauthclient);
// Set get_file method accessibility.
$method = new ReflectionMethod(activity_sender::class, 'prepare_share_contents');
$method->setAccessible(true);
// Test with invalid share format.
$this->expectException(\moodle_exception::class);
$this->expectExceptionMessage(get_string('moodlenet:invalidshareformat', 'error'));
$package = $method->invoke(new activity_sender(
$this->moduleinstance->cmid,
$USER->id,
$moodlenetclient,
$this->mockoauthclient,
random_int(5, 30)
));
// Test with valid share format and invalid course module.
$package = $method->invoke(new activity_sender(
random_int(5, 30),
$USER->id,
$moodlenetclient,
$this->mockoauthclient,
activity_sender::SHARE_FORMAT_BACKUP
));
$this->assertEmpty($package);
// Test with valid share format and valid course module.
$package = $method->invoke(new activity_sender(
$this->moduleinstance->cmid,
$USER->id,
$moodlenetclient,
$this->mockoauthclient,
activity_sender::SHARE_FORMAT_BACKUP
));
$this->assertNotEmpty($package);
// Confirm there are backup file contents returned.
$this->assertTrue(array_key_exists('filecontents', $package));
$this->assertNotEmpty($package['filecontents']);
// Confirm the expected stored_file object is returned.
$this->assertTrue(array_key_exists('storedfile', $package));
$this->assertInstanceOf(\stored_file::class, $package['storedfile']);
}
/**
* Test share_activity() method.
*
* @dataProvider share_activity_provider
* @covers ::share_activity
* @covers ::log_event
* @covers \core\moodlenet\moodlenet_client::create_resource_from_file
* @covers \core\moodlenet\moodlenet_client::prepare_file_share_request_data
* @param ResponseInterface $httpresponse
* @param array $expected
* @return void
*/
public function test_share_activity(ResponseInterface $httpresponse, array $expected) {
global $CFG, $USER;
$this->setAdminUser();
// Enable the experimental flag.
$CFG->enablesharingtomoodlenet = true;
// Set OAuth 2 service in the outbound setting to the dummy issuer.
set_config('oauthservice', $this->issuer->get('id'), 'moodlenet');
// Generate access token for the mock.
$accesstoken = new stdClass();
$accesstoken->token = random_string(64);
// Get the OAuth2 client mock and set the return value for necessary methods.
$this->mockoauthclient->method('get_issuer')->will($this->returnValue($this->issuer));
$this->mockoauthclient->method('is_logged_in')->will($this->returnValue(true));
$this->mockoauthclient->method('get_accesstoken')->will($this->returnValue($accesstoken));
// Create Guzzle mock.
$mockguzzlehandler = new MockHandler([$httpresponse]);
$handlerstack = HandlerStack::create($mockguzzlehandler);
$httpclient = new http_client(['handler' => $handlerstack]);
// Create events sink.
$sink = $this->redirectEvents();
// Create activity sender.
$moodlenetclient = new moodlenet_client($httpclient, $this->mockoauthclient);
$activitysender = new activity_sender(
$this->moduleinstance->cmid,
$USER->id,
$moodlenetclient,
$this->mockoauthclient,
activity_sender::SHARE_FORMAT_BACKUP
);
if (isset($expected['exception'])) {
$this->expectException(ClientException::class);
$this->expectExceptionMessage($expected['exception']);
}
// Call the API.
$result = $activitysender->share_activity();
// Verify the result.
$this->assertEquals($expected['response_code'], $result['responsecode']);
$this->assertEquals($expected['resource_url'], $result['drafturl']);
// Verify the events.
$events = $sink->get_events();
$event = reset($events);
$this->assertInstanceOf('\core\event\moodlenet_resource_exported', $event);
$this->assertEquals($USER->id, $event->userid);
if ($result['responsecode'] == 201) {
$description = "The user with id '{$USER->id}' successfully shared activities to MoodleNet with the " .
"following course module ids, from context with id '{$this->coursecontext->id}': '{$this->moduleinstance->cmid}'.";
} else {
$description = "The user with id '{$USER->id}' failed to share activities to MoodleNet with the " .
"following course module ids, from context with id '{$this->coursecontext->id}': '{$this->moduleinstance->cmid}'.";
}
$this->assertEquals($description, $event->get_description());
}
/**
* Provider for test share_activity().
*
* @return array Test data.
*/
public function share_activity_provider(): array {
return [
'Success' => [
'http_response' => new Response(
201,
['Content-Type' => 'application/json'],
json_encode([
'homepage' => 'https://moodlenet.example.com/drafts/view/activity_backup_1.mbz',
]),
),
'expected' => [
'response_code' => 201,
'resource_url' => 'https://moodlenet.example.com/drafts/view/activity_backup_1.mbz',
],
],
'Fail with 200 status code' => [
'http_response' => new Response(
200,
['Content-Type' => 'application/json'],
json_encode([
'homepage' => 'https://moodlenet.example.com/drafts/view/activity_backup_2.mbz',
]),
),
'expected' => [
'response_code' => 200,
'resource_url' => 'https://moodlenet.example.com/drafts/view/activity_backup_2.mbz',
],
],
'Fail with 401 status code' => [
'http_response' => new Response(
401,
),
'expected' => [
'response_code' => 401,
'resource_url' => '',
'exception' => 'Client error: ' .
'`POST https://moodlenet.example.com/.pkg/@moodlenet/ed-resource/basic/v1/create` ' .
'resulted in a `401 Unauthorized` response',
],
],
'Fail with 404 status code' => [
'http_response' => new Response(
404,
),
'expected' => [
'response_code' => 404,
'resource_url' => '',
'exception' => 'Client error: '.
'`POST https://moodlenet.example.com/.pkg/@moodlenet/ed-resource/basic/v1/create` ' .
'resulted in a `404 Not Found` response',
],
],
];
}
}

View File

@ -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/>.
namespace core\moodlenet;
use core\oauth2\issuer;
/**
* Helper for tests related to MoodleNet integration.
*
* @package core
* @copyright 2023 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class helpers {
/**
* Create and return a mock MoodleNet issuer.
*
* @param int $enabled Whether the issuer is enabled.
* @return issuer The issuer that has been created.
*/
public static function get_mock_issuer(int $enabled): issuer {
$record = (object) [
'name' => 'MoodleNet',
'image' => 'https://moodle.net/favicon.ico',
'baseurl' => 'https://moodlenet.example.com',
'loginscopes' => '',
'loginscopesoffline' => '',
'loginparamsoffline' => '',
'showonloginpage' => issuer::SERVICEONLY,
'servicetype' => 'moodlenet',
'enabled' => $enabled,
];
$issuer = new issuer(0, $record);
$issuer->create();
return $issuer;
}
}

View File

@ -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/>.
namespace core\moodlenet;
use context_course;
use stdClass;
use testing_data_generator;
/**
* Unit tests for {@see utilities}.
*
* @coversDefaultClass \core\moodlenet\utilities
* @package core
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class utilities_test extends \advanced_testcase {
/** @var testing_data_generator Data generator. */
private testing_data_generator $generator;
/** @var stdClass Activity object, */
private stdClass $course;
/** @var context_course Course context instance. */
private context_course $coursecontext;
/**
* Set up function for tests.
*/
protected function setUp(): void {
parent::setUp();
$this->resetAfterTest();
$this->generator = $this->getDataGenerator();
$this->course = $this->generator->create_course();
$this->coursecontext = context_course::instance($this->course->id);
}
/**
* Test is_valid_instance method.
*
* @covers ::is_valid_instance
* @return void
*/
public function test_is_valid_instance() {
global $CFG;
$this->setAdminUser();
// Create dummy issuer.
$issuer = new \core\oauth2\issuer(0);
$issuer->set('enabled', 0);
$issuer->set('servicetype', 'google');
// Can not share if the experimental flag it set to false.
$CFG->enablesharingtomoodlenet = false;
$this->assertFalse(utilities::is_valid_instance($issuer));
// Enable the experimental flag.
$CFG->enablesharingtomoodlenet = true;
// Can not share if the OAuth 2 service in the outbound setting is not matched the given one.
set_config('oauthservice', random_int(1, 30), 'moodlenet');
$this->assertFalse(utilities::is_valid_instance($issuer));
// Can not share if the OAuth 2 service in the outbound setting is not enabled.
set_config('oauthservice', $issuer->get('id'), 'moodlenet');
$this->assertFalse(utilities::is_valid_instance($issuer));
// Can not share if the OAuth 2 service type is not moodlenet.
$issuer->set('enabled', 1);
$this->assertFalse(utilities::is_valid_instance($issuer));
// All good now.
$issuer->set('servicetype', 'moodlenet');
$this->assertTrue(utilities::is_valid_instance($issuer));
}
/**
* Test can_user_share method.
*
* @covers ::can_user_share
* @return void
*/
public function test_can_user_share() {
global $DB;
// Generate data.
$student1 = $this->generator->create_user();
$teacher1 = $this->generator->create_user();
$teacher2 = $this->generator->create_user();
$manager1 = $this->generator->create_user();
// Enrol users.
$this->generator->enrol_user($student1->id, $this->course->id, 'student');
$this->generator->enrol_user($teacher1->id, $this->course->id, 'teacher');
$this->generator->enrol_user($teacher2->id, $this->course->id, 'editingteacher');
$this->generator->enrol_user($manager1->id, $this->course->id, 'manager');
// Get roles.
$teacherrole = $DB->get_record('role', ['shortname' => 'teacher'], 'id', MUST_EXIST);
$editingteacherrole = $DB->get_record('role', ['shortname' => 'editingteacher'], 'id', MUST_EXIST);
// Test with default settings.
// Student and Teacher cannot share the activity.
$this->assertFalse(utilities::can_user_share($this->coursecontext, $student1->id));
$this->assertFalse(utilities::can_user_share($this->coursecontext, $teacher1->id));
// Editing-teacher and Manager can share the activity.
$this->assertTrue(utilities::can_user_share($this->coursecontext, $teacher2->id));
$this->assertTrue(utilities::can_user_share($this->coursecontext, $manager1->id));
// Teacher who has the capabilities can share the activity.
assign_capability('moodle/moodlenet:shareactivity', CAP_ALLOW, $teacherrole->id, $this->coursecontext);
assign_capability('moodle/backup:backupactivity', CAP_ALLOW, $teacherrole->id, $this->coursecontext);
$this->assertTrue(utilities::can_user_share($this->coursecontext, $teacher1->id));
// Editing-teacher who does not have the capabilities can not share the activity.
assign_capability('moodle/moodlenet:shareactivity', CAP_PROHIBIT, $editingteacherrole->id, $this->coursecontext);
$this->assertFalse(utilities::can_user_share($this->coursecontext, $teacher2->id));
}
}

BIN
pix/moodlenet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

1
pix/moodlenet.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4890.54 912"><defs><style>.cls-1{fill:#f17f21;}.cls-2{fill:#333;}.cls-3,.cls-4{fill:#424343;}.cls-4{stroke:#424343;stroke-miterlimit:10;stroke-width:17.01px;}</style></defs><g><path class="cls-4" d="m3231.77,257.7c0-17.7,15.17-32.88,32.87-32.88h9.27c15.18,0,24.45,7.59,33.72,19.4l358.29,456.92v-445.12c0-17.7,14.33-32.88,32.88-32.88s32.03,15.18,32.03,32.88v532.8c0,17.7-12.65,31.19-30.35,31.19h-3.37c-14.33,0-24.45-8.43-34.57-20.23l-365.87-467.89v456.93c0,17.7-14.34,32.87-32.88,32.87s-32.03-15.17-32.03-32.87V257.7Z"/><path class="cls-4" d="m4026.76,821.59c-119.72,0-217.5-91.89-217.5-226.78v-1.69c0-125.61,88.51-226.78,209.07-226.78,128.99,0,202.33,105.38,202.33,220.87,0,17.71-14.33,30.36-30.35,30.36h-315.3c9.27,94.42,75.88,147.53,153.44,147.53,53.95,0,93.58-21.08,125.61-50.58,5.06-4.22,10.96-7.59,18.55-7.59,16.02,0,28.66,12.65,28.66,27.82,0,7.59-3.37,15.18-10.12,21.08-41.31,39.62-90.2,65.75-164.39,65.75Zm128.98-252.07c-6.74-79.25-52.27-148.37-139.1-148.37-75.87,0-133.2,63.23-141.63,148.37h280.73Z"/><path class="cls-4" d="m4307.5,695.98v-263.03h-35.41c-15.18,0-28.66-13.48-28.66-28.66,0-16.02,13.48-28.66,28.66-28.66h35.41v-102.85c0-17.7,13.49-32.87,32.03-32.87s32.88,15.17,32.88,32.87v102.85h112.97c16.02,0,29.51,13.49,29.51,28.66,0,16.02-13.49,28.66-29.51,28.66h-112.97v254.6c0,53.11,29.51,72.5,73.34,72.5,22.77,0,34.57-5.9,39.62-5.9,15.18,0,27.82,12.65,27.82,27.82,0,11.81-7.59,21.07-18.55,25.29-18.55,7.59-38.78,11.81-64.07,11.81-69.97,0-123.09-34.57-123.09-123.09Z"/></g><g><path class="cls-1" d="m782.79,820.83v-275.2c0-57.53-23.77-86.31-71.28-86.31s-71.31,28.78-71.31,86.31v275.2h-140.1v-275.2c0-57.53-23.37-86.31-70.08-86.31s-71.28,28.78-71.28,86.31v275.2h-140.12v-291.45c0-60.05,20.84-105.5,62.54-136.33,36.7-27.53,86.34-41.31,148.86-41.31s110.08,16.27,140.12,48.8c25.84-32.53,72.95-48.8,141.36-48.8,62.54,0,112.13,13.78,148.83,41.31,41.69,30.83,62.54,76.28,62.54,136.33v291.45h-140.1Z"/><path class="cls-1" d="m2499.9,819.55V33.38h140.22v786.17h-140.22Z"/><path class="cls-1" d="m2321.2,819.55v-46.31c-12.54,16.68-33.81,30.03-63.84,40.05-26.73,9.17-52.17,13.77-76.38,13.77-66.76,0-120.37-22.95-160.85-68.86-40.48-45.88-60.73-102.66-60.73-170.24s19.71-122.79,59.48-167.75c35.2-39.8,92.41-67.6,158.35-67.6,74.26,0,117.64,27.88,143.97,60.08V33.38h136.45v786.17h-136.45Zm0-261.63c0-25.04-11.91-49.04-35.69-72-23.76-22.95-48.19-34.44-73.22-34.44-35.88,0-64.26,14.64-85.13,43.83-18.36,25.91-27.54,57.58-27.54,95.14s9.17,68.05,27.54,93.9c20.86,30.07,49.25,45.06,85.13,45.06,21.69,0,45.26-10.2,70.72-30.66,25.46-20.44,38.19-41.49,38.19-63.22v-77.61Z"/><path class="cls-1" d="m1695.29,832.07c-70.96,0-129.57-22.54-175.89-67.61-46.32-45.06-69.5-103.04-69.5-174.02s23.17-128.93,69.5-173.99c46.32-45.08,104.93-67.6,175.89-67.6s129.78,22.53,176.5,67.6c46.73,45.06,70.11,103.09,70.11,173.99s-23.39,128.95-70.11,174.02c-46.73,45.08-105.59,67.61-176.5,67.61Zm0-370.56c-33.81,0-59.79,12.8-77.94,38.38-18.15,25.6-27.21,55.99-27.21,91.21s8.39,65,25.32,89.31c19.43,27.68,46.02,41.48,79.82,41.48s60.35-13.8,79.8-41.48c17.73-24.32,26.59-54.1,26.59-89.31s-8.44-65.01-25.32-89.31c-19.45-26.85-46.47-40.28-81.07-40.28Z"/><path class="cls-1" d="m1188.29,832.07c-70.94,0-129.57-22.54-175.89-67.61-46.32-45.06-69.47-103.04-69.47-174.02s23.15-128.93,69.47-173.99c46.32-45.08,104.95-67.6,175.89-67.6s129.75,22.53,176.53,67.6c46.73,45.06,70.09,103.09,70.09,173.99s-23.36,128.95-70.09,174.02c-46.77,45.08-105.61,67.61-176.53,67.61Zm0-370.56c-33.78,0-59.76,12.8-77.94,38.38-18.13,25.6-27.23,55.99-27.23,91.21s8.44,65,25.32,89.31c19.43,27.68,46.07,41.48,79.85,41.48s60.35-13.8,79.8-41.48c17.73-24.32,26.62-54.1,26.62-89.31s-8.46-65.01-25.34-89.31c-19.43-26.85-46.44-40.28-81.07-40.28Z"/><path class="cls-1" d="m2806.18,633.03c3,33.38,46.38,105.14,117.71,105.14s102.26-40.08,103.89-56.33l147.72-1.25c-16.11,49.28-81.61,153.99-254.16,153.99-71.78,0-137.46-22.34-184.64-66.98-47.15-44.65-70.72-102.84-70.72-174.64s23.57-133.31,70.72-177.13c47.17-43.83,108.3-65.73,183.41-65.73,81.78,0,145.62,27.12,191.52,81.37,42.58,50.07,63.86,117.26,63.86,201.55h-369.32Zm229.1-86.39c-5.85-30.06-18.36-54.63-37.53-73.86-21.73-20.85-46.77-31.3-75.13-31.3s-53.63,10-73.25,30.03c-19.61,20.04-32.75,45.06-39.42,75.12h225.33Z"/><path class="cls-2" d="m490.78,330.55l139.17-101.6-1.79-6.21c-251.04,30.71-365.28,52.52-581.56,177.88l2,5.7,17.2.16c-1.6,17.32-4.33,60.11-.81,124.44-24,69.46-.61,116.64,21.35,167.94,3.47-53.46,3.11-111.86-13.29-170.03-3.41-63.89-.59-105.93.95-122.27l143.37,1.37s-.94,43.3,4.24,84.06c128.13,45.02,256.97-.15,325.33-111.13-18.92-21.28-56.16-50.31-56.16-50.31Z"/></g><path class="cls-3" d="m4633.7,836.41h-19.36v-96.8h-32.89v-16.68h85.46v16.68h-33.21v96.8Zm94.44,0l-31.64-87.98h-1.26l.94,20.3v67.68h-18.73v-113.48h29.43l30.85,87.98,31.95-87.98h28.33v113.48h-19.99v-66.1l.94-21.56h-1.26l-33.21,87.67h-16.37Z"/></svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -49,3 +49,4 @@ $breadcrumb-divider-rtl: "◀" !default;
@import "moodle/secondarynavigation";
@import "moodle/tertiarynavigation";
@import "moodle/process-monitor";
@import "moodle/moodlenet";

View File

@ -0,0 +1,102 @@
.moodlenet-share-dialog {
min-height: 500px;
.modal-header {
.moodlenet-share-moodlenetinfo {
align-items: baseline;
.moodlenet-logo {
display: flex;
.icon {
width: auto;
height: 1.3rem;
}
}
.moodlenet-title {
display: flex;
padding-left: 0.5em;
}
}
&.no-border {
border-bottom: none;
}
&.no-header-text {
.moodlenet-share-moodlenetinfo {
.moodlenet-title {
display: none;
}
}
}
}
.modal-body {
.moodlenet-share-activity-info {
@include border-radius();
@include alert-variant($primary-light-background, $primary-light-border, $body-color);
border-width: 1px;
border-style: solid;
padding: 0.6em 1.5em;
margin-bottom: 1rem;
.moodlenet-share-activity-info-hr {
border-bottom: 1px solid $gray-300;
}
.moodlenet-activity-type,
.moodlenet-activity-name {
display: block;
}
}
.moodlenet-share-notice {
background-color: #f8f9fa;
padding: 1rem;
}
.moodlenet-share-modal-content {
.loading-icon {
.icon {
width: 60px;
height: 60px;
font-size: 60px;
}
}
.moodlenet-circle-status {
height: 18rem;
margin: auto;
&.success {
background: radial-gradient(circle, rgba(25, 143, 81, 0.1) 9rem, transparent 9rem);
}
&.fail {
background: radial-gradient(circle, rgba(202, 49, 32, 0.1) 9rem, transparent 9rem);
}
span {
display: block;
margin: auto;
&.status-icon {
.icon {
font-size: 8rem;
width: auto;
margin: 0;
}
}
}
}
}
}
.modal-footer {
.moodlenet-share-to {
margin-right: auto;
}
}
}

View File

@ -37741,6 +37741,84 @@ div.editor_atto_toolbar button .icon {
border-bottom: 0;
}
.moodlenet-share-dialog {
min-height: 500px;
}
.moodlenet-share-dialog .modal-header .moodlenet-share-moodlenetinfo {
align-items: baseline;
}
.moodlenet-share-dialog .modal-header .moodlenet-share-moodlenetinfo .moodlenet-logo {
display: flex;
}
.moodlenet-share-dialog .modal-header .moodlenet-share-moodlenetinfo .moodlenet-logo .icon {
width: auto;
height: 1.3rem;
}
.moodlenet-share-dialog .modal-header .moodlenet-share-moodlenetinfo .moodlenet-title {
display: flex;
padding-left: 0.5em;
}
.moodlenet-share-dialog .modal-header.no-border {
border-bottom: none;
}
.moodlenet-share-dialog .modal-header.no-header-text .moodlenet-share-moodlenetinfo .moodlenet-title {
display: none;
}
.moodlenet-share-dialog .modal-body .moodlenet-share-activity-info {
border-radius: 0.5rem;
color: #1d2125;
background-color: #f5f9fc;
border-color: #3584c9;
border-width: 1px;
border-style: solid;
padding: 0.6em 1.5em;
margin-bottom: 1rem;
}
.moodlenet-share-dialog .modal-body .moodlenet-share-activity-info hr {
border-top-color: #3077b5;
}
.moodlenet-share-dialog .modal-body .moodlenet-share-activity-info .alert-link {
color: #070808;
}
.moodlenet-share-dialog .modal-body .moodlenet-share-activity-info .moodlenet-share-activity-info-hr {
border-bottom: 1px solid #dee2e6;
}
.moodlenet-share-dialog .modal-body .moodlenet-share-activity-info .moodlenet-activity-type,
.moodlenet-share-dialog .modal-body .moodlenet-share-activity-info .moodlenet-activity-name {
display: block;
}
.moodlenet-share-dialog .modal-body .moodlenet-share-notice {
background-color: #f8f9fa;
padding: 1rem;
}
.moodlenet-share-dialog .modal-body .moodlenet-share-modal-content .loading-icon .icon {
width: 60px;
height: 60px;
font-size: 60px;
}
.moodlenet-share-dialog .modal-body .moodlenet-share-modal-content .moodlenet-circle-status {
height: 18rem;
margin: auto;
}
.moodlenet-share-dialog .modal-body .moodlenet-share-modal-content .moodlenet-circle-status.success {
background: radial-gradient(circle, rgba(25, 143, 81, 0.1) 9rem, transparent 9rem);
}
.moodlenet-share-dialog .modal-body .moodlenet-share-modal-content .moodlenet-circle-status.fail {
background: radial-gradient(circle, rgba(202, 49, 32, 0.1) 9rem, transparent 9rem);
}
.moodlenet-share-dialog .modal-body .moodlenet-share-modal-content .moodlenet-circle-status span {
display: block;
margin: auto;
}
.moodlenet-share-dialog .modal-body .moodlenet-share-modal-content .moodlenet-circle-status span.status-icon .icon {
font-size: 8rem;
width: auto;
margin: 0;
}
.moodlenet-share-dialog .modal-footer .moodlenet-share-to {
margin-right: auto;
}
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;

View File

@ -37675,6 +37675,84 @@ div.editor_atto_toolbar button .icon {
border-bottom: 0;
}
.moodlenet-share-dialog {
min-height: 500px;
}
.moodlenet-share-dialog .modal-header .moodlenet-share-moodlenetinfo {
align-items: baseline;
}
.moodlenet-share-dialog .modal-header .moodlenet-share-moodlenetinfo .moodlenet-logo {
display: flex;
}
.moodlenet-share-dialog .modal-header .moodlenet-share-moodlenetinfo .moodlenet-logo .icon {
width: auto;
height: 1.3rem;
}
.moodlenet-share-dialog .modal-header .moodlenet-share-moodlenetinfo .moodlenet-title {
display: flex;
padding-left: 0.5em;
}
.moodlenet-share-dialog .modal-header.no-border {
border-bottom: none;
}
.moodlenet-share-dialog .modal-header.no-header-text .moodlenet-share-moodlenetinfo .moodlenet-title {
display: none;
}
.moodlenet-share-dialog .modal-body .moodlenet-share-activity-info {
border-radius: 0.25rem;
color: #1d2125;
background-color: #f5f9fc;
border-color: #3584c9;
border-width: 1px;
border-style: solid;
padding: 0.6em 1.5em;
margin-bottom: 1rem;
}
.moodlenet-share-dialog .modal-body .moodlenet-share-activity-info hr {
border-top-color: #3077b5;
}
.moodlenet-share-dialog .modal-body .moodlenet-share-activity-info .alert-link {
color: #070808;
}
.moodlenet-share-dialog .modal-body .moodlenet-share-activity-info .moodlenet-share-activity-info-hr {
border-bottom: 1px solid #dee2e6;
}
.moodlenet-share-dialog .modal-body .moodlenet-share-activity-info .moodlenet-activity-type,
.moodlenet-share-dialog .modal-body .moodlenet-share-activity-info .moodlenet-activity-name {
display: block;
}
.moodlenet-share-dialog .modal-body .moodlenet-share-notice {
background-color: #f8f9fa;
padding: 1rem;
}
.moodlenet-share-dialog .modal-body .moodlenet-share-modal-content .loading-icon .icon {
width: 60px;
height: 60px;
font-size: 60px;
}
.moodlenet-share-dialog .modal-body .moodlenet-share-modal-content .moodlenet-circle-status {
height: 18rem;
margin: auto;
}
.moodlenet-share-dialog .modal-body .moodlenet-share-modal-content .moodlenet-circle-status.success {
background: radial-gradient(circle, rgba(25, 143, 81, 0.1) 9rem, transparent 9rem);
}
.moodlenet-share-dialog .modal-body .moodlenet-share-modal-content .moodlenet-circle-status.fail {
background: radial-gradient(circle, rgba(202, 49, 32, 0.1) 9rem, transparent 9rem);
}
.moodlenet-share-dialog .modal-body .moodlenet-share-modal-content .moodlenet-circle-status span {
display: block;
margin: auto;
}
.moodlenet-share-dialog .modal-body .moodlenet-share-modal-content .moodlenet-circle-status span.status-icon .icon {
font-size: 8rem;
width: auto;
margin: 0;
}
.moodlenet-share-dialog .modal-footer .moodlenet-share-to {
margin-right: auto;
}
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;

View File

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2023041400.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2023041400.03; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '4.2beta (Build: 20230414)'; // Human-friendly version name