From 58ced4add08fedf9d7ee1a2e8ad6eaf9da1af40d Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Wed, 1 Mar 2023 10:18:26 +0700 Subject: [PATCH] MDL-75316 core_moodlenet: Share to MoodleNet modal and activity menu Originally implemented as MDL-75785 --- admin/moodlenet_oauth2_callback.php | 57 ++++++ lang/en/moodle.php | 18 ++ lib/amd/build/moodlenet/oauth2callback.min.js | 11 ++ .../build/moodlenet/oauth2callback.min.js.map | 1 + .../moodlenet/send_activity_modal.min.js | 3 + .../moodlenet/send_activity_modal.min.js.map | 1 + lib/amd/build/moodlenet/send_resource.min.js | 11 ++ .../build/moodlenet/send_resource.min.js.map | 1 + lib/amd/build/moodlenet/service.min.js | 11 ++ lib/amd/build/moodlenet/service.min.js.map | 1 + lib/amd/src/moodlenet/oauth2callback.js | 60 ++++++ lib/amd/src/moodlenet/send_activity_modal.js | 44 +++++ lib/amd/src/moodlenet/send_resource.js | 186 ++++++++++++++++++ lib/amd/src/moodlenet/service.js | 84 ++++++++ lib/classes/external/moodlenet_auth_check.php | 147 ++++++++++++++ .../moodlenet_get_share_info_activity.php | 160 +++++++++++++++ lib/classes/navigation/views/secondary.php | 1 + lib/classes/oauth2/service/moodlenet.php | 12 +- lib/classes/oauth2/user_field_mapping.php | 3 + lib/db/services.php | 12 ++ lib/navigationlib.php | 19 +- .../send_activity_modal_base.mustache | 57 ++++++ .../send_activity_modal_done.mustache | 57 ++++++ .../send_activity_modal_footer_share.mustache | 28 +++ .../send_activity_modal_footer_view.mustache | 26 +++ .../send_activity_modal_packaging.mustache | 28 +++ lib/tests/behat/moodlenet_outbound.feature | 70 +++++++ .../external/moodlenet_auth_check_test.php | 87 ++++++++ ...moodlenet_get_share_info_activity_test.php | 140 +++++++++++++ pix/moodlenet.png | Bin 0 -> 79949 bytes pix/moodlenet.svg | 1 + theme/boost/scss/moodle.scss | 1 + theme/boost/scss/moodle/moodlenet.scss | 102 ++++++++++ theme/boost/style/moodle.css | 78 ++++++++ theme/classic/style/moodle.css | 78 ++++++++ version.php | 2 +- 36 files changed, 1594 insertions(+), 4 deletions(-) create mode 100644 admin/moodlenet_oauth2_callback.php create mode 100644 lib/amd/build/moodlenet/oauth2callback.min.js create mode 100644 lib/amd/build/moodlenet/oauth2callback.min.js.map create mode 100644 lib/amd/build/moodlenet/send_activity_modal.min.js create mode 100644 lib/amd/build/moodlenet/send_activity_modal.min.js.map create mode 100644 lib/amd/build/moodlenet/send_resource.min.js create mode 100644 lib/amd/build/moodlenet/send_resource.min.js.map create mode 100644 lib/amd/build/moodlenet/service.min.js create mode 100644 lib/amd/build/moodlenet/service.min.js.map create mode 100644 lib/amd/src/moodlenet/oauth2callback.js create mode 100644 lib/amd/src/moodlenet/send_activity_modal.js create mode 100644 lib/amd/src/moodlenet/send_resource.js create mode 100644 lib/amd/src/moodlenet/service.js create mode 100644 lib/classes/external/moodlenet_auth_check.php create mode 100644 lib/classes/external/moodlenet_get_share_info_activity.php create mode 100644 lib/templates/moodlenet/send_activity_modal_base.mustache create mode 100644 lib/templates/moodlenet/send_activity_modal_done.mustache create mode 100644 lib/templates/moodlenet/send_activity_modal_footer_share.mustache create mode 100644 lib/templates/moodlenet/send_activity_modal_footer_view.mustache create mode 100644 lib/templates/moodlenet/send_activity_modal_packaging.mustache create mode 100644 lib/tests/behat/moodlenet_outbound.feature create mode 100644 lib/tests/external/moodlenet_auth_check_test.php create mode 100644 lib/tests/external/moodlenet_get_share_info_activity_test.php create mode 100644 pix/moodlenet.png create mode 100644 pix/moodlenet.svg create mode 100644 theme/boost/scss/moodle/moodlenet.scss diff --git a/admin/moodlenet_oauth2_callback.php b/admin/moodlenet_oauth2_callback.php new file mode 100644 index 00000000000..e5481075d1a --- /dev/null +++ b/admin/moodlenet_oauth2_callback.php @@ -0,0 +1,57 @@ +. + +/** + * MoodleNet callback. + * + * @package core + * @copyright 2023 Huong Nguyen + * @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, 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(); diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 68bff3befee..f737937547d 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1365,10 +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 create 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.
Please try again later.'; +$string['moodlenet:sharefailtextwithsitesupport'] = 'There was an error sharing your content to MoodleNet.
Please try again later or contact site support.'; +$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'; @@ -1974,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'; diff --git a/lib/amd/build/moodlenet/oauth2callback.min.js b/lib/amd/build/moodlenet/oauth2callback.min.js new file mode 100644 index 00000000000..d7792185dfe --- /dev/null +++ b/lib/amd/build/moodlenet/oauth2callback.min.js @@ -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 + * @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 \ No newline at end of file diff --git a/lib/amd/build/moodlenet/oauth2callback.min.js.map b/lib/amd/build/moodlenet/oauth2callback.min.js.map new file mode 100644 index 00000000000..56172ac9cb6 --- /dev/null +++ b/lib/amd/build/moodlenet/oauth2callback.min.js.map @@ -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 .\n\n/**\n * A module to handle the OAuth2 callback for MoodleNet.\n *\n * @module core/moodlenet/oauth2callback\n * @copyright 2023 Huong Nguyen \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"} \ No newline at end of file diff --git a/lib/amd/build/moodlenet/send_activity_modal.min.js b/lib/amd/build/moodlenet/send_activity_modal.min.js new file mode 100644 index 00000000000..fcab79d0be5 --- /dev/null +++ b/lib/amd/build/moodlenet/send_activity_modal.min.js @@ -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 \ No newline at end of file diff --git a/lib/amd/build/moodlenet/send_activity_modal.min.js.map b/lib/amd/build/moodlenet/send_activity_modal.min.js.map new file mode 100644 index 00000000000..9dc04ef26e2 --- /dev/null +++ b/lib/amd/build/moodlenet/send_activity_modal.min.js.map @@ -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 .\n\n/**\n * Send activity modal for MoodleNet.\n *\n * @module core/moodlenet/send_activity_modal\n * @copyright 2023 Huong Nguyen \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"} \ No newline at end of file diff --git a/lib/amd/build/moodlenet/send_resource.min.js b/lib/amd/build/moodlenet/send_resource.min.js new file mode 100644 index 00000000000..24514a3da1b --- /dev/null +++ b/lib/amd/build/moodlenet/send_resource.min.js @@ -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 + * @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,courseId,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,courseId,cmId,shareFormat).then((async data=>{const status=data.status,resourceUrl=data.resourceurl;return responseFromMoodleNet(status,resourceUrl)})).catch(_notification.exception)})(issuerId,courseId,cmId,shareFormat):(window.moodleNetAuthorize=error=>{""==error?handleAuthorization(issuerId):"access_denied"!==error&&responseFromMoodleNet(!1,"")},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 \ No newline at end of file diff --git a/lib/amd/build/moodlenet/send_resource.min.js.map b/lib/amd/build/moodlenet/send_resource.min.js.map new file mode 100644 index 00000000000..a16834b1994 --- /dev/null +++ b/lib/amd/build/moodlenet/send_resource.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"send_resource.min.js","sources":["../../src/moodlenet/send_resource.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 .\n\n/**\n * A module to handle Share operations of the MoodleNet.\n *\n * @module core/moodlenet/send_resource\n * @copyright 2023 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.2\n */\n\nimport Config from 'core/config';\nimport ModalFactory from 'core/modal_factory';\nimport {exception as displayException, addNotification} from 'core/notification';\nimport {get_string as getString} from 'core/str';\nimport Prefetch from \"core/prefetch\";\nimport * as Templates from 'core/templates';\nimport * as MoodleNetService from 'core/moodlenet/service';\nimport SendActivityModal from 'core/moodlenet/send_activity_modal';\n\nconst TYPE_ACTIVITY = \"activity\";\n\nlet currentModal;\nlet siteSupportUrl;\nlet issuerId;\nlet courseId;\nlet cmId;\nlet shareFormat;\n\n/**\n * Handle send to MoodleNet.\n *\n * @param {int} issuerId The OAuth 2 issuer ID.\n * @param {int} courseId The course ID.\n * @param {int} cmId The course module ID.\n * @param {int} shareFormat The share format.\n */\nconst sendToMoodleNet = (issuerId, courseId, cmId, shareFormat) => {\n const $modal = currentModal.getModal();\n const modal = $modal[0];\n modal.querySelector('.modal-header').classList.remove('no-border');\n modal.querySelector('.modal-header').classList.add('no-header-text');\n\n currentModal.setBody(Templates.render('core/moodlenet/send_activity_modal_packaging', {}));\n currentModal.hideFooter();\n\n MoodleNetService.sendActivity(issuerId, courseId, cmId, shareFormat).then(async(data) => {\n const status = data.status;\n const resourceUrl = data.resourceurl;\n return responseFromMoodleNet(status, resourceUrl);\n }).catch(displayException);\n};\n\n/**\n * Handle response from MoodleNet.\n *\n * @param {boolean} status Response status. True if successful.\n * @param {String} resourceUrl Resource URL.\n */\nconst responseFromMoodleNet = (status, resourceUrl = '') => {\n const $modal = currentModal.getModal();\n const modal = $modal[0];\n modal.querySelector('.modal-header').classList.add('no-border');\n currentModal.setBody(Templates.render('core/moodlenet/send_activity_modal_done', {\n success: status,\n sitesupporturl: siteSupportUrl,\n }));\n\n if (status) {\n currentModal.setFooter(Templates.render('core/moodlenet/send_activity_modal_footer_view', {\n resourseurl: resourceUrl,\n }));\n currentModal.showFooter();\n }\n};\n\n/**\n * Handle authorization with MoodleNet server.\n *\n * @param {int} issuerId The OAuth 2 issuer ID.\n * @return {promise}\n */\nconst handleAuthorization = (issuerId) => {\n const windowsizewidth = 550;\n const windowsizeheight = 550;\n\n // Check if the user is authorized with MoodleNet or not.\n return MoodleNetService.authorizationCheck(issuerId, courseId).then(async(data) => {\n if (!data.status) {\n // Not yet authorized.\n // Declare moodleNetAuthorize variable, so we can call it later in the callback.\n window.moodleNetAuthorize = (error) => {\n // This will be called by the callback after the authorization is successful.\n if (error == '') {\n handleAuthorization(issuerId);\n } else if (error !== 'access_denied') {\n responseFromMoodleNet(false, '');\n }\n };\n // Open the login url of the OAuth 2 issuer for user to login into MoodleNet and authorize.\n return window.open(data.loginurl, 'moodlenet_auth',\n `location=0,status=0,width=${windowsizewidth},height=${windowsizeheight},scrollbars=yes`);\n } else {\n // Already authorized.\n return sendToMoodleNet(issuerId, courseId, cmId, shareFormat);\n }\n }).catch(displayException);\n};\n\n/**\n * Register events.\n */\nconst registerEventListeners = () => {\n document.addEventListener('click', e => {\n const shareAction = e.target.closest('[data-action=\"sendtomoodlenet\"]');\n const sendAction = e.target.closest('.moodlenet-action-buttons [data-action=\"share\"]');\n if (shareAction) {\n e.preventDefault();\n const type = shareAction.getAttribute('data-type');\n const shareType = shareAction.getAttribute('data-sharetype');\n const cmId = Config.contextInstanceId;\n if (type == TYPE_ACTIVITY) {\n MoodleNetService.getActivityInformation(cmId).then(async(data) => {\n if (data.status) {\n siteSupportUrl = data.supportpageurl;\n issuerId = data.issuerid;\n const modalPromise = ModalFactory.create({\n type: SendActivityModal.TYPE,\n large: true,\n templateContext: {\n 'activitytype': data.type,\n 'activityname': data.name,\n 'sharetype': await getString('moodlenet:sharetype' + shareType, 'moodle'),\n 'server': data.server,\n }\n });\n return modalPromise.then(modal => {\n currentModal = modal;\n modal.show();\n return modal;\n }).catch(displayException);\n } else {\n return addNotification({\n message: data.warnings[0].message,\n type: 'error'\n });\n }\n }).catch(displayException);\n }\n }\n\n if (sendAction) {\n e.preventDefault();\n courseId = Config.courseId;\n cmId = Config.contextInstanceId;\n shareFormat = 0;\n handleAuthorization(issuerId);\n }\n });\n};\n\n/**\n * Initialize.\n */\nexport const init = () => {\n Prefetch.prefetchTemplates([\n 'core/moodlenet/send_activity_modal_base',\n 'core/moodlenet/send_activity_modal_packaging',\n 'core/moodlenet/send_activity_modal_done',\n 'core/moodlenet/send_activity_modal_footer_view',\n ]);\n registerEventListeners();\n};\n"],"names":["currentModal","siteSupportUrl","issuerId","courseId","cmId","shareFormat","responseFromMoodleNet","status","resourceUrl","$modal","getModal","modal","querySelector","classList","add","setBody","Templates","render","success","sitesupporturl","setFooter","resourseurl","showFooter","handleAuthorization","MoodleNetService","authorizationCheck","then","async","data","remove","hideFooter","sendActivity","resourceurl","catch","displayException","sendToMoodleNet","window","moodleNetAuthorize","error","open","loginurl","prefetchTemplates","document","addEventListener","e","shareAction","target","closest","sendAction","preventDefault","type","getAttribute","shareType","Config","contextInstanceId","getActivityInformation","supportpageurl","issuerid","ModalFactory","create","SendActivityModal","TYPE","large","templateContext","name","server","show","message","warnings"],"mappings":";;;;;;;;0YAmCIA,aACAC,eACAC,SACAC,SACAC,KACAC,kBAgCEC,sBAAwB,SAACC,YAAQC,mEAAc,SAC3CC,OAAST,aAAaU,WACtBC,MAAQF,OAAO,GACrBE,MAAMC,cAAc,iBAAiBC,UAAUC,IAAI,aACnDd,aAAae,QAAQC,UAAUC,OAAO,0CAA2C,CAC7EC,QAASX,OACTY,eAAgBlB,kBAGhBM,SACAP,aAAaoB,UAAUJ,UAAUC,OAAO,iDAAkD,CACtFI,YAAab,eAEjBR,aAAasB,eAUfC,oBAAuBrB,UAKlBsB,iBAAiBC,mBAAmBvB,SAAUC,UAAUuB,MAAKC,MAAAA,MAC3DC,KAAKrB,OAnDM,EAACL,SAAUC,SAAUC,KAAMC,qBAEzCM,MADSX,aAAaU,WACP,GACrBC,MAAMC,cAAc,iBAAiBC,UAAUgB,OAAO,aACtDlB,MAAMC,cAAc,iBAAiBC,UAAUC,IAAI,kBAEnDd,aAAae,QAAQC,UAAUC,OAAO,+CAAgD,KACtFjB,aAAa8B,aAEbN,iBAAiBO,aAAa7B,SAAUC,SAAUC,KAAMC,aAAaqB,MAAKC,MAAAA,aAChEpB,OAASqB,KAAKrB,OACdC,YAAcoB,KAAKI,mBAClB1B,sBAAsBC,OAAQC,gBACtCyB,MAAMC,0BAsDMC,CAAgBjC,SAAUC,SAAUC,KAAMC,cAbjD+B,OAAOC,mBAAsBC,QAEZ,IAATA,MACAf,oBAAoBrB,UACH,kBAAVoC,OACPhC,uBAAsB,EAAO,KAI9B8B,OAAOG,KAAKX,KAAKY,SAAU,qDAjBlB,uBACC,2BAsBtBP,MAAMC,uCA0DO,uBACPO,kBAAkB,CACvB,0CACA,+CACA,0CACA,mDAxDJC,SAASC,iBAAiB,SAASC,UACzBC,YAAcD,EAAEE,OAAOC,QAAQ,mCAC/BC,WAAaJ,EAAEE,OAAOC,QAAQ,sDAChCF,YAAa,CACbD,EAAEK,uBACIC,KAAOL,YAAYM,aAAa,aAChCC,UAAYP,YAAYM,aAAa,kBACrC/C,KAAOiD,gBAAOC,kBApGV,YAqGNJ,MACA1B,iBAAiB+B,uBAAuBnD,MAAMsB,MAAKC,MAAAA,MAC3CC,KAAKrB,QACLN,eAAiB2B,KAAK4B,eACtBtD,SAAW0B,KAAK6B,SACKC,uBAAaC,OAAO,CACrCT,KAAMU,6BAAkBC,KACxBC,OAAO,EACPC,gBAAiB,cACGnC,KAAKsB,kBACLtB,KAAKoC,qBACF,mBAAU,sBAAwBZ,UAAW,iBACtDxB,KAAKqC,UAGHvC,MAAKf,QACrBX,aAAeW,MACfA,MAAMuD,OACCvD,SACRsB,MAAMC,2BAEF,iCAAgB,CACnBiC,QAASvC,KAAKwC,SAAS,GAAGD,QAC1BjB,KAAM,YAGfjB,MAAMC,yBAIbc,aACAJ,EAAEK,iBACF9C,SAAWkD,gBAAOlD,SAClBC,KAAOiD,gBAAOC,kBACdjD,YAAc,EACdkB,oBAAoBrB"} \ No newline at end of file diff --git a/lib/amd/build/moodlenet/service.min.js b/lib/amd/build/moodlenet/service.min.js new file mode 100644 index 00000000000..bcb8a9f091b --- /dev/null +++ b/lib/amd/build/moodlenet/service.min.js @@ -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 + * @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,courseId,cmId,shareFormat)=>{const request={methodname:"core_moodlenet_send_activity",args:{issuerid:issuerId,courseid:courseId,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 \ No newline at end of file diff --git a/lib/amd/build/moodlenet/service.min.js.map b/lib/amd/build/moodlenet/service.min.js.map new file mode 100644 index 00000000000..37058ab938b --- /dev/null +++ b/lib/amd/build/moodlenet/service.min.js.map @@ -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 .\n\n/**\n * A javascript module to handle MoodleNet ajax actions.\n *\n * @module core/moodlenet/service\n * @copyright 2023 Huong Nguyen \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} courseId The course ID.\n * @param {Integer} cmId The course module ID.\n * @param {Integer} shareFormat The share format.\n * @return {promise}\n */\nexport const sendActivity = (issuerId, courseId, cmId, shareFormat) => {\n const request = {\n methodname: 'core_moodlenet_send_activity',\n args: {\n issuerid: issuerId,\n courseid: courseId,\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","courseId","shareFormat","issuerid","courseid","shareformat"],"mappings":";;;;;;;;0OAgCuCA,aAC7BC,QAAU,CACZC,WAAY,yCACZC,KAAM,CACFC,KAAMJ,cAIPK,cAAKC,KAAK,CAACL,UAAU,0BAYJ,CAACM,SAAUC,SAAUR,KAAMS,qBAC7CR,QAAU,CACZC,WAAY,+BACZC,KAAM,CACFO,SAAUH,SACVI,SAAUH,SACVJ,KAAMJ,KACNY,YAAaH,qBAIdJ,cAAKC,KAAK,CAACL,UAAU,gCAUE,CAACM,SAAUC,kBACnCP,QAAU,CACZC,WAAY,4BACZC,KAAM,CACFO,SAAUH,SACVI,SAAUH,kBAIXH,cAAKC,KAAK,CAACL,UAAU"} \ No newline at end of file diff --git a/lib/amd/src/moodlenet/oauth2callback.js b/lib/amd/src/moodlenet/oauth2callback.js new file mode 100644 index 00000000000..76eb09fcf05 --- /dev/null +++ b/lib/amd/src/moodlenet/oauth2callback.js @@ -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 . + +/** + * A module to handle the OAuth2 callback for MoodleNet. + * + * @module core/moodlenet/oauth2callback + * @copyright 2023 Huong Nguyen + * @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); +}; diff --git a/lib/amd/src/moodlenet/send_activity_modal.js b/lib/amd/src/moodlenet/send_activity_modal.js new file mode 100644 index 00000000000..4846ad6869b --- /dev/null +++ b/lib/amd/src/moodlenet/send_activity_modal.js @@ -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 . + +/** + * Send activity modal for MoodleNet. + * + * @module core/moodlenet/send_activity_modal + * @copyright 2023 Huong Nguyen + * @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; diff --git a/lib/amd/src/moodlenet/send_resource.js b/lib/amd/src/moodlenet/send_resource.js new file mode 100644 index 00000000000..7a8b8f26c84 --- /dev/null +++ b/lib/amd/src/moodlenet/send_resource.js @@ -0,0 +1,186 @@ +// 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 . + +/** + * A module to handle Share operations of the MoodleNet. + * + * @module core/moodlenet/send_resource + * @copyright 2023 Huong Nguyen + * @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 {exception as displayException, addNotification} 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} courseId The course ID. + * @param {int} cmId The course module ID. + * @param {int} shareFormat The share format. + */ +const sendToMoodleNet = (issuerId, courseId, 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, courseId, 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) => { + // This will be called by the callback after the authorization is successful. + if (error == '') { + handleAuthorization(issuerId); + } else if (error !== 'access_denied') { + responseFromMoodleNet(false, ''); + } + }; + // 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, courseId, 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(); +}; diff --git a/lib/amd/src/moodlenet/service.js b/lib/amd/src/moodlenet/service.js new file mode 100644 index 00000000000..6da59682de6 --- /dev/null +++ b/lib/amd/src/moodlenet/service.js @@ -0,0 +1,84 @@ +// 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 . + +/** + * A javascript module to handle MoodleNet ajax actions. + * + * @module core/moodlenet/service + * @copyright 2023 Huong Nguyen + * @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} courseId The course ID. + * @param {Integer} cmId The course module ID. + * @param {Integer} shareFormat The share format. + * @return {promise} + */ +export const sendActivity = (issuerId, courseId, cmId, shareFormat) => { + const request = { + methodname: 'core_moodlenet_send_activity', + args: { + issuerid: issuerId, + courseid: courseId, + 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]; +}; diff --git a/lib/classes/external/moodlenet_auth_check.php b/lib/classes/external/moodlenet_auth_check.php new file mode 100644 index 00000000000..079287caa2c --- /dev/null +++ b/lib/classes/external/moodlenet_auth_check.php @@ -0,0 +1,147 @@ +. + +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 + * @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, 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, + ]; + } +} diff --git a/lib/classes/external/moodlenet_get_share_info_activity.php b/lib/classes/external/moodlenet_get_share_info_activity.php new file mode 100644 index 00000000000..309c54bc9fe --- /dev/null +++ b/lib/classes/external/moodlenet_get_share_info_activity.php @@ -0,0 +1,160 @@ +. + +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 + * @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 + ]; + } +} diff --git a/lib/classes/navigation/views/secondary.php b/lib/classes/navigation/views/secondary.php index b502f7300a6..9abc2b270f8 100644 --- a/lib/classes/navigation/views/secondary.php +++ b/lib/classes/navigation/views/secondary.php @@ -122,6 +122,7 @@ class secondary extends view { 'backup' => 9, 'restore' => 10, 'competencybreakdown' => 11, + 'sendtomoodlenet' => 16, ], self::TYPE_CUSTOM => [ 'advgrading' => 2, diff --git a/lib/classes/oauth2/service/moodlenet.php b/lib/classes/oauth2/service/moodlenet.php index 03900d6ee6e..24921a36b3e 100644 --- a/lib/classes/oauth2/service/moodlenet.php +++ b/lib/classes/oauth2/service/moodlenet.php @@ -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, diff --git a/lib/classes/oauth2/user_field_mapping.php b/lib/classes/oauth2/user_field_mapping.php index cecbc9e84df..6c268ec429d 100644 --- a/lib/classes/oauth2/user_field_mapping.php +++ b/lib/classes/oauth2/user_field_mapping.php @@ -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()); } diff --git a/lib/db/services.php b/lib/db/services.php index d3f46a37e4e..7cee9014921 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -3048,6 +3048,18 @@ $functions = array( '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( diff --git a/lib/navigationlib.php b/lib/navigationlib.php index 7cb22e6c5bf..833a023837f 100644 --- a/lib/navigationlib.php +++ b/lib/navigationlib.php @@ -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(); diff --git a/lib/templates/moodlenet/send_activity_modal_base.mustache b/lib/templates/moodlenet/send_activity_modal_base.mustache new file mode 100644 index 00000000000..8cba958ecad --- /dev/null +++ b/lib/templates/moodlenet/send_activity_modal_base.mustache @@ -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 . +}} +{{! + @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" + } +}} + diff --git a/lib/templates/moodlenet/send_activity_modal_done.mustache b/lib/templates/moodlenet/send_activity_modal_done.mustache new file mode 100644 index 00000000000..a16a279313f --- /dev/null +++ b/lib/templates/moodlenet/send_activity_modal_done.mustache @@ -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 . +}} +{{! + @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" + } +}} +
+
+ + {{#success}} + {{#pix}} t/check, core {{/pix}} + {{/success}} + {{^success}} + {{#pix}} fp/cross, theme {{/pix}} + {{/success}} + + + {{#success}} + {{#str}} moodlenet:sharesuccesstitle, moodle {{/str}} + {{/success}} + {{^success}} + {{#str}} moodlenet:sharefailtitle, moodle {{/str}} + {{/success}} + +
+
+ + {{#success}} + {{#str}} moodlenet:sharesuccesstext, moodle {{/str}} + {{/success}} + {{^success}} + {{#sitesupporturl}} + {{#str}} moodlenet:sharefailtextwithsitesupport, moodle, {{sitesupporturl}} {{/str}} + {{/sitesupporturl}} + {{^sitesupporturl}} + {{#str}} moodlenet:sharefailtext, moodle {{/str}} + {{/sitesupporturl}} + {{/success}} + +
+
diff --git a/lib/templates/moodlenet/send_activity_modal_footer_share.mustache b/lib/templates/moodlenet/send_activity_modal_footer_share.mustache new file mode 100644 index 00000000000..985a91908dc --- /dev/null +++ b/lib/templates/moodlenet/send_activity_modal_footer_share.mustache @@ -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 . +}} +{{! + @template core/moodlenet/send_activity_modal_footer_share + Render the footer for sharing activity + Example context (json): + { + "server": "MoodleNet Local" + } +}} +
+ {{#str}} moodlenet:sharingto, moodle {{/str}}{{server}} +
+
+ + +
diff --git a/lib/templates/moodlenet/send_activity_modal_footer_view.mustache b/lib/templates/moodlenet/send_activity_modal_footer_view.mustache new file mode 100644 index 00000000000..2c82e124f38 --- /dev/null +++ b/lib/templates/moodlenet/send_activity_modal_footer_view.mustache @@ -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 . +}} +{{! + @template core/moodlenet/send_activity_modal_footer_view + Render the footer for sharing activity + Example context (json): + { + "resourseurl": "https://moodlenet.test/draft/activity.mbz" + } +}} + diff --git a/lib/templates/moodlenet/send_activity_modal_packaging.mustache b/lib/templates/moodlenet/send_activity_modal_packaging.mustache new file mode 100644 index 00000000000..ece121f6d9d --- /dev/null +++ b/lib/templates/moodlenet/send_activity_modal_packaging.mustache @@ -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 . +}} +{{! + @template core/moodlenet/send_activity_modal_packaging + This template renders the packaging stage for MoodleNet. + Example context (json): + { + } +}} +
+
{{#str}} moodlenet:sharingstatus, moodle {{/str}}
+
{{#str}} moodlenet:sharinglargefile, moodle {{/str}}
+
+ {{> core/loading }} +
+
{{#str}} moodlenet:packagingandsending, moodle {{/str}}
+
diff --git a/lib/tests/behat/moodlenet_outbound.feature b/lib/tests/behat/moodlenet_outbound.feature new file mode 100644 index 00000000000..97242c73f2a --- /dev/null +++ b/lib/tests/behat/moodlenet_outbound.feature @@ -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" diff --git a/lib/tests/external/moodlenet_auth_check_test.php b/lib/tests/external/moodlenet_auth_check_test.php new file mode 100644 index 00000000000..049359892c8 --- /dev/null +++ b/lib/tests/external/moodlenet_auth_check_test.php @@ -0,0 +1,87 @@ +. + +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 + * @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']); + } +} diff --git a/lib/tests/external/moodlenet_get_share_info_activity_test.php b/lib/tests/external/moodlenet_get_share_info_activity_test.php new file mode 100644 index 00000000000..ec0abbded39 --- /dev/null +++ b/lib/tests/external/moodlenet_get_share_info_activity_test.php @@ -0,0 +1,140 @@ +. + +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 + * @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']); + } +} diff --git a/pix/moodlenet.png b/pix/moodlenet.png new file mode 100644 index 0000000000000000000000000000000000000000..286adbc0e888aff0e97e6ba58f34a53dba18926b GIT binary patch literal 79949 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%0D5^cR@m&w^sO5F{MnwBL<}4QxTA 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

nsVr?~5104ig!{d#{) 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 literal 0 HcmV?d00001 diff --git a/pix/moodlenet.svg b/pix/moodlenet.svg new file mode 100644 index 00000000000..10473a93d2a --- /dev/null +++ b/pix/moodlenet.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/theme/boost/scss/moodle.scss b/theme/boost/scss/moodle.scss index d41118cca32..7451eeb10ac 100644 --- a/theme/boost/scss/moodle.scss +++ b/theme/boost/scss/moodle.scss @@ -49,3 +49,4 @@ $breadcrumb-divider-rtl: "◀" !default; @import "moodle/secondarynavigation"; @import "moodle/tertiarynavigation"; @import "moodle/process-monitor"; +@import "moodle/moodlenet"; diff --git a/theme/boost/scss/moodle/moodlenet.scss b/theme/boost/scss/moodle/moodlenet.scss new file mode 100644 index 00000000000..6707682da7b --- /dev/null +++ b/theme/boost/scss/moodle/moodlenet.scss @@ -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; + } + } +} diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index dbec3dd71a2..8d0def3d5b8 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -37733,6 +37733,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; diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 32e087c3d04..ccb8053ce5a 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -37667,6 +37667,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; diff --git a/version.php b/version.php index 9009dbf0d48..57dc7722e0e 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2023041400.02; // 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