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 00000000000..286adbc0e88 Binary files /dev/null and b/pix/moodlenet.png differ 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