diff --git a/admin/tool/moodlenet/amd/build/instance_form.min.js b/admin/tool/moodlenet/amd/build/instance_form.min.js new file mode 100644 index 00000000000..a6f0561f12d --- /dev/null +++ b/admin/tool/moodlenet/amd/build/instance_form.min.js @@ -0,0 +1,2 @@ +define ("tool_moodlenet/instance_form",["tool_moodlenet/validator","tool_moodlenet/selectors","core/loadingicon","core/templates","core/notification","jquery"],function(a,b,c,d,e,f){var g=function(d){d.addEventListener("click",function(f){if(f.target.matches(b.action.submit)){var e=d.querySelector("[data-var=\"mnet-link\"]"),g=d.querySelector(b.region.spinner),h=document.querySelector(b.region.validationArea);g.classList.remove("d-none");var i=c.addIconToContainerWithPromise(g);a.validation(e).then(function(a){i.resolve();g.classList.add("d-none");if(a.result){e.classList.remove("is-invalid");e.classList.add("is-valid");h.innerText=a.message;h.classList.remove("text-error");h.classList.add("text-success");setTimeout(function(){window.location=a.domain},1e3)}else{e.classList.add("is-invalid");h.innerText=a.message;h.classList.add("text-error")}}).catch()}})},h=function(a,b,h,i){a.innerHTML="";var j=c.addIconToContainer(a),k=null,l=new Promise(function(a){k=a});f.when(j,l).then(function(){d.replaceNodeContents(a,b.customcarouseltemplate,"")}).catch(e.exception);g(a);h.one("slid.bs.carousel",function(){k()});h.carousel(2);i.setFooter(d.render("tool_moodlenet/chooser_footer_close_mnet",{}))},i=function(a,b,c){a.carousel(0);b.setFooter(c.customfootertemplate)};return{footerClickListener:function footerClickListener(a,c,d){if(a.target.matches(b.action.showMoodleNet)||a.target.closest(b.action.showMoodleNet)){a.preventDefault();var e=f(d.getBody()[0].querySelector(b.region.carousel)),g=e.find(b.region.moodleNet)[0];h(g,c,e,d)}if(a.target.matches(b.action.closeOption)){var j=f(d.getBody()[0].querySelector(b.region.carousel));i(j,d,c)}}}}); +//# sourceMappingURL=instance_form.min.js.map diff --git a/admin/tool/moodlenet/amd/build/instance_form.min.js.map b/admin/tool/moodlenet/amd/build/instance_form.min.js.map new file mode 100644 index 00000000000..3e2853cc56c --- /dev/null +++ b/admin/tool/moodlenet/amd/build/instance_form.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/instance_form.js"],"names":["define","Validator","Selectors","LoadingIcon","Templates","Notification","$","registerListenerEvents","page","addEventListener","e","target","matches","action","submit","input","querySelector","overlay","region","spinner","validationArea","document","classList","remove","addIconToContainerWithPromise","validation","then","result","resolve","add","innerText","message","setTimeout","window","location","domain","catch","chooserNavigateToMnet","showMoodleNet","footerData","carousel","modal","innerHTML","spinnerPromise","addIconToContainer","transitionPromiseResolver","transitionPromise","Promise","when","replaceNodeContents","customcarouseltemplate","exception","one","setFooter","render","chooserNavigateFromMnet","customfootertemplate","footerClickListener","closest","preventDefault","getBody","find","moodleNet","closeOption"],"mappings":"AA+BAA,OAAM,gCAAC,CAAC,0BAAD,CACC,0BADD,CAEC,kBAFD,CAGC,gBAHD,CAIC,mBAJD,CAKC,QALD,CAAD,CAMF,SAASC,CAAT,CACSC,CADT,CAESC,CAFT,CAGSC,CAHT,CAISC,CAJT,CAKSC,CALT,CAKY,IAQRC,CAAAA,CAAsB,CAAG,SAAgCC,CAAhC,CAAsC,CAC/DA,CAAI,CAACC,gBAAL,CAAsB,OAAtB,CAA+B,SAASC,CAAT,CAAY,CAGvC,GAAIA,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiBV,CAAS,CAACW,MAAV,CAAiBC,MAAlC,CAAJ,CAA+C,IACvCC,CAAAA,CAAK,CAAGP,CAAI,CAACQ,aAAL,CAAmB,0BAAnB,CAD+B,CAEvCC,CAAO,CAAGT,CAAI,CAACQ,aAAL,CAAmBd,CAAS,CAACgB,MAAV,CAAiBC,OAApC,CAF6B,CAGvCC,CAAc,CAAGC,QAAQ,CAACL,aAAT,CAAuBd,CAAS,CAACgB,MAAV,CAAiBE,cAAxC,CAHsB,CAK3CH,CAAO,CAACK,SAAR,CAAkBC,MAAlB,CAAyB,QAAzB,EACA,GAAIJ,CAAAA,CAAO,CAAGhB,CAAW,CAACqB,6BAAZ,CAA0CP,CAA1C,CAAd,CACAhB,CAAS,CAACwB,UAAV,CAAqBV,CAArB,EACKW,IADL,CACU,SAASC,CAAT,CAAiB,CACnBR,CAAO,CAACS,OAAR,GACAX,CAAO,CAACK,SAAR,CAAkBO,GAAlB,CAAsB,QAAtB,EACA,GAAIF,CAAM,CAACA,MAAX,CAAmB,CACfZ,CAAK,CAACO,SAAN,CAAgBC,MAAhB,CAAuB,YAAvB,EACAR,CAAK,CAACO,SAAN,CAAgBO,GAAhB,CAAoB,UAApB,EACAT,CAAc,CAACU,SAAf,CAA2BH,CAAM,CAACI,OAAlC,CACAX,CAAc,CAACE,SAAf,CAAyBC,MAAzB,CAAgC,YAAhC,EACAH,CAAc,CAACE,SAAf,CAAyBO,GAAzB,CAA6B,cAA7B,EAEAG,UAAU,CAAC,UAAW,CAClBC,MAAM,CAACC,QAAP,CAAkBP,CAAM,CAACQ,MAC5B,CAFS,CAEP,GAFO,CAGb,CAVD,IAUO,CACHpB,CAAK,CAACO,SAAN,CAAgBO,GAAhB,CAAoB,YAApB,EACAT,CAAc,CAACU,SAAf,CAA2BH,CAAM,CAACI,OAAlC,CACAX,CAAc,CAACE,SAAf,CAAyBO,GAAzB,CAA6B,YAA7B,CACH,CAER,CApBD,EAoBGO,KApBH,EAqBH,CACJ,CAhCD,CAiCH,CA1CW,CAqDRC,CAAqB,CAAG,SAASC,CAAT,CAAwBC,CAAxB,CAAoCC,CAApC,CAA8CC,CAA9C,CAAqD,CAC7EH,CAAa,CAACI,SAAd,CAA0B,EAA1B,CAD6E,GAIzEC,CAAAA,CAAc,CAAGxC,CAAW,CAACyC,kBAAZ,CAA+BN,CAA/B,CAJwD,CAOzEO,CAAyB,CAAG,IAP6C,CAQzEC,CAAiB,CAAG,GAAIC,CAAAA,OAAJ,CAAY,SAAAnB,CAAO,CAAI,CAC3CiB,CAAyB,CAAGjB,CAC/B,CAFuB,CARqD,CAY7EtB,CAAC,CAAC0C,IAAF,CACIL,CADJ,CAEIG,CAFJ,EAGEpB,IAHF,CAGO,UAAW,CACVtB,CAAS,CAAC6C,mBAAV,CAA8BX,CAA9B,CAA6CC,CAAU,CAACW,sBAAxD,CAAgF,EAAhF,CAEP,CAND,EAMGd,KANH,CAMS/B,CAAY,CAAC8C,SANtB,EASA5C,CAAsB,CAAC+B,CAAD,CAAtB,CAGAE,CAAQ,CAACY,GAAT,CAAa,kBAAb,CAAiC,UAAW,CACxCP,CAAyB,EAC5B,CAFD,EAIAL,CAAQ,CAACA,QAAT,CAAkB,CAAlB,EAEAC,CAAK,CAACY,SAAN,CAAgBjD,CAAS,CAACkD,MAAV,CAAiB,0CAAjB,CAA6D,EAA7D,CAAhB,CACH,CApFW,CA8FRC,CAAuB,CAAG,SAASf,CAAT,CAAmBC,CAAnB,CAA0BF,CAA1B,CAAsC,CAEhEC,CAAQ,CAACA,QAAT,CAAkB,CAAlB,EACAC,CAAK,CAACY,SAAN,CAAgBd,CAAU,CAACiB,oBAA3B,CACH,CAlGW,CA2HZ,MAAO,CACHC,mBAAmB,CAjBG,QAAtBA,CAAAA,mBAAsB,CAAS/C,CAAT,CAAY6B,CAAZ,CAAwBE,CAAxB,CAA+B,CACrD,GAAI/B,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiBV,CAAS,CAACW,MAAV,CAAiByB,aAAlC,GAAoD5B,CAAC,CAACC,MAAF,CAAS+C,OAAT,CAAiBxD,CAAS,CAACW,MAAV,CAAiByB,aAAlC,CAAxD,CAA0G,CACtG5B,CAAC,CAACiD,cAAF,GADsG,GAEhGnB,CAAAA,CAAQ,CAAGlC,CAAC,CAACmC,CAAK,CAACmB,OAAN,GAAgB,CAAhB,EAAmB5C,aAAnB,CAAiCd,CAAS,CAACgB,MAAV,CAAiBsB,QAAlD,CAAD,CAFoF,CAGhGF,CAAa,CAAGE,CAAQ,CAACqB,IAAT,CAAc3D,CAAS,CAACgB,MAAV,CAAiB4C,SAA/B,EAA0C,CAA1C,CAHgF,CAKtGzB,CAAqB,CAACC,CAAD,CAAgBC,CAAhB,CAA4BC,CAA5B,CAAsCC,CAAtC,CACxB,CAED,GAAI/B,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiBV,CAAS,CAACW,MAAV,CAAiBkD,WAAlC,CAAJ,CAAoD,CAChD,GAAMvB,CAAAA,CAAQ,CAAGlC,CAAC,CAACmC,CAAK,CAACmB,OAAN,GAAgB,CAAhB,EAAmB5C,aAAnB,CAAiCd,CAAS,CAACgB,MAAV,CAAiBsB,QAAlD,CAAD,CAAlB,CAEAe,CAAuB,CAACf,CAAD,CAAWC,CAAX,CAAkBF,CAAlB,CAC1B,CACJ,CAEM,CAGV,CAzIK,CAAN","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 * Our basic form manager for when a user either enters\n * their profile url or just wants to browse.\n *\n * This file is a mishmash of JS functions we need for both the standalone (M3.7, M3.8)\n * plugin & Moodle 3.9 functions. The 3.9 Functions have a base understanding that certain\n * things exist i.e. directory structures for templates. When this feature goes 3.9+ only\n * The goal is that we can quickly gut all AMD modules into bare JS files and use ES6 guidelines.\n * Till then this will have to do.\n *\n * @module tool_moodlenet/instance_form\n * @package tool_moodlenet\n * @copyright 2020 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['tool_moodlenet/validator',\n 'tool_moodlenet/selectors',\n 'core/loadingicon',\n 'core/templates',\n 'core/notification',\n 'jquery'],\n function(Validator,\n Selectors,\n LoadingIcon,\n Templates,\n Notification,\n $) {\n\n /**\n * Add the event listeners to our form.\n *\n * @method registerListenerEvents\n * @param {HTMLElement} page The whole page element for our form area\n */\n var registerListenerEvents = function registerListenerEvents(page) {\n page.addEventListener('click', function(e) {\n\n // Our fake submit button / browse button.\n if (e.target.matches(Selectors.action.submit)) {\n var input = page.querySelector('[data-var=\"mnet-link\"]');\n var overlay = page.querySelector(Selectors.region.spinner);\n var validationArea = document.querySelector(Selectors.region.validationArea);\n\n overlay.classList.remove('d-none');\n var spinner = LoadingIcon.addIconToContainerWithPromise(overlay);\n Validator.validation(input)\n .then(function(result) {\n spinner.resolve();\n overlay.classList.add('d-none');\n if (result.result) {\n input.classList.remove('is-invalid'); // Just in case the class has been applied already.\n input.classList.add('is-valid');\n validationArea.innerText = result.message;\n validationArea.classList.remove('text-error');\n validationArea.classList.add('text-success');\n // Give the user some time to see their input is valid.\n setTimeout(function() {\n window.location = result.domain;\n }, 1000);\n } else {\n input.classList.add('is-invalid');\n validationArea.innerText = result.message;\n validationArea.classList.add('text-error');\n }\n return;\n }).catch();\n }\n });\n };\n\n /**\n * Given a user wishes to see the MoodleNet profile url form transition them there.\n *\n * @method chooserNavigateToMnet\n * @param {HTMLElement} showMoodleNet The chooser's area for ment\n * @param {Object} footerData Our footer object to render out\n * @param {jQuery} carousel Our carousel instance to manage\n * @param {jQuery} modal Our modal instance to manage\n */\n var chooserNavigateToMnet = function(showMoodleNet, footerData, carousel, modal) {\n showMoodleNet.innerHTML = '';\n\n // Add a spinner.\n var spinnerPromise = LoadingIcon.addIconToContainer(showMoodleNet);\n\n // Used later...\n var transitionPromiseResolver = null;\n var transitionPromise = new Promise(resolve => {\n transitionPromiseResolver = resolve;\n });\n\n $.when(\n spinnerPromise,\n transitionPromise\n ).then(function() {\n Templates.replaceNodeContents(showMoodleNet, footerData.customcarouseltemplate, '');\n return;\n }).catch(Notification.exception);\n\n // We apply our handlers in here to minimise plugin dependency in the Chooser.\n registerListenerEvents(showMoodleNet);\n\n // Move to the next slide, and resolve the transition promise when it's done.\n carousel.one('slid.bs.carousel', function() {\n transitionPromiseResolver();\n });\n // Trigger the transition between 'pages'.\n carousel.carousel(2);\n // eslint-disable-next-line max-len\n modal.setFooter(Templates.render('tool_moodlenet/chooser_footer_close_mnet', {}));\n };\n\n /**\n * Given a user no longer wishes to see the MoodleNet profile url form transition them from there.\n *\n * @method chooserNavigateFromMnet\n * @param {jQuery} carousel Our carousel instance to manage\n * @param {jQuery} modal Our modal instance to manage\n * @param {Object} footerData Our footer object to render out\n */\n var chooserNavigateFromMnet = function(carousel, modal, footerData) {\n // Trigger the transition between 'pages'.\n carousel.carousel(0);\n modal.setFooter(footerData.customfootertemplate);\n };\n\n /**\n * Create the custom listener that would handle anything in the footer.\n *\n * @param {Event} e The event being triggered.\n * @param {Object} footerData The data generated from the exporter.\n * @param {Object} modal The chooser modal.\n */\n var footerClickListener = function(e, footerData, modal) {\n if (e.target.matches(Selectors.action.showMoodleNet) || e.target.closest(Selectors.action.showMoodleNet)) {\n e.preventDefault();\n const carousel = $(modal.getBody()[0].querySelector(Selectors.region.carousel));\n const showMoodleNet = carousel.find(Selectors.region.moodleNet)[0];\n\n chooserNavigateToMnet(showMoodleNet, footerData, carousel, modal);\n }\n // From the help screen go back to the module overview.\n if (e.target.matches(Selectors.action.closeOption)) {\n const carousel = $(modal.getBody()[0].querySelector(Selectors.region.carousel));\n\n chooserNavigateFromMnet(carousel, modal, footerData);\n }\n };\n\n return {\n footerClickListener: footerClickListener\n };\n});\n"],"file":"instance_form.min.js"} \ No newline at end of file diff --git a/admin/tool/moodlenet/amd/build/select_page.min.js b/admin/tool/moodlenet/amd/build/select_page.min.js new file mode 100644 index 00000000000..558896069b9 --- /dev/null +++ b/admin/tool/moodlenet/amd/build/select_page.min.js @@ -0,0 +1,2 @@ +define ("tool_moodlenet/select_page",["core/ajax","core/templates","tool_moodlenet/selectors","core/notification"],function(a,b,c,d){var e,f=function(a){return b.renderPix("courses","tool_moodlenet").then(function(a){return a}).then(function(a){var c=document.createElement("div");c.innerHTML=a.trim();return b.render("core_course/no-courses",{nocoursesimg:c.firstChild.src})}).then(function(c,d){b.replaceNodeContents(a,c,d);a.classList.add("mx-auto");a.classList.add("w-25")})},g=function(a,c){return b.render("tool_moodlenet/view-cards",{courses:c}).then(function(c,d){b.replaceNodeContents(a,c,d);a.classList.remove("mx-auto");a.classList.remove("w-25")})},h=function(b,h,i){var j=h.querySelector(c.region.searchIcon),k=h.querySelector(c.region.clearIcon);if(""!==b){j.classList.add("d-none");k.parentElement.classList.remove("d-none")}else{j.classList.remove("d-none");k.parentElement.classList.add("d-none")}a.call([{methodname:"tool_moodlenet_search_courses",args:{searchvalue:b}}])[0].then(function(a){if(0===a.courses.length){return f(i)}else{a.courses.forEach(function(a){a.viewurl+="&id="+e});return g(i,a.courses)}}).catch(d.exception)},i=function(a){var b=a.querySelector(c.region.searchInput),d=a.querySelector(c.region.courses),e=a.querySelector(c.region.clearIcon);e.addEventListener("click",function(){b.value="";h("",a,d)});b.addEventListener("input",k(function(){h(b.value,a,d)},300))},j=function(a){var b=a.querySelector(c.region.courses);h("",a,b)},k=function(a,b,c){var d;return function(){var e=this,f=arguments,g=c&&!d;clearTimeout(d);d=setTimeout(function later(){d=null;if(!c){a.apply(e,f)}},b);if(g){a.apply(e,f)}}};return{init:function init(a){e=a;var b=document.querySelector(c.region.selectPage);i(b);j(b)}}}); +//# sourceMappingURL=select_page.min.js.map diff --git a/admin/tool/moodlenet/amd/build/select_page.min.js.map b/admin/tool/moodlenet/amd/build/select_page.min.js.map new file mode 100644 index 00000000000..49e47572829 --- /dev/null +++ b/admin/tool/moodlenet/amd/build/select_page.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/select_page.js"],"names":["define","Ajax","Templates","Selectors","Notification","importId","renderNoCourses","areaReplace","renderPix","then","img","temp","document","createElement","innerHTML","trim","render","nocoursesimg","firstChild","src","html","js","replaceNodeContents","classList","add","renderCourses","courses","remove","searchCourses","inputValue","page","searchIcon","querySelector","region","clearIcon","parentElement","call","methodname","args","searchvalue","result","length","forEach","course","viewurl","catch","exception","registerListenerEvents","input","searchInput","courseArea","addEventListener","value","debounce","addCourses","func","wait","immediate","timeout","context","arguments","callNow","clearTimeout","setTimeout","later","apply","init","importIdString","selectPage"],"mappings":"AAwBAA,OAAM,8BAAC,CACH,WADG,CAEH,gBAFG,CAGH,0BAHG,CAIH,mBAJG,CAAD,CAKH,SACCC,CADD,CAECC,CAFD,CAGCC,CAHD,CAICC,CAJD,CAKD,IAIMC,CAAAA,CAJN,CAyBMC,CAAe,CAAG,SAASC,CAAT,CAAsB,CACxC,MAAOL,CAAAA,CAAS,CAACM,SAAV,CAAoB,SAApB,CAA+B,gBAA/B,EAAiDC,IAAjD,CAAsD,SAASC,CAAT,CAAc,CACvE,MAAOA,CAAAA,CACV,CAFM,EAEJD,IAFI,CAEC,SAASC,CAAT,CAAc,CAClB,GAAIC,CAAAA,CAAI,CAAGC,QAAQ,CAACC,aAAT,CAAuB,KAAvB,CAAX,CACAF,CAAI,CAACG,SAAL,CAAiBJ,CAAG,CAACK,IAAJ,EAAjB,CACA,MAAOb,CAAAA,CAAS,CAACc,MAAV,CAAiB,wBAAjB,CAA2C,CAC9CC,YAAY,CAAEN,CAAI,CAACO,UAAL,CAAgBC,GADgB,CAA3C,CAGV,CARM,EAQJV,IARI,CAQC,SAASW,CAAT,CAAeC,CAAf,CAAmB,CACvBnB,CAAS,CAACoB,mBAAV,CAA8Bf,CAA9B,CAA2Ca,CAA3C,CAAiDC,CAAjD,EACAd,CAAW,CAACgB,SAAZ,CAAsBC,GAAtB,CAA0B,SAA1B,EACAjB,CAAW,CAACgB,SAAZ,CAAsBC,GAAtB,CAA0B,MAA1B,CAEH,CAbM,CAcV,CAxCH,CAiDMC,CAAa,CAAG,SAASlB,CAAT,CAAsBmB,CAAtB,CAA+B,CAC/C,MAAOxB,CAAAA,CAAS,CAACc,MAAV,CAAiB,2BAAjB,CAA8C,CACjDU,OAAO,CAAEA,CADwC,CAA9C,EAEJjB,IAFI,CAEC,SAASW,CAAT,CAAeC,CAAf,CAAmB,CACvBnB,CAAS,CAACoB,mBAAV,CAA8Bf,CAA9B,CAA2Ca,CAA3C,CAAiDC,CAAjD,EACAd,CAAW,CAACgB,SAAZ,CAAsBI,MAAtB,CAA6B,SAA7B,EACApB,CAAW,CAACgB,SAAZ,CAAsBI,MAAtB,CAA6B,MAA7B,CAEH,CAPM,CAQV,CA1DH,CAoEMC,CAAa,CAAG,SAASC,CAAT,CAAqBC,CAArB,CAA2BvB,CAA3B,CAAwC,IACpDwB,CAAAA,CAAU,CAAGD,CAAI,CAACE,aAAL,CAAmB7B,CAAS,CAAC8B,MAAV,CAAiBF,UAApC,CADuC,CAEpDG,CAAS,CAAGJ,CAAI,CAACE,aAAL,CAAmB7B,CAAS,CAAC8B,MAAV,CAAiBC,SAApC,CAFwC,CAIxD,GAAmB,EAAf,GAAAL,CAAJ,CAAuB,CACnBE,CAAU,CAACR,SAAX,CAAqBC,GAArB,CAAyB,QAAzB,EACAU,CAAS,CAACC,aAAV,CAAwBZ,SAAxB,CAAkCI,MAAlC,CAAyC,QAAzC,CACH,CAHD,IAGO,CACHI,CAAU,CAACR,SAAX,CAAqBI,MAArB,CAA4B,QAA5B,EACAO,CAAS,CAACC,aAAV,CAAwBZ,SAAxB,CAAkCC,GAAlC,CAAsC,QAAtC,CACH,CAIDvB,CAAI,CAACmC,IAAL,CAAU,CAAC,CACPC,UAAU,CAAE,+BADL,CAEPC,IAAI,CALG,CACPC,WAAW,CAAEV,CADN,CAGA,CAAD,CAAV,EAGI,CAHJ,EAGOpB,IAHP,CAGY,SAAS+B,CAAT,CAAiB,CACzB,GAA8B,CAA1B,GAAAA,CAAM,CAACd,OAAP,CAAee,MAAnB,CAAiC,CAC7B,MAAOnC,CAAAA,CAAe,CAACC,CAAD,CACzB,CAFD,IAEO,CAEHiC,CAAM,CAACd,OAAP,CAAegB,OAAf,CAAuB,SAASC,CAAT,CAAiB,CACpCA,CAAM,CAACC,OAAP,EAAkB,OAASvC,CAC9B,CAFD,EAGA,MAAOoB,CAAAA,CAAa,CAAClB,CAAD,CAAciC,CAAM,CAACd,OAArB,CACvB,CACJ,CAbD,EAaGmB,KAbH,CAaSzC,CAAY,CAAC0C,SAbtB,CAcH,CAhGH,CAwGMC,CAAsB,CAAG,SAASjB,CAAT,CAAe,IACpCkB,CAAAA,CAAK,CAAGlB,CAAI,CAACE,aAAL,CAAmB7B,CAAS,CAAC8B,MAAV,CAAiBgB,WAApC,CAD4B,CAEpCC,CAAU,CAAGpB,CAAI,CAACE,aAAL,CAAmB7B,CAAS,CAAC8B,MAAV,CAAiBP,OAApC,CAFuB,CAGpCQ,CAAS,CAAGJ,CAAI,CAACE,aAAL,CAAmB7B,CAAS,CAAC8B,MAAV,CAAiBC,SAApC,CAHwB,CAIxCA,CAAS,CAACiB,gBAAV,CAA2B,OAA3B,CAAoC,UAAW,CAC3CH,CAAK,CAACI,KAAN,CAAc,EAAd,CACAxB,CAAa,CAAC,EAAD,CAAKE,CAAL,CAAWoB,CAAX,CAChB,CAHD,EAKAF,CAAK,CAACG,gBAAN,CAAuB,OAAvB,CAAgCE,CAAQ,CAAC,UAAW,CAChDzB,CAAa,CAACoB,CAAK,CAACI,KAAP,CAActB,CAAd,CAAoBoB,CAApB,CAChB,CAFuC,CAErC,GAFqC,CAAxC,CAGH,CApHH,CA4HMI,CAAU,CAAG,SAASxB,CAAT,CAAe,CAC5B,GAAIoB,CAAAA,CAAU,CAAGpB,CAAI,CAACE,aAAL,CAAmB7B,CAAS,CAAC8B,MAAV,CAAiBP,OAApC,CAAjB,CACAE,CAAa,CAAC,EAAD,CAAKE,CAAL,CAAWoB,CAAX,CAChB,CA/HH,CA6IMG,CAAQ,CAAG,SAASE,CAAT,CAAeC,CAAf,CAAqBC,CAArB,CAAgC,CAC3C,GAAIC,CAAAA,CAAJ,CACA,MAAO,WAAW,IACVC,CAAAA,CAAO,CAAG,IADA,CAEVrB,CAAI,CAAGsB,SAFG,CASVC,CAAO,CAAGJ,CAAS,EAAI,CAACC,CATd,CAUdI,YAAY,CAACJ,CAAD,CAAZ,CACAA,CAAO,CAAGK,UAAU,CARR,QAARC,CAAAA,KAAQ,EAAW,CACnBN,CAAO,CAAG,IAAV,CACA,GAAI,CAACD,CAAL,CAAgB,CACZF,CAAI,CAACU,KAAL,CAAWN,CAAX,CAAoBrB,CAApB,CACH,CACJ,CAGmB,CAAQkB,CAAR,CAApB,CACA,GAAIK,CAAJ,CAAa,CACTN,CAAI,CAACU,KAAL,CAAWN,CAAX,CAAoBrB,CAApB,CACH,CACJ,CACJ,CA/JH,CAgKE,MAAO,CACH4B,IAAI,CArJG,QAAPA,CAAAA,IAAO,CAASC,CAAT,CAAyB,CAChC9D,CAAQ,CAAG8D,CAAX,CACA,GAAIrC,CAAAA,CAAI,CAAGlB,QAAQ,CAACoB,aAAT,CAAuB7B,CAAS,CAAC8B,MAAV,CAAiBmC,UAAxC,CAAX,CACArB,CAAsB,CAACjB,CAAD,CAAtB,CACAwB,CAAU,CAACxB,CAAD,CACb,CA+IM,CAGV,CA7KK,CAAN","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 * When returning to Moodle let the user select which course to add the resource to.\n *\n * @module tool_moodlenet/select_page\n * @package tool_moodlenet\n * @copyright 2020 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine([\n 'core/ajax',\n 'core/templates',\n 'tool_moodlenet/selectors',\n 'core/notification'\n], function(\n Ajax,\n Templates,\n Selectors,\n Notification\n) {\n /**\n * @var {string} The id corresponding to the import.\n */\n var importId;\n\n /**\n * Set up the page.\n *\n * @method init\n * @param {string} importIdString the string ID of the import.\n */\n var init = function(importIdString) {\n importId = importIdString;\n var page = document.querySelector(Selectors.region.selectPage);\n registerListenerEvents(page);\n addCourses(page);\n };\n\n /**\n * Renders the 'no-courses' template.\n *\n * @param {HTMLElement} areaReplace the DOM node to replace.\n * @returns {Promise}\n */\n var renderNoCourses = function(areaReplace) {\n return Templates.renderPix('courses', 'tool_moodlenet').then(function(img) {\n return img;\n }).then(function(img) {\n var temp = document.createElement('div');\n temp.innerHTML = img.trim();\n return Templates.render('core_course/no-courses', {\n nocoursesimg: temp.firstChild.src\n });\n }).then(function(html, js) {\n Templates.replaceNodeContents(areaReplace, html, js);\n areaReplace.classList.add('mx-auto');\n areaReplace.classList.add('w-25');\n return;\n });\n };\n\n /**\n * Render the course cards for those supplied courses.\n *\n * @param {HTMLElement} areaReplace the DOM node to replace.\n * @param {Array} courses the courses to render.\n * @returns {Promise}\n */\n var renderCourses = function(areaReplace, courses) {\n return Templates.render('tool_moodlenet/view-cards', {\n courses: courses\n }).then(function(html, js) {\n Templates.replaceNodeContents(areaReplace, html, js);\n areaReplace.classList.remove('mx-auto');\n areaReplace.classList.remove('w-25');\n return;\n });\n };\n\n /**\n * For a given input, the page & what to replace fetch courses and manage icons too.\n *\n * @method searchCourses\n * @param {string} inputValue What to search for\n * @param {HTMLElement} page The whole page element for our page\n * @param {HTMLElement} areaReplace The Element to replace the contents of\n */\n var searchCourses = function(inputValue, page, areaReplace) {\n var searchIcon = page.querySelector(Selectors.region.searchIcon);\n var clearIcon = page.querySelector(Selectors.region.clearIcon);\n\n if (inputValue !== '') {\n searchIcon.classList.add('d-none');\n clearIcon.parentElement.classList.remove('d-none');\n } else {\n searchIcon.classList.remove('d-none');\n clearIcon.parentElement.classList.add('d-none');\n }\n var args = {\n searchvalue: inputValue,\n };\n Ajax.call([{\n methodname: 'tool_moodlenet_search_courses',\n args: args\n }])[0].then(function(result) {\n if (result.courses.length === 0) {\n return renderNoCourses(areaReplace);\n } else {\n // Add the importId to the course link\n result.courses.forEach(function(course) {\n course.viewurl += '&id=' + importId;\n });\n return renderCourses(areaReplace, result.courses);\n }\n }).catch(Notification.exception);\n };\n\n /**\n * Add the event listeners to our page.\n *\n * @method registerListenerEvents\n * @param {HTMLElement} page The whole page element for our page\n */\n var registerListenerEvents = function(page) {\n var input = page.querySelector(Selectors.region.searchInput);\n var courseArea = page.querySelector(Selectors.region.courses);\n var clearIcon = page.querySelector(Selectors.region.clearIcon);\n clearIcon.addEventListener('click', function() {\n input.value = '';\n searchCourses('', page, courseArea);\n });\n\n input.addEventListener('input', debounce(function() {\n searchCourses(input.value, page, courseArea);\n }, 300));\n };\n\n /**\n * Fetch the courses to show the user. We use the same WS structure & template as the search for consistency.\n *\n * @method addCourses\n * @param {HTMLElement} page The whole page element for our course page\n */\n var addCourses = function(page) {\n var courseArea = page.querySelector(Selectors.region.courses);\n searchCourses('', page, courseArea);\n };\n\n /**\n * Define our own debounce function as Moodle 3.7 does not have it.\n *\n * @method debounce\n * @from underscore.js\n * @copyright 2009-2020 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n * @licence MIT\n * @param {function} func The function we want to keep calling\n * @param {number} wait Our timeout\n * @param {boolean} immediate Do we want to apply the function immediately\n * @return {function}\n */\n var debounce = function(func, wait, immediate) {\n var timeout;\n return function() {\n var context = this;\n var args = arguments;\n var later = function() {\n timeout = null;\n if (!immediate) {\n func.apply(context, args);\n }\n };\n var callNow = immediate && !timeout;\n clearTimeout(timeout);\n timeout = setTimeout(later, wait);\n if (callNow) {\n func.apply(context, args);\n }\n };\n };\n return {\n init: init,\n };\n});\n"],"file":"select_page.min.js"} \ No newline at end of file diff --git a/admin/tool/moodlenet/amd/build/selectors.min.js b/admin/tool/moodlenet/amd/build/selectors.min.js new file mode 100644 index 00000000000..b1db64f13e2 --- /dev/null +++ b/admin/tool/moodlenet/amd/build/selectors.min.js @@ -0,0 +1,2 @@ +define ("tool_moodlenet/selectors",[],function(){return{action:{browse:"[data-action=\"browse\"]",submit:"[data-action=\"submit\"]",showMoodleNet:"[data-action=\"show-moodlenet\"]",closeOption:"[data-action=\"close-chooser-option-summary\"]"},region:{clearIcon:"[data-region=\"clear-icon\"]",courses:"[data-region=\"mnet-courses\"]",instancePage:"[data-region=\"moodle-net\"]",searchInput:"[data-region=\"search-input\"]",searchIcon:"[data-region=\"search-icon\"]",selectPage:"[data-region=\"moodle-net-select\"]",spinner:"[data-region=\"spinner\"]",validationArea:"[data-region=\"validation-area\"]",carousel:"[data-region=\"carousel\"]",moodleNet:"[data-region=\"pluginCarousel\"]"}}}); +//# sourceMappingURL=selectors.min.js.map diff --git a/admin/tool/moodlenet/amd/build/selectors.min.js.map b/admin/tool/moodlenet/amd/build/selectors.min.js.map new file mode 100644 index 00000000000..8ef6f4bba46 --- /dev/null +++ b/admin/tool/moodlenet/amd/build/selectors.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/selectors.js"],"names":["define","action","browse","submit","showMoodleNet","closeOption","region","clearIcon","courses","instancePage","searchInput","searchIcon","selectPage","spinner","validationArea","carousel","moodleNet"],"mappings":"AAuBAA,OAAM,4BAAC,EAAD,CAAK,UAAW,CAClB,MAAO,CACHC,MAAM,CAAE,CACJC,MAAM,CAAE,0BADJ,CAEJC,MAAM,CAAE,0BAFJ,CAGJC,aAAa,CAAE,kCAHX,CAIJC,WAAW,CAAE,gDAJT,CADL,CAOHC,MAAM,CAAE,CACJC,SAAS,CAAE,8BADP,CAEJC,OAAO,CAAE,gCAFL,CAGJC,YAAY,CAAE,8BAHV,CAIJC,WAAW,CAAE,gCAJT,CAKJC,UAAU,CAAE,+BALR,CAMJC,UAAU,CAAE,qCANR,CAOJC,OAAO,CAAE,2BAPL,CAQJC,cAAc,CAAE,mCARZ,CASJC,QAAQ,CAAE,4BATN,CAUJC,SAAS,CAAE,kCAVP,CAPL,CAoBV,CArBK,CAAN","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 * Define all of the selectors we will be using within MoodleNet plugin.\n *\n * @module tool_moodlenet/selectors\n * @package tool_moodlenet\n * @copyright 2020 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([], function() {\n return {\n action: {\n browse: '[data-action=\"browse\"]',\n submit: '[data-action=\"submit\"]',\n showMoodleNet: '[data-action=\"show-moodlenet\"]',\n closeOption: '[data-action=\"close-chooser-option-summary\"]',\n },\n region: {\n clearIcon: '[data-region=\"clear-icon\"]',\n courses: '[data-region=\"mnet-courses\"]',\n instancePage: '[data-region=\"moodle-net\"]',\n searchInput: '[data-region=\"search-input\"]',\n searchIcon: '[data-region=\"search-icon\"]',\n selectPage: '[data-region=\"moodle-net-select\"]',\n spinner: '[data-region=\"spinner\"]',\n validationArea: '[data-region=\"validation-area\"]',\n carousel: '[data-region=\"carousel\"]',\n moodleNet: '[data-region=\"pluginCarousel\"]',\n },\n };\n});\n"],"file":"selectors.min.js"} \ No newline at end of file diff --git a/admin/tool/moodlenet/amd/build/validator.min.js b/admin/tool/moodlenet/amd/build/validator.min.js new file mode 100644 index 00000000000..1c4d02ae503 --- /dev/null +++ b/admin/tool/moodlenet/amd/build/validator.min.js @@ -0,0 +1,2 @@ +define ("tool_moodlenet/validator",["jquery","core/ajax","core/str","core/notification"],function(a,b,c,d){return{validation:function(e){var f=e.value;if(""===f||!f.includes("@")){a.when(c.get_string("profilevalidationerror","tool_moodlenet")).then(function(a){return Promise.reject().catch(function(){return{result:!1,message:a[0]}})}).fail(d.exception)}return b.call([{methodname:"tool_moodlenet_verify_webfinger",args:{profileurl:f,course:e.dataset.courseid,section:e.dataset.sectionid}}])[0].then(function(a){return a}).catch()}}}); +//# sourceMappingURL=validator.min.js.map diff --git a/admin/tool/moodlenet/amd/build/validator.min.js.map b/admin/tool/moodlenet/amd/build/validator.min.js.map new file mode 100644 index 00000000000..34c0c5b8fe0 --- /dev/null +++ b/admin/tool/moodlenet/amd/build/validator.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/validator.js"],"names":["define","$","Ajax","Str","Notification","validation","inputElement","inputValue","value","includes","when","get_string","then","strings","Promise","reject","catch","result","message","fail","exception","call","methodname","args","profileurl","course","dataset","courseid","section","sectionid"],"mappings":"AAuBAA,OAAM,4BAAC,CAAC,QAAD,CAAW,WAAX,CAAwB,UAAxB,CAAoC,mBAApC,CAAD,CAA2D,SAASC,CAAT,CAAYC,CAAZ,CAAkBC,CAAlB,CAAuBC,CAAvB,CAAqC,CAgClG,MAAO,CACHC,UAAU,CAzBG,SAAoBC,CAApB,CAAkC,CAC/C,GAAIC,CAAAA,CAAU,CAAGD,CAAY,CAACE,KAA9B,CAGA,GAAmB,EAAf,GAAAD,CAAU,EAAW,CAACA,CAAU,CAACE,QAAX,CAAoB,GAApB,CAA1B,CAAoD,CAEhDR,CAAC,CAACS,IAAF,CAAOP,CAAG,CAACQ,UAAJ,CAAe,wBAAf,CAAyC,gBAAzC,CAAP,EAAmEC,IAAnE,CAAwE,SAASC,CAAT,CAAkB,CACtF,MAAOC,CAAAA,OAAO,CAACC,MAAR,GAAiBC,KAAjB,CAAuB,UAAW,CACrC,MAAO,CAACC,MAAM,GAAP,CAAgBC,OAAO,CAAEL,CAAO,CAAC,CAAD,CAAhC,CACV,CAFM,CAGV,CAJD,EAIGM,IAJH,CAIQf,CAAY,CAACgB,SAJrB,CAKH,CAED,MAAOlB,CAAAA,CAAI,CAACmB,IAAL,CAAU,CAAC,CACdC,UAAU,CAAE,iCADE,CAEdC,IAAI,CAAE,CACFC,UAAU,CAAEjB,CADV,CAEFkB,MAAM,CAAEnB,CAAY,CAACoB,OAAb,CAAqBC,QAF3B,CAGFC,OAAO,CAAEtB,CAAY,CAACoB,OAAb,CAAqBG,SAH5B,CAFQ,CAAD,CAAV,EAOH,CAPG,EAOAjB,IAPA,CAOK,SAASK,CAAT,CAAiB,CACzB,MAAOA,CAAAA,CACV,CATM,EASJD,KATI,EAUV,CACM,CAGV,CAnCK,CAAN","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 * Our validator that splits the user's input then fires off to a webservice\n *\n * @module tool_moodlenet/validator\n * @package tool_moodlenet\n * @copyright 2020 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/ajax', 'core/str', 'core/notification'], function($, Ajax, Str, Notification) {\n /**\n * Handle form validation\n *\n * @method validation\n * @param {HTMLElement} inputElement The element the user entered text into.\n * @return {Promise} Was the users' entry a valid profile URL?\n */\n var validation = function validation(inputElement) {\n var inputValue = inputElement.value;\n\n // They didn't submit anything or they gave us a simple string that we can't do anything with.\n if (inputValue === \"\" || !inputValue.includes(\"@\")) {\n // Create a promise and immediately reject it.\n $.when(Str.get_string('profilevalidationerror', 'tool_moodlenet')).then(function(strings) {\n return Promise.reject().catch(function() {\n return {result: false, message: strings[0]};\n });\n }).fail(Notification.exception);\n }\n\n return Ajax.call([{\n methodname: 'tool_moodlenet_verify_webfinger',\n args: {\n profileurl: inputValue,\n course: inputElement.dataset.courseid,\n section: inputElement.dataset.sectionid\n }\n }])[0].then(function(result) {\n return result;\n }).catch();\n };\n return {\n validation: validation,\n };\n});\n"],"file":"validator.min.js"} \ No newline at end of file diff --git a/admin/tool/moodlenet/amd/src/instance_form.js b/admin/tool/moodlenet/amd/src/instance_form.js new file mode 100644 index 00000000000..a1443a0d51e --- /dev/null +++ b/admin/tool/moodlenet/amd/src/instance_form.js @@ -0,0 +1,169 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Our basic form manager for when a user either enters + * their profile url or just wants to browse. + * + * This file is a mishmash of JS functions we need for both the standalone (M3.7, M3.8) + * plugin & Moodle 3.9 functions. The 3.9 Functions have a base understanding that certain + * things exist i.e. directory structures for templates. When this feature goes 3.9+ only + * The goal is that we can quickly gut all AMD modules into bare JS files and use ES6 guidelines. + * Till then this will have to do. + * + * @module tool_moodlenet/instance_form + * @package tool_moodlenet + * @copyright 2020 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define(['tool_moodlenet/validator', + 'tool_moodlenet/selectors', + 'core/loadingicon', + 'core/templates', + 'core/notification', + 'jquery'], + function(Validator, + Selectors, + LoadingIcon, + Templates, + Notification, + $) { + + /** + * Add the event listeners to our form. + * + * @method registerListenerEvents + * @param {HTMLElement} page The whole page element for our form area + */ + var registerListenerEvents = function registerListenerEvents(page) { + page.addEventListener('click', function(e) { + + // Our fake submit button / browse button. + if (e.target.matches(Selectors.action.submit)) { + var input = page.querySelector('[data-var="mnet-link"]'); + var overlay = page.querySelector(Selectors.region.spinner); + var validationArea = document.querySelector(Selectors.region.validationArea); + + overlay.classList.remove('d-none'); + var spinner = LoadingIcon.addIconToContainerWithPromise(overlay); + Validator.validation(input) + .then(function(result) { + spinner.resolve(); + overlay.classList.add('d-none'); + if (result.result) { + input.classList.remove('is-invalid'); // Just in case the class has been applied already. + input.classList.add('is-valid'); + validationArea.innerText = result.message; + validationArea.classList.remove('text-error'); + validationArea.classList.add('text-success'); + // Give the user some time to see their input is valid. + setTimeout(function() { + window.location = result.domain; + }, 1000); + } else { + input.classList.add('is-invalid'); + validationArea.innerText = result.message; + validationArea.classList.add('text-error'); + } + return; + }).catch(); + } + }); + }; + + /** + * Given a user wishes to see the MoodleNet profile url form transition them there. + * + * @method chooserNavigateToMnet + * @param {HTMLElement} showMoodleNet The chooser's area for ment + * @param {Object} footerData Our footer object to render out + * @param {jQuery} carousel Our carousel instance to manage + * @param {jQuery} modal Our modal instance to manage + */ + var chooserNavigateToMnet = function(showMoodleNet, footerData, carousel, modal) { + showMoodleNet.innerHTML = ''; + + // Add a spinner. + var spinnerPromise = LoadingIcon.addIconToContainer(showMoodleNet); + + // Used later... + var transitionPromiseResolver = null; + var transitionPromise = new Promise(resolve => { + transitionPromiseResolver = resolve; + }); + + $.when( + spinnerPromise, + transitionPromise + ).then(function() { + Templates.replaceNodeContents(showMoodleNet, footerData.customcarouseltemplate, ''); + return; + }).catch(Notification.exception); + + // We apply our handlers in here to minimise plugin dependency in the Chooser. + registerListenerEvents(showMoodleNet); + + // Move to the next slide, and resolve the transition promise when it's done. + carousel.one('slid.bs.carousel', function() { + transitionPromiseResolver(); + }); + // Trigger the transition between 'pages'. + carousel.carousel(2); + // eslint-disable-next-line max-len + modal.setFooter(Templates.render('tool_moodlenet/chooser_footer_close_mnet', {})); + }; + + /** + * Given a user no longer wishes to see the MoodleNet profile url form transition them from there. + * + * @method chooserNavigateFromMnet + * @param {jQuery} carousel Our carousel instance to manage + * @param {jQuery} modal Our modal instance to manage + * @param {Object} footerData Our footer object to render out + */ + var chooserNavigateFromMnet = function(carousel, modal, footerData) { + // Trigger the transition between 'pages'. + carousel.carousel(0); + modal.setFooter(footerData.customfootertemplate); + }; + + /** + * Create the custom listener that would handle anything in the footer. + * + * @param {Event} e The event being triggered. + * @param {Object} footerData The data generated from the exporter. + * @param {Object} modal The chooser modal. + */ + var footerClickListener = function(e, footerData, modal) { + if (e.target.matches(Selectors.action.showMoodleNet) || e.target.closest(Selectors.action.showMoodleNet)) { + e.preventDefault(); + const carousel = $(modal.getBody()[0].querySelector(Selectors.region.carousel)); + const showMoodleNet = carousel.find(Selectors.region.moodleNet)[0]; + + chooserNavigateToMnet(showMoodleNet, footerData, carousel, modal); + } + // From the help screen go back to the module overview. + if (e.target.matches(Selectors.action.closeOption)) { + const carousel = $(modal.getBody()[0].querySelector(Selectors.region.carousel)); + + chooserNavigateFromMnet(carousel, modal, footerData); + } + }; + + return { + footerClickListener: footerClickListener + }; +}); diff --git a/admin/tool/moodlenet/amd/src/select_page.js b/admin/tool/moodlenet/amd/src/select_page.js new file mode 100644 index 00000000000..d9a1dc4ef20 --- /dev/null +++ b/admin/tool/moodlenet/amd/src/select_page.js @@ -0,0 +1,198 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * When returning to Moodle let the user select which course to add the resource to. + * + * @module tool_moodlenet/select_page + * @package tool_moodlenet + * @copyright 2020 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define([ + 'core/ajax', + 'core/templates', + 'tool_moodlenet/selectors', + 'core/notification' +], function( + Ajax, + Templates, + Selectors, + Notification +) { + /** + * @var {string} The id corresponding to the import. + */ + var importId; + + /** + * Set up the page. + * + * @method init + * @param {string} importIdString the string ID of the import. + */ + var init = function(importIdString) { + importId = importIdString; + var page = document.querySelector(Selectors.region.selectPage); + registerListenerEvents(page); + addCourses(page); + }; + + /** + * Renders the 'no-courses' template. + * + * @param {HTMLElement} areaReplace the DOM node to replace. + * @returns {Promise} + */ + var renderNoCourses = function(areaReplace) { + return Templates.renderPix('courses', 'tool_moodlenet').then(function(img) { + return img; + }).then(function(img) { + var temp = document.createElement('div'); + temp.innerHTML = img.trim(); + return Templates.render('core_course/no-courses', { + nocoursesimg: temp.firstChild.src + }); + }).then(function(html, js) { + Templates.replaceNodeContents(areaReplace, html, js); + areaReplace.classList.add('mx-auto'); + areaReplace.classList.add('w-25'); + return; + }); + }; + + /** + * Render the course cards for those supplied courses. + * + * @param {HTMLElement} areaReplace the DOM node to replace. + * @param {Array} courses the courses to render. + * @returns {Promise} + */ + var renderCourses = function(areaReplace, courses) { + return Templates.render('tool_moodlenet/view-cards', { + courses: courses + }).then(function(html, js) { + Templates.replaceNodeContents(areaReplace, html, js); + areaReplace.classList.remove('mx-auto'); + areaReplace.classList.remove('w-25'); + return; + }); + }; + + /** + * For a given input, the page & what to replace fetch courses and manage icons too. + * + * @method searchCourses + * @param {string} inputValue What to search for + * @param {HTMLElement} page The whole page element for our page + * @param {HTMLElement} areaReplace The Element to replace the contents of + */ + var searchCourses = function(inputValue, page, areaReplace) { + var searchIcon = page.querySelector(Selectors.region.searchIcon); + var clearIcon = page.querySelector(Selectors.region.clearIcon); + + if (inputValue !== '') { + searchIcon.classList.add('d-none'); + clearIcon.parentElement.classList.remove('d-none'); + } else { + searchIcon.classList.remove('d-none'); + clearIcon.parentElement.classList.add('d-none'); + } + var args = { + searchvalue: inputValue, + }; + Ajax.call([{ + methodname: 'tool_moodlenet_search_courses', + args: args + }])[0].then(function(result) { + if (result.courses.length === 0) { + return renderNoCourses(areaReplace); + } else { + // Add the importId to the course link + result.courses.forEach(function(course) { + course.viewurl += '&id=' + importId; + }); + return renderCourses(areaReplace, result.courses); + } + }).catch(Notification.exception); + }; + + /** + * Add the event listeners to our page. + * + * @method registerListenerEvents + * @param {HTMLElement} page The whole page element for our page + */ + var registerListenerEvents = function(page) { + var input = page.querySelector(Selectors.region.searchInput); + var courseArea = page.querySelector(Selectors.region.courses); + var clearIcon = page.querySelector(Selectors.region.clearIcon); + clearIcon.addEventListener('click', function() { + input.value = ''; + searchCourses('', page, courseArea); + }); + + input.addEventListener('input', debounce(function() { + searchCourses(input.value, page, courseArea); + }, 300)); + }; + + /** + * Fetch the courses to show the user. We use the same WS structure & template as the search for consistency. + * + * @method addCourses + * @param {HTMLElement} page The whole page element for our course page + */ + var addCourses = function(page) { + var courseArea = page.querySelector(Selectors.region.courses); + searchCourses('', page, courseArea); + }; + + /** + * Define our own debounce function as Moodle 3.7 does not have it. + * + * @method debounce + * @from underscore.js + * @copyright 2009-2020 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * @licence MIT + * @param {function} func The function we want to keep calling + * @param {number} wait Our timeout + * @param {boolean} immediate Do we want to apply the function immediately + * @return {function} + */ + var debounce = function(func, wait, immediate) { + var timeout; + return function() { + var context = this; + var args = arguments; + var later = function() { + timeout = null; + if (!immediate) { + func.apply(context, args); + } + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { + func.apply(context, args); + } + }; + }; + return { + init: init, + }; +}); diff --git a/admin/tool/moodlenet/amd/src/selectors.js b/admin/tool/moodlenet/amd/src/selectors.js new file mode 100644 index 00000000000..5feb0f39d20 --- /dev/null +++ b/admin/tool/moodlenet/amd/src/selectors.js @@ -0,0 +1,45 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Define all of the selectors we will be using within MoodleNet plugin. + * + * @module tool_moodlenet/selectors + * @package tool_moodlenet + * @copyright 2020 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define([], function() { + return { + action: { + browse: '[data-action="browse"]', + submit: '[data-action="submit"]', + showMoodleNet: '[data-action="show-moodlenet"]', + closeOption: '[data-action="close-chooser-option-summary"]', + }, + region: { + clearIcon: '[data-region="clear-icon"]', + courses: '[data-region="mnet-courses"]', + instancePage: '[data-region="moodle-net"]', + searchInput: '[data-region="search-input"]', + searchIcon: '[data-region="search-icon"]', + selectPage: '[data-region="moodle-net-select"]', + spinner: '[data-region="spinner"]', + validationArea: '[data-region="validation-area"]', + carousel: '[data-region="carousel"]', + moodleNet: '[data-region="pluginCarousel"]', + }, + }; +}); diff --git a/admin/tool/moodlenet/amd/src/validator.js b/admin/tool/moodlenet/amd/src/validator.js new file mode 100644 index 00000000000..704bd6710ac --- /dev/null +++ b/admin/tool/moodlenet/amd/src/validator.js @@ -0,0 +1,59 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Our validator that splits the user's input then fires off to a webservice + * + * @module tool_moodlenet/validator + * @package tool_moodlenet + * @copyright 2020 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/ajax', 'core/str', 'core/notification'], function($, Ajax, Str, Notification) { + /** + * Handle form validation + * + * @method validation + * @param {HTMLElement} inputElement The element the user entered text into. + * @return {Promise} Was the users' entry a valid profile URL? + */ + var validation = function validation(inputElement) { + var inputValue = inputElement.value; + + // They didn't submit anything or they gave us a simple string that we can't do anything with. + if (inputValue === "" || !inputValue.includes("@")) { + // Create a promise and immediately reject it. + $.when(Str.get_string('profilevalidationerror', 'tool_moodlenet')).then(function(strings) { + return Promise.reject().catch(function() { + return {result: false, message: strings[0]}; + }); + }).fail(Notification.exception); + } + + return Ajax.call([{ + methodname: 'tool_moodlenet_verify_webfinger', + args: { + profileurl: inputValue, + course: inputElement.dataset.courseid, + section: inputElement.dataset.sectionid + } + }])[0].then(function(result) { + return result; + }).catch(); + }; + return { + validation: validation, + }; +}); diff --git a/admin/tool/moodlenet/classes/external.php b/admin/tool/moodlenet/classes/external.php new file mode 100644 index 00000000000..5fbfd2052d8 --- /dev/null +++ b/admin/tool/moodlenet/classes/external.php @@ -0,0 +1,189 @@ +. + +/** + * This is the external API for this component. + * + * @package tool_moodlenet + * @copyright 2020 Mathew May {@link https://mathew.solutions} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_moodlenet; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir .'/externallib.php'); +require_once($CFG->libdir . '/filelib.php'); +require_once(__DIR__ . '/../lib.php'); + +use core_course\external\course_summary_exporter; +use external_api; +use external_function_parameters; +use external_value; +use external_single_structure; + +/** + * This is the external API for this component. + * + * @copyright 2020 Mathew May {@link https://mathew.solutions} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class external extends external_api { + + /** + * verify_webfinger parameters + * + * @return external_function_parameters + */ + public static function verify_webfinger_parameters() { + return new external_function_parameters( + array( + 'profileurl' => new external_value(PARAM_RAW, 'The profile url that the user has given us', VALUE_REQUIRED), + 'course' => new external_value(PARAM_INT, 'The course we are adding to', VALUE_REQUIRED), + 'section' => new external_value(PARAM_INT, 'The section within the course we are adding to', VALUE_REQUIRED), + ) + ); + } + + /** + * Figure out if the passed content resolves with a WebFinger account. + * + * @param string $profileurl The profile url that the user states exists + * @param int $course The course we are adding to + * @param int $section The section within the course we are adding to + * @return array Contains the result and domain if any + * @throws \invalid_parameter_exception + */ + public static function verify_webfinger(string $profileurl, int $course, int $section) { + global $USER; + + $params = self::validate_parameters(self::verify_webfinger_parameters(), [ + 'profileurl' => $profileurl, + 'section' => $section, + 'course' => $course + ] + ); + try { + $mnetprofile = new moodlenet_user_profile($params['profileurl'], $USER->id); + } catch (\Exception $e) { + return [ + 'result' => false, + 'message' => get_string('profilevalidationfail', 'tool_moodlenet'), + ]; + } + + $userlink = profile_manager::get_moodlenet_profile_link($mnetprofile); + + // There were no problems verifying the account so lets store it. + if ($userlink['result'] === true) { + profile_manager::save_moodlenet_user_profile($mnetprofile); + $userlink['domain'] = generate_mnet_endpoint($mnetprofile->get_profile_name(), $course, $section); + } + + return $userlink; + } + + /** + * verify_webfinger return. + * + * @return \external_description + */ + public static function verify_webfinger_returns() { + return new external_single_structure([ + 'result' => new external_value(PARAM_BOOL, 'Was the passed content a valid WebFinger?'), + 'message' => new external_value(PARAM_TEXT, 'Our message for the user'), + 'domain' => new external_value(PARAM_RAW, 'Domain to redirect the user to', VALUE_OPTIONAL), + ]); + } + + /** + * search_courses_parameters + * + * @return external_function_parameters + */ + public static function search_courses_parameters() { + return new external_function_parameters( + array( + 'searchvalue' => new external_value(PARAM_RAW, 'search value'), + ) + ); + } + + /** + * For some given input find and return any course that matches it. + * + * @param string $searchvalue The profile url that the user states exists + * @return array Contains the result set of courses for the value + */ + public static function search_courses(string $searchvalue) { + global $OUTPUT; + + $params = self::validate_parameters( + self::search_courses_parameters(), + ['searchvalue' => $searchvalue] + ); + self::validate_context(\context_system::instance()); + + $courses = array(); + + if ($arrcourses = \core_course_category::search_courses(array('search' => $params['searchvalue']))) { + foreach ($arrcourses as $course) { + if (has_capability('moodle/course:manageactivities', \context_course::instance($course->id))) { + $data = new \stdClass(); + $data->id = $course->id; + $data->fullname = $course->fullname; + $data->hidden = $course->visible; + $options = [ + 'course' => $course->id, + ]; + $viewurl = new \moodle_url('/admin/tool/moodlenet/options.php', $options); + $data->viewurl = $viewurl->out(false); + $category = \core_course_category::get($course->category); + $data->coursecategory = $category->name; + $courseimage = course_summary_exporter::get_course_image($data); + if (!$courseimage) { + $courseimage = $OUTPUT->get_generated_image_for_id($data->id); + } + $data->courseimage = $courseimage; + $courses[] = $data; + } + } + } + return array( + 'courses' => $courses + ); + } + + /** + * search_courses_returns. + * + * @return \external_description + */ + public static function search_courses_returns() { + return new external_single_structure([ + 'courses' => new \external_multiple_structure( + new external_single_structure([ + 'id' => new external_value(PARAM_INT, 'course id'), + 'fullname' => new external_value(PARAM_TEXT, 'course full name'), + 'hidden' => new external_value(PARAM_INT, 'is the course visible'), + 'viewurl' => new external_value(PARAM_URL, 'Next step of import'), + 'coursecategory' => new external_value(PARAM_TEXT, 'Category name'), + 'courseimage' => new external_value(PARAM_RAW, 'course image'), + ])) + ]); + } +} diff --git a/admin/tool/moodlenet/classes/local/import_backup_helper.php b/admin/tool/moodlenet/classes/local/import_backup_helper.php new file mode 100644 index 00000000000..a2f923afb74 --- /dev/null +++ b/admin/tool/moodlenet/classes/local/import_backup_helper.php @@ -0,0 +1,194 @@ +. +/** + * Contains the import_backup_helper class. + * + * @package tool_moodlenet + * @copyright 2020 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_moodlenet\local; + +/** + * The import_backup_helper class. + * + * The import_backup_helper objects provide a means to prepare a backup for for restoration of a course or activity backup file. + * + * @copyright 2020 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class import_backup_helper { + + /** @var remote_resource $remoteresource A file resource to be restored. */ + protected $remoteresource; + + /** @var user $user The user trying to restore a file. */ + protected $user; + + /** @var context $context The context we are trying to restore this file into. */ + protected $context; + + /** @var int $useruploadlimit The size limit that this user can upload in this context. */ + protected $useruploadlimit; + + /** + * Constructor for the import backup helper. + * + * @param remote_resource $remoteresource A remote file resource + * @param \stdClass $user The user importing a file. + * @param \context $context Context to restore into. + */ + public function __construct(remote_resource $remoteresource, \stdClass $user, \context $context) { + $this->remoteresource = $remoteresource; + $this->user = $user; + $this->context = $context; + + $maxbytes = 0; + if ($this->context->contextlevel == CONTEXT_COURSE) { + $course = get_course($this->context->instanceid); + $maxbytes = $course->maxbytes; + } + $this->useruploadlimit = get_user_max_upload_file_size($this->context, get_config('core', 'maxbytes'), + $maxbytes, 0, $this->user); + } + + /** + * Return a stored user draft file for processing. + * + * @return \stored_file The imported file to ultimately be restored. + */ + public function get_stored_file(): \stored_file { + + // Check if the user can upload a backup to this context. + require_capability('moodle/restore:uploadfile', $this->context, $this->user->id); + + // Before starting a potentially lengthy download, try to ensure the file size does not exceed the upload size restrictions + // for the user. This is a time saving measure. + // This is a naive check, that serves only to catch files if they provide the content length header. + // Because of potential content encoding (compression), the stored file will be checked again after download as well. + $size = $this->remoteresource->get_download_size() ?? -1; + if ($this->size_exceeds_upload_limit($size)) { + throw new \moodle_exception('uploadlimitexceeded', 'tool_moodlenet', '', ['filesize' => $size, + 'uploadlimit' => $this->useruploadlimit]); + } + + [$filepath, $filename] = $this->remoteresource->download_to_requestdir(); + \core\antivirus\manager::scan_file($filepath, $filename, true); + + // Check the final size of file against the user upload limits. + $localsize = filesize(sprintf('%s/%s', $filepath, $filename)); + if ($this->size_exceeds_upload_limit($localsize)) { + throw new \moodle_exception('uploadlimitexceeded', 'tool_moodlenet', '', ['filesize' => $localsize, + 'uploadlimit' => $this->useruploadlimit]); + } + + return $this->create_user_draft_stored_file($filename, $filepath); + } + + /** + * Does the size exceed the upload limit for the current import, taking into account user and core settings. + * + * @param int $sizeinbytes + * @return bool true if exceeded, false otherwise. + */ + protected function size_exceeds_upload_limit(int $sizeinbytes): bool { + $maxbytes = 0; + if ($this->context->contextlevel == CONTEXT_COURSE) { + $course = get_course($this->context->instanceid); + $maxbytes = $course->maxbytes; + } + $maxbytes = get_user_max_upload_file_size($this->context, get_config('core', 'maxbytes'), $maxbytes, 0, + $this->user); + if ($maxbytes != USER_CAN_IGNORE_FILE_SIZE_LIMITS && $sizeinbytes > $maxbytes) { + return true; + } + return false; + } + + /** + * Create a file in the user drafts ready for use by plugins implementing dndupload_handle(). + * + * @param string $filename the name of the file on disk + * @param string $path the path where the file is stored on disk + * @return \stored_file + */ + protected function create_user_draft_stored_file(string $filename, string $path): \stored_file { + global $CFG; + + $record = new \stdClass(); + $record->filearea = 'draft'; + $record->component = 'user'; + $record->filepath = '/'; + $record->itemid = file_get_unused_draft_itemid(); + $record->license = $CFG->sitedefaultlicense; + $record->author = ''; + $record->filename = clean_param($filename, PARAM_FILE); + $record->contextid = \context_user::instance($this->user->id)->id; + $record->userid = $this->user->id; + + $fullpathwithname = sprintf('%s/%s', $path, $filename); + + $fs = get_file_storage(); + + return $fs->create_file_from_pathname($record, $fullpathwithname); + } + + /** + * Looks for a context that this user has permission to upload backup files to. + * This gets a list of roles that the user has, checks for the restore:uploadfile capability and then sends back a context + * that has this permission if available. + * + * This starts with the highest context level and moves down i.e. system -> category -> course. + * + * @param int $userid The user ID that we are looking for a working context for. + * @return \context A context that allows the upload of backup files. + */ + public static function get_context_for_user(int $userid): ?\context { + global $DB; + + if (is_siteadmin()) { + return \context_system::instance(); + } + + $sql = "SELECT ctx.id, ctx.contextlevel, ctx.instanceid, ctx.path, ctx.depth, ctx.locked + FROM {context} ctx + JOIN {role_assignments} r ON ctx.id = r.contextid + WHERE r.userid = :userid AND ctx.contextlevel IN (:contextsystem, :contextcategory, :contextcourse) + ORDER BY ctx.contextlevel ASC"; + + $params = [ + 'userid' => $userid, + 'contextsystem' => CONTEXT_SYSTEM, + 'contextcategory' => CONTEXT_COURSECAT, + 'contextcourse' => CONTEXT_COURSE + ]; + $records = $DB->get_records_sql($sql, $params); + foreach ($records as $record) { + \context_helper::preload_from_record($record); + if ($record->contextlevel == CONTEXT_COURSECAT) { + $context = \context_coursecat::instance($record->instanceid); + } else if ($record->contextlevel == CONTEXT_COURSE) { + $context = \context_course::instance($record->instanceid); + } else { + $context = \context_system::instance(); + } + if (has_capability('moodle/restore:uploadfile', $context, $userid)) { + return $context; + } + } + return null; + } +} diff --git a/admin/tool/moodlenet/classes/local/import_handler_info.php b/admin/tool/moodlenet/classes/local/import_handler_info.php new file mode 100644 index 00000000000..8517f376c25 --- /dev/null +++ b/admin/tool/moodlenet/classes/local/import_handler_info.php @@ -0,0 +1,91 @@ +. +/** + * Contains the import_handler_info class. + * + * @package tool_moodlenet + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_moodlenet\local; + +/** + * The import_handler_info class. + * + * An import_handler_info object represent an resource import handler for a particular module. + * + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class import_handler_info { + + /** @var string $modulename the name of the module. */ + protected $modulename; + + /** @var string $description the description. */ + protected $description; + + /** @var import_strategy $importstrategy the strategy which will be used to import resources handled by this handler */ + protected $importstrategy; + + /** + * The import_handler_info constructor. + * + * @param string $modulename the name of the module handling the file extension. E.g. 'label'. + * @param string $description A description of how the module handles files of this extension type. + * @param import_strategy $strategy the strategy which will be used to import the resource. + * @throws \coding_exception + */ + public function __construct(string $modulename, string $description, import_strategy $strategy) { + if (empty($modulename)) { + throw new \coding_exception("Module name cannot be empty."); + } + if (empty($description)) { + throw new \coding_exception("Description cannot be empty."); + } + $this->modulename = $modulename; + $this->description = $description; + $this->importstrategy = $strategy; + } + + /** + * Get the name of the module. + * + * @return string the module name, e.g. 'label'. + */ + public function get_module_name(): string { + return $this->modulename; + } + + /** + * Get a human readable, localised description of how the file is handled by the module. + * + * @return string the localised description. + */ + public function get_description(): string { + return $this->description; + } + + /** + * Get the import strategy used by this handler. + * + * @return import_strategy the import strategy object. + */ + public function get_strategy(): import_strategy { + return $this->importstrategy; + } +} diff --git a/admin/tool/moodlenet/classes/local/import_handler_registry.php b/admin/tool/moodlenet/classes/local/import_handler_registry.php new file mode 100644 index 00000000000..b5860b5dbba --- /dev/null +++ b/admin/tool/moodlenet/classes/local/import_handler_registry.php @@ -0,0 +1,188 @@ +. +/** + * Contains the import_handler_registry class. + * + * @package tool_moodlenet + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_moodlenet\local; + +/** + * The import_handler_registry class. + * + * The import_handler_registry objects represent a register of modules handling various file extensions for a given course and user. + * Only modules which are available to the user in the course are included in the register for that user. + * + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class import_handler_registry { + + /** + * @var array array containing the names and messages of all modules handling import of resources as a 'file' type. + */ + protected $filehandlers = []; + + /** + * @var array $typehandlers the array of modules registering as handlers of other, non-file types, indexed by typename. + */ + protected $typehandlers = []; + + /** + * @var array $registry the aggregate of all registrations made by plugins, indexed by 'file' and 'type'. + */ + protected $registry = []; + + /** + * @var \context_course the course context object. + */ + protected $context; + + /** + * @var \stdClass a course object. + */ + protected $course; + + /** + * @var \stdClass a user object. + */ + protected $user; + + /** + * The import_handler_registry constructor. + * + * @param \stdClass $course the course, which impacts available handlers. + * @param \stdClass $user the user, which impacts available handlers. + */ + public function __construct(\stdClass $course, \stdClass $user) { + $this->course = $course; + $this->user = $user; + $this->context = \context_course::instance($course->id); + + // Generate the full list of handlers for all extensions for this user and course. + $this->populate_handlers(); + } + + /** + * Get all handlers for the remote resource, depending on the strategy being used to import the resource. + * + * @param remote_resource $resource the remote resource. + * @param import_strategy $strategy an import_strategy instance. + * @return import_handler_info[] the array of import_handler_info handlers. + */ + public function get_resource_handlers_for_strategy(remote_resource $resource, import_strategy $strategy): array { + return $strategy->get_handlers($this->registry, $resource); + } + + /** + * Get a specific handler for the resource, belonging to a specific module and for a specific strategy. + * + * @param remote_resource $resource the remote resource. + * @param string $modname the name of the module, e.g. 'label'. + * @param import_strategy $strategy a string representing how to treat the resource. e.g. 'file', 'link'. + * @return import_handler_info|null the import_handler_info object, if found, otherwise null. + */ + public function get_resource_handler_for_mod_and_strategy(remote_resource $resource, string $modname, + import_strategy $strategy): ?import_handler_info { + foreach ($strategy->get_handlers($this->registry, $resource) as $handler) { + if ($handler->get_module_name() === $modname) { + return $handler; + } + } + return null; + } + + /** + * Build up a list of extension handlers by leveraging the dndupload_register callbacks. + */ + protected function populate_handlers() { + // Generate a dndupload_handler object, just so we can call ->is_known_type() on the types being registered by plugins. + // We must vet each type which is reported to be handled against the list of known, supported types. + global $CFG; + require_once($CFG->dirroot . '/course/dnduploadlib.php'); + $dndhandlers = new \dndupload_handler($this->course); + + // Get the list of mods enabled at site level first. We need to cross check this. + $pluginman = \core_plugin_manager::instance(); + $sitemods = $pluginman->get_plugins_of_type('mod'); + $sitedisabledmods = array_filter($sitemods, function(\core\plugininfo\mod $modplugininfo){ + return !$modplugininfo->is_enabled(); + }); + $sitedisabledmods = array_map(function($modplugininfo) { + return $modplugininfo->name; + }, $sitedisabledmods); + + // Loop through all modules to find the registered handlers. + $mods = get_plugin_list_with_function('mod', 'dndupload_register'); + foreach ($mods as $component => $funcname) { + list($modtype, $modname) = \core_component::normalize_component($component); + if (!empty($sitedisabledmods) && array_key_exists($modname, $sitedisabledmods)) { + continue; // Module is disabled at the site level. + } + if (!course_allowed_module($this->course, $modname, $this->user)) { + continue; // User does not have permission to add this module to the course. + } + + if (!$resp = component_callback($component, 'dndupload_register')) { + continue; + }; + + if (isset($resp['files'])) { + foreach ($resp['files'] as $file) { + $this->register_file_handler($file['extension'], $modname, $file['message']); + } + } + if (isset($resp['types'])) { + foreach ($resp['types'] as $type) { + if (!$dndhandlers->is_known_type($type['identifier'])) { + throw new \coding_exception("Trying to add handler for unknown type $type"); + } + $this->register_type_handler($type['identifier'], $modname, $type['message']); + } + } + } + $this->registry = [ + 'files' => $this->filehandlers, + 'types' => $this->typehandlers + ]; + } + + /** + * Adds a type handler to the list. + * + * @param string $identifier the name of the type. + * @param string $module the name of the module, e.g. 'label'. + * @param string $message the message describing how the module handles the type. + */ + protected function register_type_handler(string $identifier, string $module, string $message) { + $this->typehandlers[$identifier][] = ['module' => $module, 'message' => $message]; + } + + /** + * Adds a file extension handler to the list. + * + * @param string $extension the extension, e.g. 'png'. + * @param string $module the name of the module handling this extension + * @param string $message the message describing how the module handles the extension. + */ + protected function register_file_handler(string $extension, string $module, string $message) { + $extension = strtolower($extension); + $this->filehandlers[$extension][] = ['module' => $module, 'message' => $message]; + } +} + diff --git a/admin/tool/moodlenet/classes/local/import_info.php b/admin/tool/moodlenet/classes/local/import_info.php new file mode 100644 index 00000000000..818f4c551e2 --- /dev/null +++ b/admin/tool/moodlenet/classes/local/import_info.php @@ -0,0 +1,126 @@ +. +/** + * Contains the import_info class. + * + * @package tool_moodlenet + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_moodlenet\local; + +/** + * Class import_info, describing objects which represent a resource being imported by a user. + * + * Objects of this class encapsulate both: + * - information about the resource (remote_resource). + * - config data pertaining to the import process, such as the destination course and section + * and how the resource should be treated (i.e. the type and the name of the module selected as the import handler) + * + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class import_info { + + /** @var int $userid the user conducting this import. */ + protected $userid; + + /** @var remote_resource $resource the resource being imported. */ + protected $resource; + + /** @var \stdClass $config config data pertaining to the import process, e.g. course, section, type. */ + protected $config; + + /** @var string $id string identifier for this object. */ + protected $id; + + /** + * The import_controller constructor. + * + * @param int $userid the id of the user performing the import. + * @param remote_resource $resource the resource being imported. + * @param \stdClass $config import config like 'course', 'section', 'type'. + */ + public function __construct(int $userid, remote_resource $resource, \stdClass $config) { + $this->userid = $userid; + $this->resource = $resource; + $this->config = $config; + $this->id = md5($resource->get_url()->get_value()); + } + + /** + * Get the id of this object. + */ + public function get_id() { + return $this->id; + } + + /** + * Get the remote resource being imported. + * + * @return remote_resource the remote resource being imported. + */ + public function get_resource(): remote_resource { + return $this->resource; + } + + /** + * Get the configuration data pertaining to the import. + * + * @return \stdClass the import configuration data. + */ + public function get_config(): \stdClass { + return $this->config; + } + + /** + * Set the configuration data pertaining to the import. + * + * @param \stdClass $config the configuration data to set. + */ + public function set_config(\stdClass $config): void { + $this->config = $config; + } + + /** + * Get an import_info object by id. + * + * @param string $id the id of the import_info object to load. + * @return mixed an import_info object if found, otherwise null. + */ + public static function load(string $id): ?import_info { + // This currently lives in the session, so we don't need userid. + // It might be useful if we ever move to another storage mechanism however, where we would need it. + global $SESSION; + return isset($SESSION->moodlenetimports[$id]) ? unserialize($SESSION->moodlenetimports[$id]) : null; + } + + /** + * Save this object to a store which is accessible across requests. + */ + public function save(): void { + global $SESSION; + $SESSION->moodlenetimports[$this->id] = serialize($this); + } + + /** + * Remove all information about an import from the store. + */ + public function purge(): void { + global $SESSION; + unset($SESSION->moodlenetimports[$this->id]); + } +} diff --git a/admin/tool/moodlenet/classes/local/import_processor.php b/admin/tool/moodlenet/classes/local/import_processor.php new file mode 100644 index 00000000000..8afd4d3b8cc --- /dev/null +++ b/admin/tool/moodlenet/classes/local/import_processor.php @@ -0,0 +1,206 @@ +. +/** + * Contains the import_processor class. + * + * @package tool_moodlenet + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_moodlenet\local; + +/** + * The import_processor class. + * + * The import_processor objects provide a means to import a remote resource into a course section, delegating the handling of + * content to the relevant module, via its dndupload_handler callback. + * + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class import_processor { + + /** @var object The course that we are uploading to */ + protected $course = null; + + /** @var int The section number we are uploading to */ + protected $section = null; + + /** @var import_handler_registry $handlerregistry registry object to use for cross checking the supplied handler.*/ + protected $handlerregistry; + + /** @var import_handler_info $handlerinfo information about the module handling the import.*/ + protected $handlerinfo; + + /** @var \stdClass $user the user conducting the import.*/ + protected $user; + + /** @var remote_resource $remoteresource the remote resource being imported.*/ + protected $remoteresource; + + /** @var string[] $descriptionoverrides list of modules which support having their descriptions updated, post-import. */ + protected $descriptionoverrides = ['folder', 'page', 'resource', 'scorm', 'url']; + + /** + * The import_processor constructor. + * + * @param \stdClass $course the course object. + * @param int $section the section number in the course, starting at 0. + * @param remote_resource $remoteresource the remote resource to import. + * @param import_handler_info $handlerinfo information about which module is handling the import. + * @param import_handler_registry $handlerregistry A registry of import handlers, to use for validation. + * @throws \coding_exception If any of the params are invalid. + */ + public function __construct(\stdClass $course, int $section, remote_resource $remoteresource, import_handler_info $handlerinfo, + import_handler_registry $handlerregistry) { + + global $DB, $USER; + + if ($section < 0) { + throw new \coding_exception("Invalid section number $section. Must be > 0."); + } + if (!$DB->record_exists('modules', array('name' => $handlerinfo->get_module_name()))) { + throw new \coding_exception("Module {$handlerinfo->get_module_name()} does not exist"); + } + + $this->course = $course; + $this->section = $section; + $this->handlerregistry = $handlerregistry; + $this->user = $USER; + $this->remoteresource = $remoteresource; + $this->handlerinfo = $handlerinfo; + + // ALL handlers must have a strategy and ANY strategy can process ANY resource. + // It is therefore NOT POSSIBLE to have a resource that CANNOT be processed by a handler. + // So, there's no need to verify that the remote_resource CAN be handled by the handler. It always can. + } + + /** + * Run the import process, including file download, module creation and cleanup (cache purge, etc). + */ + public function process(): void { + // Allow the strategy to do setup for this file import. + $moduledata = $this->handlerinfo->get_strategy()->import($this->remoteresource, $this->user, $this->course, $this->section); + + // Create the course module, and add that information to the data to be sent to the plugin handling the resource. + $cmdata = $this->create_course_module($this->course, $this->section, $this->handlerinfo->get_module_name()); + $moduledata->coursemodule = $cmdata->id; + + // Now, send the data to the handling plugin to let it set up. + $instanceid = plugin_callback('mod', $this->handlerinfo->get_module_name(), 'dndupload', 'handle', [$moduledata], + 'invalidfunction'); + if ($instanceid == 'invalidfunction') { + $name = $this->handlerinfo->get_module_name(); + throw new \coding_exception("$name does not support drag and drop upload (missing {$name}_dndupload_handle function)"); + } + + // Now, update the module description if the module supports it and only if it's not currently set. + $this->update_module_description($instanceid); + + // Finish setting up the course module. + $this->finish_setup_course_module($instanceid, $cmdata->id); + } + + /** + * Update the module's description (intro), if that feature is supported. + * + * @param int $instanceid the instance id of the module to update. + */ + protected function update_module_description(int $instanceid): void { + global $DB, $CFG; + require_once($CFG->libdir . '/moodlelib.php'); + + if (plugin_supports('mod', $this->handlerinfo->get_module_name(), FEATURE_MOD_INTRO, true)) { + require_once($CFG->libdir . '/editorlib.php'); + require_once($CFG->libdir . '/modinfolib.php'); + + $rec = $DB->get_record($this->handlerinfo->get_module_name(), ['id' => $instanceid]); + + if (empty($rec->intro) || in_array($this->handlerinfo->get_module_name(), $this->descriptionoverrides)) { + $updatedata = (object)[ + 'id' => $instanceid, + 'intro' => clean_param($this->remoteresource->get_description(), PARAM_TEXT), + 'introformat' => editors_get_preferred_format() + ]; + + $DB->update_record($this->handlerinfo->get_module_name(), $updatedata); + + rebuild_course_cache($this->course->id, true); + } + } + } + + /** + * Create the course module to hold the file/content that has been uploaded. + * @param \stdClass $course the course object. + * @param int $section the section. + * @param string $modname the name of the module, e.g. 'label'. + * @return \stdClass the course module data. + */ + protected function create_course_module(\stdClass $course, int $section, string $modname): \stdClass { + global $CFG; + require_once($CFG->dirroot . '/course/modlib.php'); + list($module, $context, $cw, $cm, $data) = prepare_new_moduleinfo_data($course, $modname, $section); + $data->visible = false; // The module is created in a hidden state. + $data->coursemodule = $data->id = add_course_module($data); + return $data; + } + + /** + * Finish off any course module setup, such as adding to the course section and firing events. + * + * @param int $instanceid id returned by the mod when it was created. + * @param int $cmid the course module record id, for removal if something went wrong. + */ + protected function finish_setup_course_module($instanceid, int $cmid): void { + global $DB; + + if (!$instanceid) { + // Something has gone wrong - undo everything we can. + course_delete_module($cmid); + throw new \moodle_exception('errorcreatingactivity', 'moodle', '', $this->handlerinfo->get_module_name()); + } + + // Note the section visibility. + $visible = get_fast_modinfo($this->course)->get_section_info($this->section)->visible; + + $DB->set_field('course_modules', 'instance', $instanceid, array('id' => $cmid)); + + // Rebuild the course cache after update action. + rebuild_course_cache($this->course->id, true); + + course_add_cm_to_section($this->course, $cmid, $this->section); + + set_coursemodule_visible($cmid, $visible); + if (!$visible) { + $DB->set_field('course_modules', 'visibleold', 1, array('id' => $cmid)); + } + + // Retrieve the final info about this module. + $info = get_fast_modinfo($this->course, $this->user->id); + if (!isset($info->cms[$cmid])) { + // The course module has not been properly created in the course - undo everything. + course_delete_module($cmid); + throw new \moodle_exception('errorcreatingactivity', 'moodle', '', $this->handlerinfo->get_module_name()); + } + $mod = $info->get_cm($cmid); + + // Trigger course module created event. + $event = \core\event\course_module_created::create_from_cm($mod); + $event->trigger(); + } +} + diff --git a/admin/tool/moodlenet/classes/local/import_strategy.php b/admin/tool/moodlenet/classes/local/import_strategy.php new file mode 100644 index 00000000000..d647e26b63c --- /dev/null +++ b/admin/tool/moodlenet/classes/local/import_strategy.php @@ -0,0 +1,69 @@ +. +/** + * Contains the import_strategy interface. + * + * @package tool_moodlenet + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_moodlenet\local; + +/** + * The import_strategy interface. + * + * This provides a contract allowing different import strategies to be implemented. + * + * An import_strategy encapsulates the logic used to prepare a remote_resource for import into Moodle in some way and is used by the + * import_processor (to perform aforementioned preparations) before it hands control of the import over to a course module plugin. + * + * We may wish to have many strategies because the preparation steps may vary depending on how the resource is to be treated. + * E.g. We may wish to import as a file in which case download steps will be required, or we may simply wish to import the remote + * resource as a link, in which cases setup steps will not require any file download. + * + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface import_strategy { + + /** + * Get an array of import_handler_info objects supported by this import strategy, based on the registrydata and resource. + * + * Implementations should check the registry data for any entries which align with their import strategy and should create + * import_handler_info objects to represent each relevant entry. If an entry represents a module, or handling type which does + * not align with the strategy, that item should simply be skipped. + * + * E.g. If one strategy aims to import all remote resources as files (e.g. import_strategy_file), it would only generate a list + * of import_handler_info objects created from those registry entries of type 'file', as those entries represent the modules + * which have said they can handle resources as files. + * + * @param array $registrydata The fully populated handler registry. + * @param remote_resource $resource the remote resource. + * @return import_handler_info[] the array of import_handler_info objects, or an empty array if none were matched. + */ + public function get_handlers(array $registrydata, remote_resource $resource): array; + + /** + * Called during import to perform required import setup steps. + * + * @param remote_resource $resource the resource to import. + * @param \stdClass $user the user to import on behalf of. + * @param \stdClass $course the course into which the remote resource is being imported. + * @param int $section the section into which the remote resource is being imported. + * @return \stdClass the module data which will be passed on to the course module plugin. + */ + public function import(remote_resource $resource, \stdClass $user, \stdClass $course, int $section): \stdClass; +} diff --git a/admin/tool/moodlenet/classes/local/import_strategy_file.php b/admin/tool/moodlenet/classes/local/import_strategy_file.php new file mode 100644 index 00000000000..e34092a62dc --- /dev/null +++ b/admin/tool/moodlenet/classes/local/import_strategy_file.php @@ -0,0 +1,170 @@ +. +/** + * Contains the import_strategy_file class. + * + * @package tool_moodlenet + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_moodlenet\local; + +use core\antivirus\manager as avmanager; + +/** + * The import_strategy_file class. + * + * The import_strategy_file objects contains the setup steps needed to prepare a resource for import as a file into Moodle. This + * ensures the remote_resource is first downloaded and put in a draft file area, ready for use as a file by the handling module. + * + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class import_strategy_file implements import_strategy { + + /** + * Get an array of import_handler_info objects representing modules supporting import of this file type. + * + * @param array $registrydata the fully populated registry. + * @param remote_resource $resource the remote resource. + * @return import_handler_info[] the array of import_handler_info objects. + */ + public function get_handlers(array $registrydata, remote_resource $resource): array { + $handlers = []; + foreach ($registrydata['files'] as $index => $items) { + foreach ($items as $item) { + if ($index === $resource->get_extension() || $index === '*') { + $handlers[] = new import_handler_info($item['module'], $item['message'], $this); + } + } + } + return $handlers; + } + + /** + * Import the remote resource according to the rules of this strategy. + * + * @param remote_resource $resource the resource to import. + * @param \stdClass $user the user to import on behalf of. + * @param \stdClass $course the course into which the remote_resource is being imported. + * @param int $section the section into which the remote_resource is being imported. + * @return \stdClass the module data. + * @throws \moodle_exception if the file size means the upload limit is exceeded for the user. + */ + public function import(remote_resource $resource, \stdClass $user, \stdClass $course, int $section): \stdClass { + // Before starting a potentially lengthy download, try to ensure the file size does not exceed the upload size restrictions + // for the user. This is a time saving measure. + // This is a naive check, that serves only to catch files if they provide the content length header. + // Because of potential content encoding (compression), the stored file will be checked again after download as well. + $size = $resource->get_download_size() ?? -1; + $useruploadlimit = $this->get_user_upload_limit($user, $course); + if ($this->size_exceeds_upload_limit($size, $useruploadlimit)) { + throw new \moodle_exception('uploadlimitexceeded', 'tool_moodlenet', '', ['filesize' => $size, + 'uploadlimit' => $useruploadlimit]); + } + + // Download the file into a request directory and scan it. + [$filepath, $filename] = $resource->download_to_requestdir(); + avmanager::scan_file($filepath, $filename, true); + + // Check the final size of file against the user upload limits. + $localsize = filesize(sprintf('%s/%s', $filepath, $filename)); + if ($this->size_exceeds_upload_limit($localsize, $useruploadlimit)) { + throw new \moodle_exception('uploadlimitexceeded', 'tool_moodlenet', '', ['filesize' => $localsize, + 'uploadlimit' => $useruploadlimit]); + } + + // Store in the user draft file area. + $storedfile = $this->create_user_draft_stored_file($user, $filename, $filepath); + + // Prepare the data to be sent to the modules dndupload_handle hook. + return $this->prepare_module_data($course, $resource, $storedfile->get_itemid()); + } + + + /** + * Creates the data to pass to the dndupload_handle() hooks. + * + * @param \stdClass $course the course record. + * @param remote_resource $resource the resource being imported as a file. + * @param int $draftitemid the itemid of the draft file. + * @return \stdClass the data object. + */ + protected function prepare_module_data(\stdClass $course, remote_resource $resource, int $draftitemid): \stdClass { + $data = new \stdClass(); + $data->type = 'Files'; + $data->course = $course; + $data->draftitemid = $draftitemid; + $data->displayname = $resource->get_name(); + return $data; + } + + /** + * Get the max file size limit for the user in the course. + * + * @param \stdClass $user the user to check. + * @param \stdClass $course the course to check in. + * @return int the file size limit, in bytes. + */ + protected function get_user_upload_limit(\stdClass $user, \stdClass $course): int { + return get_user_max_upload_file_size(\context_course::instance($course->id), get_config('core', 'maxbytes'), + $course->maxbytes, 0, $user); + } + + /** + * Does the size exceed the upload limit for the current import, taking into account user and core settings. + * + * @param int $sizeinbytes the size, in bytes. + * @param int $useruploadlimit the upload limit, in bytes. + * @return bool true if exceeded, false otherwise. + * @throws \dml_exception + */ + protected function size_exceeds_upload_limit(int $sizeinbytes, int $useruploadlimit): bool { + if ($useruploadlimit != USER_CAN_IGNORE_FILE_SIZE_LIMITS && $sizeinbytes > $useruploadlimit) { + return true; + } + return false; + } + + /** + * Create a file in the user drafts ready for use by plugins implementing dndupload_handle(). + * + * @param \stdClass $user the user object. + * @param string $filename the name of the file on disk + * @param string $path the path where the file is stored on disk + * @return \stored_file + */ + protected function create_user_draft_stored_file(\stdClass $user, string $filename, string $path): \stored_file { + global $CFG; + + $record = new \stdClass(); + $record->filearea = 'draft'; + $record->component = 'user'; + $record->filepath = '/'; + $record->itemid = file_get_unused_draft_itemid(); + $record->license = $CFG->sitedefaultlicense; + $record->author = ''; + $record->filename = clean_param($filename, PARAM_FILE); + $record->contextid = \context_user::instance($user->id)->id; + $record->userid = $user->id; + + $fullpathwithname = sprintf('%s/%s', $path, $filename); + + $fs = get_file_storage(); + + return $fs->create_file_from_pathname($record, $fullpathwithname); + } +} diff --git a/admin/tool/moodlenet/classes/local/import_strategy_link.php b/admin/tool/moodlenet/classes/local/import_strategy_link.php new file mode 100644 index 00000000000..ca04f061d88 --- /dev/null +++ b/admin/tool/moodlenet/classes/local/import_strategy_link.php @@ -0,0 +1,71 @@ +. +/** + * Contains the import_strategy_link class. + * + * @package tool_moodlenet + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_moodlenet\local; + +/** + * The import_strategy_link class. + * + * The import_strategy_link objects contains the setup steps needed to prepare a resource for import as a URL into Moodle. + * + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class import_strategy_link implements import_strategy { + + /** + * Get an array of import_handler_info objects representing modules supporting import of the resource. + * + * @param array $registrydata the fully populated registry. + * @param remote_resource $resource the remote resource. + * @return import_handler_info[] the array of import_handler_info objects. + */ + public function get_handlers(array $registrydata, remote_resource $resource): array { + $handlers = []; + foreach ($registrydata['types'] as $identifier => $items) { + foreach ($items as $item) { + if ($identifier === 'url') { + $handlers[] = new import_handler_info($item['module'], $item['message'], $this); + } + } + } + return $handlers; + } + + /** + * Import the remote resource according to the rules of this strategy. + * + * @param remote_resource $resource the resource to import. + * @param \stdClass $user the user to import on behalf of. + * @param \stdClass $course the course into which the remote_resource is being imported. + * @param int $section the section into which the remote_resource is being imported. + * @return \stdClass the module data. + */ + public function import(remote_resource $resource, \stdClass $user, \stdClass $course, int $section): \stdClass { + $data = new \stdClass(); + $data->type = 'url'; + $data->course = $course; + $data->content = $resource->get_url()->get_value(); + $data->displayname = $resource->get_name(); + return $data; + } +} diff --git a/admin/tool/moodlenet/classes/local/remote_resource.php b/admin/tool/moodlenet/classes/local/remote_resource.php new file mode 100644 index 00000000000..e9526184b8c --- /dev/null +++ b/admin/tool/moodlenet/classes/local/remote_resource.php @@ -0,0 +1,169 @@ +. +/** + * Contains the remote_resource class definition. + * + * @package tool_moodlenet + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_moodlenet\local; + +/** + * The remote_resource class. + * + * Objects of type remote_resource provide a means of interacting with resources over HTTP. + * + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class remote_resource { + + /** @var \curl $curl the curl http helper.*/ + protected $curl; + + /** @var url $url the url to the remote resource.*/ + protected $url; + + /** @var string $filename the name of this remote file.*/ + protected $filename; + + /** @var string $extension the file extension of this remote file.*/ + protected $extension; + + /** @var array $headinfo the array of information for the most recent HEAD request.*/ + protected $headinfo = []; + + /** @var \stdClass $metadata information about the resource. */ + protected $metadata; + + /** + * The remote_resource constructor. + * + * @param \curl $curl a curl object for HTTP requests. + * @param url $url the URL of the remote resource. + * @param \stdClass $metadata resource metadata such as name, summary, license, etc. + */ + public function __construct(\curl $curl, url $url, \stdClass $metadata) { + $this->curl = $curl; + $this->url = $url; + $this->filename = pathinfo($this->url->get_path(), PATHINFO_FILENAME); + $this->extension = pathinfo($this->url->get_path(), PATHINFO_EXTENSION); + $this->metadata = $metadata; + } + + /** + * Return the URL for this remote resource. + * + * @return url the url object. + */ + public function get_url(): url { + return $this->url; + } + + /** + * Get the name of the file as taken from the metadata. + */ + public function get_name(): string { + return $this->metadata->name ?? ''; + } + + /** + * Get the resource metadata. + * + * @return \stdClass the metadata. + */ + public function get_metadata(): \stdClass { + return$this->metadata; + } + + /** + * Get the description of the resource as taken from the metadata. + * + * @return string + */ + public function get_description(): string { + return $this->metadata->description ?? ''; + } + + /** + * Return the extension of the file, if found. + * + * @return string the extension of the file, if found. + */ + public function get_extension(): string { + return $this->extension; + } + + /** + * Returns the file size of the remote file, in bytes, or null if it cannot be determined. + * + * @return int|null the content length, if able to be determined, otherwise null. + */ + public function get_download_size(): ?int { + $this->get_resource_info(); + return $this->headinfo['download_content_length'] ?? null; + } + + /** + * Download the remote resource to a local requestdir, returning the path and name of the resulting file. + * + * @return array an array containing filepath adn filename, e.g. [filepath, filename]. + * @throws \moodle_exception if the file cannot be downloaded. + */ + public function download_to_requestdir(): array { + $filename = sprintf('%s.%s', $this->filename, $this->get_extension()); + $path = make_request_directory(); + $fullpathwithname = sprintf('%s/%s', $path, $filename); + + // In future, use a timeout (download and/or connection) controlled by a tool_moodlenet setting. + $downloadtimeout = 30; + + $result = $this->curl->download_one($this->url->get_value(), null, ['filepath' => $fullpathwithname, + 'timeout' => $downloadtimeout]); + if ($result !== true) { + throw new \moodle_exception('errorduringdownload', 'tool_moodlenet', '', $result); + } + + return [$path, $filename]; + } + + /** + * Fetches information about the remote resource via a HEAD request. + * + * @throws \coding_exception if any connection problems occur. + */ + protected function get_resource_info() { + if (!empty($this->headinfo)) { + return; + } + $options['CURLOPT_RETURNTRANSFER'] = 1; + $options['CURLOPT_FOLLOWLOCATION'] = 1; + $options['CURLOPT_MAXREDIRS'] = 5; + $options['CURLOPT_FAILONERROR'] = 1; // We want to consider http error codes as errors to report, not just status codes. + + $this->curl->head($this->url->get_value(), $options); + $errorno = $this->curl->get_errno(); + $this->curl->resetopt(); + + if ($errorno !== 0) { + $message = 'Problem during HEAD request for remote resource \''.$this->url->get_value().'\'. Curl Errno: ' . $errorno; + throw new \coding_exception($message); + } + $this->headinfo = $this->curl->get_info(); + } + +} diff --git a/admin/tool/moodlenet/classes/local/url.php b/admin/tool/moodlenet/classes/local/url.php new file mode 100644 index 00000000000..233b770f56e --- /dev/null +++ b/admin/tool/moodlenet/classes/local/url.php @@ -0,0 +1,84 @@ +. +/** + * Contains the url class, providing a representation of a url and operations on its component parts. + * + * @package tool_moodlenet + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_moodlenet\local; + +/** + * The url class, providing a representation of a url and operations on its component parts. + * + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class url { + + /** @var string $url the full URL string.*/ + protected $url; + + /** @var string|null $path the path component of this URL.*/ + protected $path; + + /** @var host|null $host the host component of this URL.*/ + protected $host; + + /** + * The url constructor. + * + * @param string $url the URL string. + * @throws \coding_exception if the URL does not pass syntax validation. + */ + public function __construct(string $url) { + // This object supports URLs as per the spec, so non-ascii chars must be encoded as per IDNA rules. + if (!filter_var($url, FILTER_VALIDATE_URL)) { + throw new \coding_exception('Malformed URL'); + } + $this->url = $url; + $this->path = parse_url($url, PHP_URL_PATH); + $this->host = parse_url($url, PHP_URL_HOST); + } + + /** + * Get the path component of the URL. + * + * @return string|null the path component of the URL. + */ + public function get_path(): ?string { + return $this->path; + } + + /** + * Return the domain component of the URL. + * + * @return string|null the domain component of the URL. + */ + public function get_host(): ?string { + return $this->host; + } + + /** + * Return the full URL string. + * + * @return string the full URL string. + */ + public function get_value() { + return $this->url; + } +} diff --git a/admin/tool/moodlenet/classes/moodlenet_user_profile.php b/admin/tool/moodlenet/classes/moodlenet_user_profile.php new file mode 100644 index 00000000000..51e33f96b80 --- /dev/null +++ b/admin/tool/moodlenet/classes/moodlenet_user_profile.php @@ -0,0 +1,107 @@ +. + +/** + * Moodle net user profile class. + * + * @package tool_moodlenet + * @copyright 2020 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_moodlenet; + +/** + * A class to represent the moodlenet profile. + * + * @package tool_moodlenet + * @copyright 2020 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class moodlenet_user_profile { + + /** @var string $profile The full profile name. */ + protected $profile; + + /** @var int $userid The user ID that this profile belongs to. */ + protected $userid; + + /** @var string $username The username from $userprofile */ + protected $username; + + /** @var string $domain The domain from $domain */ + protected $domain; + + /** + * Constructor method. + * + * @param string $userprofile The moodle net user profile string. + * @param int $userid The user ID that this profile belongs to. + */ + public function __construct(string $userprofile, int $userid) { + $this->profile = $userprofile; + $this->userid = $userid; + + $explodedprofile = explode('@', $this->profile); + if (count($explodedprofile) === 2) { + // It'll either be an email or WebFinger entry. + $this->username = $explodedprofile[0]; + $this->domain = $explodedprofile[1]; + } else if (count($explodedprofile) === 3) { + // We may have a profile link as MoodleNet gives to the user. + $this->username = $explodedprofile[1]; + $this->domain = $explodedprofile[2]; + } else { + throw new \moodle_exception('invalidmoodlenetprofile', 'tool_moodlenet'); + } + } + + /** + * Get the full moodle net profile. + * + * @return string The moodle net profile. + */ + public function get_profile_name(): string { + return $this->profile; + } + + /** + * Get the user ID that this profile belongs to. + * + * @return int The user ID. + */ + public function get_userid(): int { + return $this->userid; + } + + /** + * Get the username for this profile. + * + * @return string The username. + */ + public function get_username(): string { + return $this->username; + } + + /** + * Get the domain for this profile. + * + * @return string The domain. + */ + public function get_domain(): string { + return $this->domain; + } +} diff --git a/admin/tool/moodlenet/classes/output/renderer.php b/admin/tool/moodlenet/classes/output/renderer.php new file mode 100644 index 00000000000..98de3e4ccbf --- /dev/null +++ b/admin/tool/moodlenet/classes/output/renderer.php @@ -0,0 +1,52 @@ +. + +/** + * Renderer. + * + * @package tool_moodlenet + * @copyright 2020 Mathew May {@link https://mathew.solutions} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_moodlenet\output; + +defined('MOODLE_INTERNAL') || die(); + +use plugin_renderer_base; + +/** + * Renderer class. + * + * @package tool_moodlenet + * @copyright 2020 Mathew May {@link https://mathew.solutions} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends plugin_renderer_base { + + /** + * Defer to template. + * + * @param select_page $selectpage + * @return string HTML + */ + protected function render_select_page(select_page $selectpage): string { + + $this->page->requires->js_call_amd('tool_moodlenet/select_page', 'init', [$selectpage->get_import_info()->get_id()]); + $data = $selectpage->export_for_template($this); + return parent::render_from_template('tool_moodlenet/select_page', $data); + } +} diff --git a/admin/tool/moodlenet/classes/output/select_page.php b/admin/tool/moodlenet/classes/output/select_page.php new file mode 100644 index 00000000000..60bbde16572 --- /dev/null +++ b/admin/tool/moodlenet/classes/output/select_page.php @@ -0,0 +1,76 @@ +. + +/** + * Select page renderable. + * + * @package tool_moodlenet + * @copyright 2020 Mathew May {@link https://mathew.solutions} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_moodlenet\output; + +defined('MOODLE_INTERNAL') || die; + +use tool_moodlenet\local\import_info; + +/** + * Select page renderable. + * + * @package tool_moodlenet + * @copyright 2020 Mathew May {@link https://mathew.solutions} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class select_page implements \renderable, \templatable { + + /** @var import_info $importinfo resource and config information pertaining to an import. */ + protected $importinfo; + + /** + * Inits the Select page renderable. + * + * @param import_info $importinfo resource and config information pertaining to an import. + */ + public function __construct(import_info $importinfo) { + $this->importinfo = $importinfo; + } + + /** + * Return the import info. + * + * @return import_info the import information. + */ + public function get_import_info(): import_info { + return $this->importinfo; + } + + /** + * Export the data. + * + * @param \renderer_base $output + * @return \stdClass + */ + public function export_for_template(\renderer_base $output): \stdClass { + + // Prepare the context object. + return (object) [ + 'name' => $this->importinfo->get_resource()->get_name(), + 'type' => $this->importinfo->get_config()->type, + 'cancellink' => new \moodle_url('/my'), + ]; + } +} diff --git a/admin/tool/moodlenet/classes/privacy/provider.php b/admin/tool/moodlenet/classes/privacy/provider.php new file mode 100644 index 00000000000..a076f4bc744 --- /dev/null +++ b/admin/tool/moodlenet/classes/privacy/provider.php @@ -0,0 +1,44 @@ +. +/** + * Privacy class for tool_moodlenet. + * + * @package tool_moodlenet + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_moodlenet\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy class for tool_moodlenet. + * + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} diff --git a/admin/tool/moodlenet/classes/profile_manager.php b/admin/tool/moodlenet/classes/profile_manager.php new file mode 100644 index 00000000000..f1a922adac6 --- /dev/null +++ b/admin/tool/moodlenet/classes/profile_manager.php @@ -0,0 +1,350 @@ +. + +/** + * Profile manager class + * + * @package tool_moodlenet + * @copyright 2020 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_moodlenet; + +/** + * Class for handling interaction with the moodlenet profile. + * + * @package tool_moodlenet + * @copyright 2020 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class profile_manager { + + /** + * Get the mnet profile for a user. + * + * @param int $userid The ID for the user to get the profile form + * @return moodlenet_user_profile or null. + */ + public static function get_moodlenet_user_profile(int $userid): ?moodlenet_user_profile { + global $CFG; + // Check for official profile. + if (self::official_profile_exists()) { + $user = \core_user::get_user($userid, 'moodlenetprofile'); + try { + $userprofile = $user->moodlenetprofile ? $user->moodlenetprofile : ''; + return (isset($user)) ? new moodlenet_user_profile($userprofile, $userid) : null; + } catch (\moodle_exception $e) { + // If an exception is thrown, means there isn't a valid profile set. No need to log exception. + return null; + } + } + // Otherwise get hacked in user profile field. + require_once($CFG->dirroot . '/user/profile/lib.php'); + $profilefields = profile_get_user_fields_with_data($userid); + foreach ($profilefields as $key => $field) { + if ($field->get_category_name() == self::get_category_name() + && $field->inputname == 'profile_field_mnetprofile') { + try { + return new moodlenet_user_profile($field->display_data(), $userid); + } catch (\moodle_exception $e) { + // If an exception is thrown, means there isn't a valid profile set. No need to log exception. + return null; + } + } + } + return null; + } + + /** + * Save the moodlenet profile. + * + * @param moodlenet_user_profile $moodlenetprofile The moodlenet profile to save. + */ + public static function save_moodlenet_user_profile(moodlenet_user_profile $moodlenetprofile): void { + global $CFG, $DB; + // Do some cursory checks first to see if saving is possible. + if (self::official_profile_exists()) { + // All good. Let's save. + $user = \core_user::get_user($moodlenetprofile->get_userid()); + $user->moodlenetprofile = $moodlenetprofile->get_profile_name(); + + require_once($CFG->dirroot . '/user/lib.php'); + + \user_update_user($user, false, true); + return; + } + $fielddata = self::get_user_profile_field(); + $fielddata = self::validate_and_fix_missing_profile_items($fielddata); + // Everything should be back to normal. Let's save. + require_once($CFG->dirroot . '/user/profile/lib.php'); + \profile_save_custom_fields($moodlenetprofile->get_userid(), + [$fielddata->shortname => $moodlenetprofile->get_profile_name()]); + } + + /** + * Checks to see if the required user profile fields and categories are in place. If not it regenerates them. + * + * @param stdClass $fielddata The moodlenet profile field. + * @return stdClass The same moodlenet profile field, with any necessary updates made. + */ + private static function validate_and_fix_missing_profile_items(\stdClass $fielddata): \stdClass { + global $DB; + + if (empty((array) $fielddata)) { + // We need to regenerate the category and field to store this data. + if (!self::check_profile_category()) { + $categoryid = self::create_user_profile_category(); + self::create_user_profile_text_field($categoryid); + } else { + // We need the category id. + $category = $DB->get_record('user_info_category', ['name' => self::get_category_name()]); + self::create_user_profile_text_field($category->id); + } + $fielddata = self::get_user_profile_field(); + } else { + if (!self::check_profile_category($fielddata->categoryid)) { + $categoryid = self::create_user_profile_category(); + // Update the field to put it back into this category. + $fielddata->categoryid = $categoryid; + $DB->update_record('user_info_field', $fielddata); + } + } + return $fielddata; + } + + /** + * Returns the user profile field table object. + * + * @return stdClass the moodlenet profile table object. False if no record found. + */ + private static function get_user_profile_field(): \stdClass { + global $DB; + $fieldname = self::get_profile_field_name(); + $record = $DB->get_record('user_info_field', ['shortname' => $fieldname]); + return ($record) ? $record : (object) []; + } + + /** + * This reports back if the category has been deleted or the config value is different. + * + * @param int $categoryid The category id to check against. + * @return bool True is the category checks out, otherwise false. + */ + private static function check_profile_category(int $categoryid = null): bool { + global $DB; + $categoryname = self::get_category_name(); + $categorydata = $DB->get_record('user_info_category', ['name' => $categoryname]); + if (empty($categorydata)) { + return false; + } + if (isset($categoryid) && $categorydata->id != $categoryid) { + return false; + } + return true; + } + + /** + * Are we using the proper user profile field to hold the mnet profile? + * + * @return bool True if we are using a user table field for the mnet profile. False means we are using costom profile fields. + */ + public static function official_profile_exists(): bool { + global $DB; + + $usertablecolumns = $DB->get_columns('user', false); + if (isset($usertablecolumns['moodlenetprofile'])) { + return true; + } + return false; + } + + /** + * Gets the category name that is set for this site. + * + * @return string The category used to hold the moodle net profile field. + */ + public static function get_category_name(): string { + return get_config('tool_moodlenet', 'profile_category'); + } + + /** + * Sets the a unique category to hold the moodle net user profile. + * + * @param string $categoryname The base category name to use. + * @return string The actual name of the category to use. + */ + private static function set_category_name(string $categoryname): string { + global $DB; + + $attemptname = $categoryname; + + // Check if this category already exists. + $foundcategoryname = false; + $i = 0; + do { + $category = $DB->count_records('user_info_category', ['name' => $attemptname]); + if ($category > 0) { + $i++; + $attemptname = $categoryname . $i; + } else { + set_config('profile_category', $attemptname, 'tool_moodlenet'); + $foundcategoryname = true; + } + } while (!$foundcategoryname); + return $attemptname; + } + + /** + * Create a custom user profile category to hold our custom field. + * + * @return int The id of the created category. + */ + public static function create_user_profile_category(): int { + global $DB; + // No nice API to do this, so direct DB calls it is. + $data = new \stdClass(); + $data->sortorder = $DB->count_records('user_info_category') + 1; + $data->name = self::set_category_name(get_string('pluginname', 'tool_moodlenet')); + $data->id = $DB->insert_record('user_info_category', $data, true); + + $createdcategory = $DB->get_record('user_info_category', array('id' => $data->id)); + \core\event\user_info_category_created::create_from_category($createdcategory)->trigger(); + return $createdcategory->id; + } + + /** + * Sets a unique name to be used for the moodle net profile. + * + * @param string $fieldname The base fieldname to use. + * @return string The actual profile field name. + */ + private static function set_profile_field_name(string $fieldname): string { + global $DB; + + $attemptname = $fieldname; + + // Check if this profilefield already exists. + $foundfieldname = false; + $i = 0; + do { + $profilefield = $DB->count_records('user_info_field', ['shortname' => $attemptname]); + if ($profilefield > 0) { + $i++; + $attemptname = $fieldname . $i; + } else { + set_config('profile_field_name', $attemptname, 'tool_moodlenet'); + $foundfieldname = true; + } + } while (!$foundfieldname); + return $attemptname; + } + + /** + * Gets the unique profile field used to hold the moodle net profile. + * + * @return string The profile field name being used on this site. + */ + public static function get_profile_field_name(): string { + return get_config('tool_moodlenet', 'profile_field_name'); + } + + + /** + * Create a user profile field to hold the moodlenet profile information. + * + * @param int $categoryid The category to put this field into. + */ + public static function create_user_profile_text_field(int $categoryid): void { + global $CFG; + + require_once($CFG->dirroot . '/user/profile/definelib.php'); + require_once($CFG->dirroot . '/user/profile/field/text/define.class.php'); + + // Add our moodlenet profile field. + $profileclass = new \profile_define_text(); + $data = (object) [ + 'shortname' => self::set_profile_field_name('mnetprofile'), + 'name' => get_string('mnetprofile', 'tool_moodlenet'), + 'datatype' => 'text', + 'description' => get_string('mnetprofiledesc', 'tool_moodlenet'), + 'descriptionformat' => 1, + 'categoryid' => $categoryid, + 'signup' => 1, + 'forceunique' => 1, + 'visible' => 2, + 'param1' => 30, + 'param2' => 2048 + ]; + $profileclass->define_save($data); + } + + /** + * Given our $moodlenetprofile let's cURL the domains' WebFinger endpoint + * + * @param moodlenet_user_profile $moodlenetprofile The moodlenet profile to get info from. + * @return array [bool, text, raw] + */ + public static function get_moodlenet_profile_link(moodlenet_user_profile $moodlenetprofile): array { + $domain = $moodlenetprofile->get_domain(); + $username = $moodlenetprofile->get_username(); + + // Assumption: All MoodleNet instance's will contain a WebFinger validation script. + $url = "https://".$domain."/.well-known/webfinger?resource=acct:".$username."@".$domain; + + $curl = new \curl(); + $options = [ + 'CURLOPT_HEADER' => 0, + ]; + $content = $curl->get($url, null, $options); + $errno = $curl->get_errno(); + $info = $curl->get_info(); + + // The base cURL seems fine, let's press on. + if (!$errno) { + // WebFinger gave us a 404 back so the user has no droids here. + if ($info['http_code'] >= 400) { + if ($info['http_code'] === 404) { + // User not found. + return [ + 'result' => false, + 'message' => get_string('profilevalidationfail', 'tool_moodlenet'), + ]; + } else { + // There was some other error that was not a missing account. + return [ + 'result' => false, + 'message' => get_string('profilevalidationerror', 'tool_moodlenet'), + ]; + } + } + + // We must have a valid link so give it back to the user. + $data = json_decode($content); + return [ + 'result' => true, + 'message' => get_string('profilevalidationpass', 'tool_moodlenet'), + 'domain' => $data->aliases[0] + ]; + } else { + // There was some failure in curl so report it back. + return [ + 'result' => false, + 'message' => get_string('profilevalidationerror', 'tool_moodlenet'), + ]; + } + } +} diff --git a/admin/tool/moodlenet/db/services.php b/admin/tool/moodlenet/db/services.php new file mode 100644 index 00000000000..a5b452a0af0 --- /dev/null +++ b/admin/tool/moodlenet/db/services.php @@ -0,0 +1,44 @@ +. + +/** + * Tool Moodle.Net webservice definitions. + * + * @package tool_moodlenet + * @copyright 2020 Mathew May {@link https://mathew.solutions} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$functions = [ + 'tool_moodlenet_verify_webfinger' => [ + 'classname' => 'tool_moodlenet\external', + 'methodname' => 'verify_webfinger', + 'description' => 'Verify if the passed information resolves into a WebFinger profile URL', + 'type' => 'read', + 'ajax' => true, + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE] + ], + 'tool_moodlenet_search_courses' => [ + 'classname' => 'tool_moodlenet\external', + 'methodname' => 'search_courses', + 'description' => 'For some given input search for a course that matches', + 'type' => 'read', + 'ajax' => true, + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE] + ], +]; diff --git a/admin/tool/moodlenet/db/upgrade.php b/admin/tool/moodlenet/db/upgrade.php new file mode 100644 index 00000000000..24f6beb74c2 --- /dev/null +++ b/admin/tool/moodlenet/db/upgrade.php @@ -0,0 +1,81 @@ +. + +/** + * Upgrade script for tool_moodlenet. + * + * @package tool_moodlenet + * @copyright 2020 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Upgrade the plugin. + * + * @param int $oldversion + * @return bool always true + */ +function xmldb_tool_moodlenet_upgrade(int $oldversion) { + global $CFG, $DB; + if ($oldversion < 2020060500) { + + // Grab some of the old settings. + $categoryname = get_config('tool_moodlenet', 'profile_category'); + $profilefield = get_config('tool_moodlenet', 'profile_field_name'); + + // Master version only! + + // Find out if we have a custom profile field for moodle.net. + $sql = "SELECT f.* + FROM {user_info_field} f + JOIN {user_info_category} c ON c.id = f.categoryid and c.name = :categoryname + WHERE f.shortname = :name"; + + $params = [ + 'categoryname' => $categoryname, + 'name' => $profilefield + ]; + + $record = $DB->get_record_sql($sql, $params); + + if (!empty($record)) { + $userentries = $DB->get_recordset('user_info_data', ['fieldid' => $record->id]); + $recordstodelete = []; + foreach ($userentries as $userentry) { + $data = (object) [ + 'id' => $userentry->userid, + 'moodlenetprofile' => $userentry->data + ]; + $DB->update_record('user', $data, true); + $recordstodelete[] = $userentry->id; + } + $userentries->close(); + + // Remove the user profile data, fields, and category. + $DB->delete_records_list('user_info_data', 'id', $recordstodelete); + $DB->delete_records('user_info_field', ['id' => $record->id]); + $DB->delete_records('user_info_category', ['name' => $categoryname]); + unset_config('profile_field_name', 'tool_moodlenet'); + unset_config('profile_category', 'tool_moodlenet'); + } + + upgrade_plugin_savepoint(true, 2020060500, 'tool', 'moodlenet'); + } + + return true; +} diff --git a/admin/tool/moodlenet/import.php b/admin/tool/moodlenet/import.php new file mode 100644 index 00000000000..1b57ce9d6de --- /dev/null +++ b/admin/tool/moodlenet/import.php @@ -0,0 +1,85 @@ +. + +/** + * This is the main endpoint which MoodleNet instances POST to. + * + * MoodleNet instances send the user agent to this endpoint via a form POST. + * Then: + * 1. The POSTed resource information is put in a session store for cross-request access. + * 2. This page makes a GET request for admin/tool/moodlenet/index.php (the import confirmation page). + * 3. Then, depending on whether the user is authenticated, the user will either: + * - If not authenticated, they will be asked to login, after which they will see the confirmation page (leveraging $wantsurl). + * - If authenticated, they will see the confirmation page immediately. + * + * @package tool_moodlenet + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use tool_moodlenet\local\import_info; +use tool_moodlenet\local\remote_resource; +use tool_moodlenet\local\url; + +require_once(__DIR__ . '/../../../config.php'); + +// The integration must be enabled for this import endpoint to be active. +if (!get_config('tool_moodlenet', 'enablemoodlenet')) { + print_error('moodlenetnotenabled', 'tool_moodlenet'); +} + +$resourceurl = required_param('resourceurl', PARAM_URL); +$resourceinfo = required_param('resource_info', PARAM_RAW); +$resourceinfo = json_decode($resourceinfo); +$type = optional_param('type', 'link', PARAM_TEXT); +$course = optional_param('course', 0, PARAM_INT); +$section = optional_param('section', 0, PARAM_INT); +// If course isn't provided, course and section are null. +if (empty($course)) { + $course = null; + $section = null; +} +$name = validate_param($resourceinfo->name, PARAM_TEXT); +$description = validate_param($resourceinfo->summary, PARAM_TEXT); + +// Only accept POSTs. +if (!empty($_POST)) { + // Store information about the import of the resource for the current user. + $importconfig = (object) [ + 'course' => $course, + 'section' => $section, + 'type' => $type, + ]; + $metadata = (object) [ + 'name' => $name, + 'description' => $description ?? '' + ]; + + require_once($CFG->libdir . '/filelib.php'); + $importinfo = new import_info( + $USER->id, + new remote_resource(new \curl(), new url($resourceurl), $metadata), + $importconfig + ); + $importinfo->save(); + + // Redirect to the import confirmation page, detouring via the log in page if required. + redirect(new moodle_url('/admin/tool/moodlenet/index.php', ['id' => $importinfo->get_id()])); + +} + +// Invalid or missing POST data. Show an error to the user. +print_error('missinginvalidpostdata', 'tool_moodlenet'); diff --git a/admin/tool/moodlenet/index.php b/admin/tool/moodlenet/index.php new file mode 100644 index 00000000000..5680d19a8dc --- /dev/null +++ b/admin/tool/moodlenet/index.php @@ -0,0 +1,136 @@ +. + +/** + * Landing page for all imports from MoodleNet. + * + * This page asks the user to confirm the import process, and takes them to the relevant next step. + * + * @package tool_moodlenet + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use tool_moodlenet\local\import_info; +use tool_moodlenet\local\import_backup_helper; + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->dirroot .'/course/lib.php'); + +$cancel = optional_param('cancel', null, PARAM_TEXT); +$continue = optional_param('continue', null, PARAM_TEXT); +$id = required_param('id', PARAM_ALPHANUM); + +if (is_null($importinfo = import_info::load($id))) { + throw new moodle_exception('missinginvalidpostdata', 'tool_moodlenet'); +} + +// Access control. +require_login($importinfo->get_config()->course, false); // Course may be null here - that's ok. +if ($importinfo->get_config()->course) { + require_capability('moodle/course:manageactivities', context_course::instance($importinfo->get_config()->course)); +} +if (!get_config('tool_moodlenet', 'enablemoodlenet')) { + print_error('moodlenetnotenabled', 'tool_moodlenet'); +} + +// Handle the form submits. +// This page POSTs to self to verify the sesskey for the confirm action. +// The next page will either be: +// - 1. The restore process for a course or module, if the file is an mbz file. +// - 2. The 'select a course' tool page, if course and section are not provided. +// - 3. The 'select what to do with the content' tool page, provided course and section are present. +// - 4. The dashboard, if the user decides to cancel and course or section is not found. +// - 5. The course home, if the user decides to cancel but the course and section are found. +if ($cancel) { + if (!empty($importinfo->get_config()->course)) { + $url = new \moodle_url('/course/view.php', ['id' => $importinfo->get_config()->course]); + } else { + $url = new \moodle_url('/'); + } + redirect($url); +} else if ($continue) { + confirm_sesskey(); + + // Handle backups. + if (strtolower($importinfo->get_resource()->get_extension()) == 'mbz') { + if (empty($importinfo->get_config()->course)) { + // Find a course that the user has permission to upload a backup file. + // This is likely to be very slow on larger sites. + $context = import_backup_helper::get_context_for_user($USER->id); + + if (is_null($context)) { + print_error('nopermissions', 'error', '', get_string('restore:uploadfile', 'core_role')); + } + } else { + $context = context_course::instance($importinfo->get_config()->course); + } + + $importbackuphelper = new import_backup_helper($importinfo->get_resource(), $USER, $context); + $storedfile = $importbackuphelper->get_stored_file(); + + $url = new \moodle_url('/backup/restorefile.php', [ + 'component' => $storedfile->get_component(), + 'filearea' => $storedfile->get_filearea(), + 'itemid' => $storedfile->get_itemid(), + 'filepath' => $storedfile->get_filepath(), + 'filename' => $storedfile->get_filename(), + 'filecontextid' => $storedfile->get_contextid(), + 'contextid' => $context->id, + 'action' => 'choosebackupfile' + ]); + redirect($url); + } + + // Handle adding files to a course. + // Course and section data present and confirmed. Redirect to the option select view. + if (!is_null($importinfo->get_config()->course) && !is_null($importinfo->get_config()->section)) { + redirect(new \moodle_url('/admin/tool/moodlenet/options.php', ['id' => $id])); + } + + if (is_null($importinfo->get_config()->course)) { + redirect(new \moodle_url('/admin/tool/moodlenet/select.php', ['id' => $id])); + } +} + +// Display the page. +$PAGE->set_context(context_system::instance()); +$PAGE->set_pagelayout('base'); +$PAGE->set_title(get_string('addingaresource', 'tool_moodlenet')); +$PAGE->set_heading(get_string('addingaresource', 'tool_moodlenet')); +$url = new moodle_url('/admin/tool/moodlenet/index.php'); +$PAGE->set_url($url); +$renderer = $PAGE->get_renderer('core'); + +// Relevant confirmation form. +$context = $context = [ + 'resourceurl' => $importinfo->get_resource()->get_url()->get_value(), + 'resourcename' => $importinfo->get_resource()->get_name(), + 'resourcetype' => $importinfo->get_config()->type, + 'sesskey' => sesskey() +]; +if (!is_null($importinfo->get_config()->course) && !is_null($importinfo->get_config()->section)) { + $course = get_course($importinfo->get_config()->course); + $context = array_merge($context, [ + 'course' => $course->id, + 'coursename' => $course->shortname, + 'section' => $importinfo->get_config()->section + ]); +} + +echo $OUTPUT->header(); +echo $renderer->render_from_template('tool_moodlenet/import_confirmation', $context); +echo $OUTPUT->footer(); diff --git a/admin/tool/moodlenet/lang/en/tool_moodlenet.php b/admin/tool/moodlenet/lang/en/tool_moodlenet.php new file mode 100644 index 00000000000..1ba4e5b6fb2 --- /dev/null +++ b/admin/tool/moodlenet/lang/en/tool_moodlenet.php @@ -0,0 +1,69 @@ +. + +/** + * Strings for the tool_moodlenet component. + * + * @package tool_moodlenet + * @category string + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$string['addingaresource'] = 'Adding content from MoodleNet'; +$string['aria:enterprofile'] = "Enter your MoodleNet profile URL"; +$string['aria:footermessage'] = "Browse for content on MoodleNet"; +$string['browsecontentmoodlenet'] = "Or browse for content on MoodleNet"; +$string['clearsearch'] = "Clear search"; +$string['connectandbrowse'] = "Connect to and browse:"; +$string['defaultmoodlenet'] = "Default MoodleNet URL"; +$string['defaultmoodlenet_desc'] = "The URL to either Moodle HQ's MoodleNet instance, or your preferred instance."; +$string['defaultmoodlenetname'] = "MoodleNet instance name"; +$string['defaultmoodlenetname_desc'] = 'The name of either Moodle HQ\'s MoodleNet instance or your preferred MoodleNet instance to browse on.'; +$string['enablemoodlenet'] = 'Enable MoodleNet integration'; +$string['enablemoodlenet_desc'] = 'Enabling the integration allows users with the \'xx\' capability to browse MoodleNet from the +activity chooser and import MoodleNet resources into their course. It also allows users to push backups from MoodleNet into Moodle. +'; +$string['errorduringdownload'] = 'An error occurred while downloading the file: {$a}'; +$string['forminfo'] = "It will be automatically saved on your moodle profile."; +$string['footermessage'] = "Or browse for content on"; +$string['instancedescription'] = "MoodleNet is an open social media platform for educators, with a focus on the collaborative curation of collections of open resources. "; +$string['instanceplaceholder'] = '@yourprofile@moodle.net'; +$string['inputhelp'] = 'Or if you have a MoodleNet account already, enter your MoodleNet profile:'; +$string['invalidmoodlenetprofile'] = '$userprofile is not correctly formatted'; +$string['importconfirm'] = 'You are about to import the content "{$a->resourcename} ({$a->resourcetype})" into the course "{$a->coursename}". Are you sure you want to continue?'; +$string['importconfirmnocourse'] = 'You are about to import the content "{$a->resourcename} ({$a->resourcetype})" into your site. Are you sure you want to continue?'; +$string['importformatselectguidingtext'] = 'In which format would you like this content "{$a->name} ({$a->type})" to be added to your course?'; +$string['importformatselectheader'] = 'Choose the content display format'; +$string['missinginvalidpostdata'] = 'The resource information from MoodleNet is either missing, or is in an incorrect format. +If this happens repeatedly, please contact the site administrator.'; +$string['mnetprofile'] = 'MoodleNet profile'; +$string['mnetprofiledesc'] = '

Enter in your MoodleNet profile details here to be redirected to your profile while visiting MoodleNet.

'; +$string['moodlenetsettings'] = 'MoodleNet settings'; +$string['moodlenetnotenabled'] = 'The MoodleNet integration must be enabled before resource imports can be processed. +To enable this feature, see the \'enablemoodlenet\' setting.'; +$string['notification'] = 'You are about to import the content "{$a->name} ({$a->type})" into your site. Select the course in which it should be added, or cancel.'; +$string['searchcourses'] = "Search courses"; +$string['selectpagetitle'] = 'Select page'; +$string['pluginname'] = 'MoodleNet'; +$string['privacy:metadata'] = "The MoodleNet tool only facilitates communication with MoodleNet. It stores no data."; +$string['profilevalidationerror'] = 'There was a problem trying to validate your profile'; +$string['profilevalidationfail'] = 'Please enter a valid MoodleNet profile'; +$string['profilevalidationpass'] = 'Looks good!'; +$string['saveandgo'] = "Save and go"; +$string['uploadlimitexceeded'] = 'The file size {$a->filesize} exceeds the user upload limit of {$a->uploadlimit} bytes.'; diff --git a/admin/tool/moodlenet/lib.php b/admin/tool/moodlenet/lib.php new file mode 100644 index 00000000000..5affcd9de19 --- /dev/null +++ b/admin/tool/moodlenet/lib.php @@ -0,0 +1,104 @@ +. + +/** + * This page lists public api for tool_moodlenet plugin. + * + * @package tool_moodlenet + * @copyright 2020 Peter Dias + * @license http://www.gnu.org/copyleft/gpl.html GNU + */ + +defined('MOODLE_INTERNAL') || die; + +use \core_course\local\entity\activity_chooser_footer; + +/** + * The default endpoint to MoodleNet. + */ +define('MOODLENET_DEFAULT_ENDPOINT', "lms/moodle/search"); + +/** + * Generate the endpoint url to the user's moodlenet site. + * + * @param string $profileurl The user's moodlenet profile page + * @param int $course The moodle course the mnet resource will be added to + * @param int $section The section of the course will be added to. Defaults to the 0th element. + * @return string the resulting endpoint + * @throws moodle_exception + */ +function generate_mnet_endpoint(string $profileurl, int $course, int $section = 0) { + global $CFG; + $urlportions = explode('@', $profileurl); + $domain = end($urlportions); + $parsedurl = parse_url($domain); + $params = [ + 'site' => $CFG->wwwroot, + 'course' => $course, + 'section' => $section + ]; + $endpoint = new moodle_url(MOODLENET_DEFAULT_ENDPOINT, $params); + return (isset($parsedurl['scheme']) ? $domain : "https://$domain")."/{$endpoint->out(false)}"; +} + +/** + * Hooking function to build up the initial Activity Chooser footer information for MoodleNet + * + * @param int $courseid The course the user is currently in and wants to add resources to + * @param int $sectionid The section the user is currently in and wants to add resources to + * @return activity_chooser_footer + * @throws dml_exception + * @throws moodle_exception + */ +function tool_moodlenet_custom_chooser_footer(int $courseid, int $sectionid): activity_chooser_footer { + global $CFG, $USER, $OUTPUT; + $defaultlink = get_config('tool_moodlenet', 'defaultmoodlenet'); + $enabled = get_config('tool_moodlenet', 'enablemoodlenet'); + + $advanced = false; + // We are in the MoodleNet lib. It is safe assume we have our own functions here. + $mnetprofile = \tool_moodlenet\profile_manager::get_moodlenet_user_profile($USER->id); + if ($mnetprofile !== null) { + $advanced = $mnetprofile->get_domain() ?? false; + } + + $defaultlink = generate_mnet_endpoint($defaultlink, $courseid, $sectionid); + if ($advanced !== false) { + $advanced = generate_mnet_endpoint($advanced, $courseid, $sectionid); + } + + $renderedfooter = $OUTPUT->render_from_template('tool_moodlenet/chooser_footer', (object)[ + 'enabled' => (bool)$enabled, + 'generic' => $defaultlink, + 'advanced' => $advanced, + 'courseID' => $courseid, + 'sectionID' => $sectionid, + 'img' => $OUTPUT->image_url('MoodleNet', 'tool_moodlenet')->out(false), + ]); + + $renderedcarousel = $OUTPUT->render_from_template('tool_moodlenet/chooser_moodlenet', (object)[ + 'buttonName' => get_config('tool_moodlenet', 'defaultmoodlenetname'), + 'generic' => $defaultlink, + 'courseID' => $courseid, + 'sectionID' => $sectionid, + 'img' => $OUTPUT->image_url('MoodleNet', 'tool_moodlenet')->out(false), + ]); + return new activity_chooser_footer( + 'tool_moodlenet/instance_form', + $renderedfooter, + $renderedcarousel + ); +} diff --git a/admin/tool/moodlenet/options.php b/admin/tool/moodlenet/options.php new file mode 100644 index 00000000000..a510f53e206 --- /dev/null +++ b/admin/tool/moodlenet/options.php @@ -0,0 +1,128 @@ +. + +/** + * Page to select WHAT to do with a given resource stored on MoodleNet. + * + * This collates and presents the same options as a user would see for a drag and drop upload. + * That is, it leverages the dndupload_register() hooks and delegates the resource handling to the dndupload_handle hooks. + * + * This page requires a course, section an resourceurl to be provided via import_info. + * + * @package tool_moodlenet + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +use tool_moodlenet\local\import_handler_registry; +use tool_moodlenet\local\import_processor; +use tool_moodlenet\local\import_info; +use tool_moodlenet\local\import_strategy_file; +use tool_moodlenet\local\import_strategy_link; + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->dirroot . '/course/lib.php'); + +$module = optional_param('module', null, PARAM_PLUGIN); +$import = optional_param('import', null, PARAM_ALPHA); +$cancel = optional_param('cancel', null, PARAM_ALPHA); +$id = required_param('id', PARAM_ALPHANUM); + +if (is_null($importinfo = import_info::load($id))) { + throw new moodle_exception('missinginvalidpostdata', 'tool_moodlenet'); +} + +// Resolve course and section params. +// If course is not already set in the importinfo, we require it in the URL params. +$config = $importinfo->get_config(); +if (!isset($config->course)) { + $course = required_param('course', PARAM_INT); + $config->course = $course; + $config->section = 0; + $importinfo->set_config($config); + $importinfo->save(); +} + +// Access control. +require_login($config->course, false); +require_capability('moodle/course:manageactivities', context_course::instance($config->course)); +if (!get_config('tool_moodlenet', 'enablemoodlenet')) { + print_error('moodlenetnotenabled', 'tool_moodlenet'); +} + +// If the user cancelled, break early. +if ($cancel) { + redirect(new moodle_url('/course/view.php', ['id' => $config->course])); +} + +// Set up required objects. +$course = get_course($config->course); +$handlerregistry = new import_handler_registry($course, $USER); +switch ($config->type) { + case 'file': + $strategy = new import_strategy_file(); + break; + case 'link': + default: + $strategy = new import_strategy_link(); + break; +} + +if ($import && $module) { + confirm_sesskey(); + + $handlerinfo = $handlerregistry->get_resource_handler_for_mod_and_strategy($importinfo->get_resource(), $module, $strategy); + if (is_null($handlerinfo)) { + throw new coding_exception("Invalid handler '$module'. The import handler could not be found."); + } + $importproc = new import_processor($course, $config->section, $importinfo->get_resource(), $handlerinfo, $handlerregistry); + $importproc->process(); + + $importinfo->purge(); // We don't need information about the import any more. + + redirect(new moodle_url('/course/view.php', ['id' => $course->id])); +} + +// Setup the page and display the form. +$PAGE->set_context(context_course::instance($course->id)); +$PAGE->set_pagelayout('base'); +$PAGE->set_title(get_string('coursetitle', 'moodle', array('course' => $course->fullname))); +$PAGE->set_heading($course->fullname); +$PAGE->set_url(new moodle_url('/admin/tool/moodlenet/options.php')); + +// Fetch the handlers supporting this resource. We'll display each of these as an option in the form. +$handlercontext = []; +foreach ($handlerregistry->get_resource_handlers_for_strategy($importinfo->get_resource(), $strategy) as $handler) { + $handlercontext[] = [ + 'module' => $handler->get_module_name(), + 'message' => $handler->get_description(), + ]; +} + +// Template context. +$context = [ + 'resourcename' => $importinfo->get_resource()->get_name(), + 'resourcetype' => $importinfo->get_config()->type, + 'resourceurl' => urlencode($importinfo->get_resource()->get_url()->get_value()), + 'course' => $course->id, + 'section' => $config->section, + 'sesskey' => sesskey(), + 'handlers' => $handlercontext, + 'oneoption' => sizeof($handlercontext) === 1 +]; + +echo $OUTPUT->header(); +echo $PAGE->get_renderer('core')->render_from_template('tool_moodlenet/import_options_select', $context); +echo $OUTPUT->footer(); diff --git a/pix/MoodleNet.png b/admin/tool/moodlenet/pix/MoodleNet.png similarity index 100% rename from pix/MoodleNet.png rename to admin/tool/moodlenet/pix/MoodleNet.png diff --git a/pix/MoodleNet.svg b/admin/tool/moodlenet/pix/MoodleNet.svg similarity index 100% rename from pix/MoodleNet.svg rename to admin/tool/moodlenet/pix/MoodleNet.svg diff --git a/admin/tool/moodlenet/pix/courses.svg b/admin/tool/moodlenet/pix/courses.svg new file mode 100644 index 00000000000..75e59fcf04b --- /dev/null +++ b/admin/tool/moodlenet/pix/courses.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/admin/tool/moodlenet/select.php b/admin/tool/moodlenet/select.php new file mode 100644 index 00000000000..704de2ff19c --- /dev/null +++ b/admin/tool/moodlenet/select.php @@ -0,0 +1,53 @@ +. + +/** + * Select page. + * + * @package tool_moodlenet + * @copyright 2020 Mathew May {@link https://mathew.solutions} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +use tool_moodlenet\local\import_info; +use tool_moodlenet\output\select_page; + +require_once(__DIR__ . '/../../../config.php'); + +$id = required_param('id', PARAM_ALPHANUM); + +// Access control. +require_login(); +if (!get_config('tool_moodlenet', 'enablemoodlenet')) { + print_error('moodlenetnotenabled', 'tool_moodlenet'); +} + +if (is_null($importinfo = import_info::load($id))) { + throw new moodle_exception('missinginvalidpostdata', 'tool_moodlenet'); +} + +$PAGE->set_url('/admin/tool/moodlenet/select.php'); +$PAGE->set_context(context_system::instance()); +$PAGE->set_pagelayout('standard'); +$PAGE->set_title(get_string('selectpagetitle', 'tool_moodlenet')); +$PAGE->set_heading(format_string($SITE->fullname)); + +echo $OUTPUT->header(); + +$renderable = new select_page($importinfo); +$renderer = $PAGE->get_renderer('tool_moodlenet'); +echo $renderer->render($renderable); + +echo $OUTPUT->footer(); diff --git a/admin/tool/moodlenet/settings.php b/admin/tool/moodlenet/settings.php new file mode 100644 index 00000000000..4b6fb4f4d31 --- /dev/null +++ b/admin/tool/moodlenet/settings.php @@ -0,0 +1,46 @@ +. + +/** + * Puts the plugin actions into the admin settings tree. + * + * @package tool_moodlenet + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +if ($hassiteconfig) { + // Create a MoodleNet category. + $ADMIN->add('root', new admin_category('moodlenet', get_string('pluginname', 'tool_moodlenet'))); + // Our settings page. + $settings = new admin_settingpage('tool_moodlenet', get_string('moodlenetsettings', 'tool_moodlenet')); + $ADMIN->add('moodlenet', $settings); + + $temp = new admin_setting_configcheckbox('tool_moodlenet/enablemoodlenet', get_string('enablemoodlenet', 'tool_moodlenet'), + new lang_string('enablemoodlenet_desc', 'tool_moodlenet'), 1, 1, 0); + $settings->add($temp); + + $temp = new admin_setting_configtext('tool_moodlenet/defaultmoodlenetname', + get_string('defaultmoodlenetname', 'tool_moodlenet'), new lang_string('defaultmoodlenetname_desc', 'tool_moodlenet'), + 'Moodle HQ MoodleNet'); + $settings->add($temp); + + $temp = new admin_setting_configtext('tool_moodlenet/defaultmoodlenet', get_string('defaultmoodlenet', 'tool_moodlenet'), + new lang_string('defaultmoodlenet_desc', 'tool_moodlenet'), 'https://home.moodle.net'); + $settings->add($temp); +} diff --git a/admin/tool/moodlenet/templates/chooser_footer.mustache b/admin/tool/moodlenet/templates/chooser_footer.mustache new file mode 100644 index 00000000000..6748023cd37 --- /dev/null +++ b/admin/tool/moodlenet/templates/chooser_footer.mustache @@ -0,0 +1,46 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_moodlenet/chooser_footer + + Chooser favourite template partial. + + Example context (json): + { + } +}} +{{#enabled}} + +{{/enabled}} diff --git a/admin/tool/moodlenet/templates/chooser_footer_close_mnet.mustache b/admin/tool/moodlenet/templates/chooser_footer_close_mnet.mustache new file mode 100644 index 00000000000..2e6f05e3481 --- /dev/null +++ b/admin/tool/moodlenet/templates/chooser_footer_close_mnet.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 tool_moodlenet/chooser_footer_close_mnet + + Chooser favourite template partial. + + Example context (json): + { + } +}} + diff --git a/admin/tool/moodlenet/templates/chooser_moodlenet.mustache b/admin/tool/moodlenet/templates/chooser_moodlenet.mustache new file mode 100644 index 00000000000..c0c7fc534ad --- /dev/null +++ b/admin/tool/moodlenet/templates/chooser_moodlenet.mustache @@ -0,0 +1,67 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_moodlenet/chooser_moodlenet + + Chooser favourite template partial. + + Example context (json): + { + } +}} +
+
+
+
+ +

{{#str}} instancedescription, tool_moodlenet {{/str}}

+

{{#str}} connectandbrowse, tool_moodlenet {{/str}}

+ + {{{buttonName}}} + +
+ +
+

{{#str}}inputhelp, tool_moodlenet{{/str}}

+
+ +
+ +
+
+

+

{{#str}} forminfo, tool_moodlenet {{/str}}

+
+
+
+
+
diff --git a/admin/tool/moodlenet/templates/import_confirmation.mustache b/admin/tool/moodlenet/templates/import_confirmation.mustache new file mode 100644 index 00000000000..337b22abd5e --- /dev/null +++ b/admin/tool/moodlenet/templates/import_confirmation.mustache @@ -0,0 +1,76 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_moodlenet/import_confirmation + + MoodleNet import confirmation template. + + The purpose of this template is to present the user with a confirm/cancel dialog-like page. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * resourceurl The URL to the remote resource on MoodleNet. + * resourcename The name of the remote resource on MoodleNet. + * sesskey The CSRF token, as per sesskey() + + Example context (json): + { + "course": 33, + "coursename": "Introduction to quantum physics", + "section": 0, + "resourceurl": "http://example.com/test.png", + "resourcename": "test.png", + "sesskey": "abc123" + } +}} + diff --git a/admin/tool/moodlenet/templates/import_options_select.mustache b/admin/tool/moodlenet/templates/import_options_select.mustache new file mode 100644 index 00000000000..9dc42ebdd6a --- /dev/null +++ b/admin/tool/moodlenet/templates/import_options_select.mustache @@ -0,0 +1,80 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_moodlenet/import_options_select + + MoodleNet import options template. + + The purpose of this template is to render an list of import options as radio-button-like controls. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * course The course id. + * section The sectio id. + * resourceurl The URL to the remote resource on MoodleNet. + * resourcename The name of the remote resource on MoodleNet. + * sesskey The CSRF token, as per sesskey() + * handlers The array of handler options to present the user with + + Example context (json): + { + "course": 33, + "coursename": "Introduction to quantum physics", + "section": 0, + "resourceurl": "http://example.com/test.png", + "resourcename": "A test image", + "sesskey": "abc123", + "handlers": [ + { + "module": "label", + "message": "Add media to the course page" + } + ] + } +}} + diff --git a/admin/tool/moodlenet/templates/select_page.mustache b/admin/tool/moodlenet/templates/select_page.mustache new file mode 100644 index 00000000000..a49903bd244 --- /dev/null +++ b/admin/tool/moodlenet/templates/select_page.mustache @@ -0,0 +1,58 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_moodlenet/select_page + + This template renders the course selection page for the MoodleNet tool. + + Example context (json): + { + "name": "A cat picture", + "cancellink": "https://moodlesite/my" + } +}} +
+ +

{{#str}} selectacourse {{/str}}

+ +
+
diff --git a/admin/tool/moodlenet/templates/view-cards.mustache b/admin/tool/moodlenet/templates/view-cards.mustache new file mode 100644 index 00000000000..da742ffe671 --- /dev/null +++ b/admin/tool/moodlenet/templates/view-cards.mustache @@ -0,0 +1,54 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_moodlenet/view-cards + + This template renders the cards view for the MoodleNet tool. + + Example context (json): + { + "courses": [ + { + "name": "Assignment due 1", + "viewurl": "https://moodlesite/course/view.php?id=2", + "courseimage": "https://moodlesite/pluginfile/123/course/overviewfiles/123.jpg", + "fullname": "course 3", + "coursecategory": "Miscellaneous", + "visible": true + } + ] + } +}} + +{{< core_course/coursecards }} + {{$coursename}} + + {{#shortentext}}50, {{{fullname}}} {{/shortentext}} + + {{/coursename}} + {{$coursecategory}} + + {{#str}}aria:coursecategory, core_course{{/str}} + + + {{{coursecategory}}} + + {{/coursecategory}} + {{$divider}} +
|
+ {{/divider}} +{{/ core_course/coursecards }} diff --git a/admin/tool/moodlenet/tests/import_backup_helper_test.php b/admin/tool/moodlenet/tests/import_backup_helper_test.php new file mode 100644 index 00000000000..81ea96a3fde --- /dev/null +++ b/admin/tool/moodlenet/tests/import_backup_helper_test.php @@ -0,0 +1,84 @@ +. + +/** + * Unit tests for the import_backup_helper + * + * @package tool_moodlenet + * @category test + * @copyright 2020 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +/** + * Class import_backup_helper tests + */ +class tool_moodlenet_import_backup_helper_testcase extends advanced_testcase { + + /** + * Test that the first available context with the capability to upload backup files is returned. + */ + public function test_get_context_for_user() { + global $DB; + + $this->resetAfterTest(); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + $user4 = $this->getDataGenerator()->create_user(); + $user5 = $this->getDataGenerator()->create_user(); + + $course = $this->getDataGenerator()->create_course(); + $coursecontext = context_course::instance($course->id); + + $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); + $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'editingteacher'); + $this->getDataGenerator()->enrol_user($user5->id, $course->id, 'student'); + + $category = $this->getDataGenerator()->create_category(); + $rolerecord = $DB->get_record('role', ['shortname' => 'manager']); + $categorycontext = context_coursecat::instance($category->id); + $this->getDataGenerator()->role_assign($rolerecord->id, $user3->id, $categorycontext->id); + $this->getDataGenerator()->role_assign($rolerecord->id, $user5->id, $categorycontext->id); + + $roleid = $this->getDataGenerator()->create_role(); + $sitecontext = context_system::instance(); + assign_capability('moodle/restore:uploadfile', CAP_ALLOW, $roleid, $sitecontext->id, true); + accesslib_clear_all_caches_for_unit_testing(); + $this->getDataGenerator()->role_assign($roleid, $user4->id, $sitecontext->id); + + $result = \tool_moodlenet\local\import_backup_helper::get_context_for_user($user1->id); + $this->assertNull($result); + $result = \tool_moodlenet\local\import_backup_helper::get_context_for_user($user2->id); + $this->assertEquals($result, $coursecontext); + $this->assertEquals(CONTEXT_COURSE, $result->contextlevel); + $result = \tool_moodlenet\local\import_backup_helper::get_context_for_user($user3->id); + $this->assertEquals($result, $categorycontext); + $this->assertEquals(CONTEXT_COURSECAT, $result->contextlevel); + $result = \tool_moodlenet\local\import_backup_helper::get_context_for_user($user4->id); + $this->assertEquals($result, $sitecontext); + $this->assertEquals(CONTEXT_SYSTEM, $result->contextlevel); + $result = \tool_moodlenet\local\import_backup_helper::get_context_for_user($user5->id); + $this->assertEquals($result, $categorycontext); + $this->assertEquals(CONTEXT_COURSECAT, $result->contextlevel); + } + +} \ No newline at end of file diff --git a/admin/tool/moodlenet/tests/import_handler_info_test.php b/admin/tool/moodlenet/tests/import_handler_info_test.php new file mode 100644 index 00000000000..7f308c040b6 --- /dev/null +++ b/admin/tool/moodlenet/tests/import_handler_info_test.php @@ -0,0 +1,75 @@ +. + +/** + * Unit tests for the import_handler_info class. + * + * @package tool_moodlenet + * @category test + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_moodlenet\local\tests; + +use tool_moodlenet\local\import_handler_info; +use tool_moodlenet\local\import_strategy; +use tool_moodlenet\local\import_strategy_file; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class tool_moodlenet_import_handler_info_testcase, providing test cases for the import_handler_info class. + */ +class tool_moodlenet_import_handler_info_testcase extends \advanced_testcase { + + /** + * Test init and the getters. + * + * @dataProvider handler_info_data_provider + * @param string $modname the name of the mod. + * @param string $description description of the mod. + * @param bool $expectexception whether we expect an exception during init or not. + */ + public function test_initialisation($modname, $description, $expectexception) { + $this->resetAfterTest(); + // Skip those cases we cannot init. + if ($expectexception) { + $this->expectException(\coding_exception::class); + $handlerinfo = new import_handler_info($modname, $description, new import_strategy_file()); + } + + $handlerinfo = new import_handler_info($modname, $description, new import_strategy_file()); + + $this->assertEquals($modname, $handlerinfo->get_module_name()); + $this->assertEquals($description, $handlerinfo->get_description()); + $this->assertInstanceOf(import_strategy::class, $handlerinfo->get_strategy()); + } + + + /** + * Data provider for creation of import_handler_info objects. + * + * @return array the data for creation of the info object. + */ + public function handler_info_data_provider() { + return [ + 'All data present' => ['label', 'Add a label to the course', false], + 'Empty module name' => ['', 'Add a file resource to the course', true], + 'Empty description' => ['resource', '', true], + + ]; + } +} diff --git a/admin/tool/moodlenet/tests/import_handler_registry_test.php b/admin/tool/moodlenet/tests/import_handler_registry_test.php new file mode 100644 index 00000000000..df7399c85a2 --- /dev/null +++ b/admin/tool/moodlenet/tests/import_handler_registry_test.php @@ -0,0 +1,119 @@ +. + +/** + * Unit tests for the import_handler_registry class. + * + * @package tool_moodlenet + * @category test + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_moodlenet\local\tests; + +use tool_moodlenet\local\import_handler_registry; +use tool_moodlenet\local\import_handler_info; +use tool_moodlenet\local\import_strategy_file; +use tool_moodlenet\local\import_strategy_link; +use tool_moodlenet\local\remote_resource; +use tool_moodlenet\local\url; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class tool_moodlenet_import_handler_registry_testcase, providing test cases for the import_handler_registry class. + */ +class tool_moodlenet_import_handler_registry_testcase extends \advanced_testcase { + + /** + * Test confirming the behaviour of get_resource_handlers_for_strategy with different params. + */ + public function test_get_resource_handlers_for_strategy() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); + $ihr = new import_handler_registry($course, $teacher); + $resource = new remote_resource( + new \curl(), + new url('http://example.org'), + (object) [ + 'name' => 'Resource name', + 'description' => 'Resource description' + ] + ); + + $handlers = $ihr->get_resource_handlers_for_strategy($resource, new import_strategy_file()); + $this->assertIsArray($handlers); + foreach ($handlers as $handler) { + $this->assertInstanceOf(import_handler_info::class, $handler); + } + } + + /** + * Test confirming that the results are scoped to the provided user. + */ + public function test_get_resource_handlers_for_strategy_user_scoping() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); + + $studentihr = new import_handler_registry($course, $student); + $teacherihr = new import_handler_registry($course, $teacher); + $resource = new remote_resource( + new \curl(), + new url('http://example.org'), + (object) [ + 'name' => 'Resource name', + 'description' => 'Resource description' + ] + ); + + $this->assertEmpty($studentihr->get_resource_handlers_for_strategy($resource, new import_strategy_file())); + $this->assertNotEmpty($teacherihr->get_resource_handlers_for_strategy($resource, new import_strategy_file())); + } + + /** + * Test confirming that we can find a unique handler based on the module and strategy name. + */ + public function test_get_resource_handler_for_module_and_strategy() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); + $ihr = new import_handler_registry($course, $teacher); + $resource = new remote_resource( + new \curl(), + new url('http://example.org'), + (object) [ + 'name' => 'Resource name', + 'description' => 'Resource description' + ] + ); + + // Resource handles every file type, so we'll always be able to find that unique handler when looking. + $handler = $ihr->get_resource_handler_for_mod_and_strategy($resource, 'resource', new import_strategy_file()); + $this->assertInstanceOf(import_handler_info::class, $handler); + + // URL handles every resource, so we'll always be able to find that unique handler when looking with a link strategy. + $handler = $ihr->get_resource_handler_for_mod_and_strategy($resource, 'url', new import_strategy_link()); + $this->assertInstanceOf(import_handler_info::class, $handler); + $this->assertEquals('url', $handler->get_module_name()); + $this->assertInstanceOf(import_strategy_link::class, $handler->get_strategy()); + } +} diff --git a/admin/tool/moodlenet/tests/import_info_test.php b/admin/tool/moodlenet/tests/import_info_test.php new file mode 100644 index 00000000000..11513710c39 --- /dev/null +++ b/admin/tool/moodlenet/tests/import_info_test.php @@ -0,0 +1,105 @@ +. + +/** + * Unit tests for the import_info class. + * + * @package tool_moodlenet + * @category test + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_moodlenet\local\tests; + +use tool_moodlenet\local\import_info; +use tool_moodlenet\local\remote_resource; +use tool_moodlenet\local\url; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class tool_moodlenet_import_info_testcase, providing test cases for the import_info class. + */ +class tool_moodlenet_import_info_testcase extends \advanced_testcase { + + /** + * Create some test objects. + * + * @return array + */ + protected function create_test_info(): array { + $user = $this->getDataGenerator()->create_user(); + $resource = new remote_resource(new \curl(), + new url('http://example.org'), + (object) [ + 'name' => 'Resource name', + 'description' => 'Resource summary' + ] + ); + $importinfo = new import_info($user->id, $resource, (object)[]); + + return [$user, $resource, $importinfo]; + } + + /** + * Test for creation and getters. + */ + public function test_getters() { + $this->resetAfterTest(); + [$user, $resource, $importinfo] = $this->create_test_info(); + + $this->assertEquals($resource, $importinfo->get_resource()); + $this->assertEquals(new \stdClass(), $importinfo->get_config()); + $this->assertNotEmpty($importinfo->get_id()); + } + + /** + * Test for setters. + */ + public function test_set_config() { + $this->resetAfterTest(); + [$user, $resource, $importinfo] = $this->create_test_info(); + + $config = $importinfo->get_config(); + $this->assertEquals(new \stdClass(), $config); + $config->course = 3; + $config->section = 1; + $importinfo->set_config($config); + $this->assertEquals((object) ['course' => 3, 'section' => 1], $importinfo->get_config()); + } + + /** + * Verify the object can be stored and loaded. + */ + public function test_persistence() { + $this->resetAfterTest(); + [$user, $resource, $importinfo] = $this->create_test_info(); + + // Nothing to load initially since nothing has been saved. + $loadedinfo = import_info::load($importinfo->get_id()); + $this->assertNull($loadedinfo); + + // Now, save and confirm we can load the data into a new object. + $importinfo->save(); + $loadedinfo2 = import_info::load($importinfo->get_id()); + $this->assertEquals($importinfo, $loadedinfo2); + + // Purge and confirm the load returns null now. + $importinfo->purge(); + $loadedinfo3 = import_info::load($importinfo->get_id()); + $this->assertNull($loadedinfo3); + } +} diff --git a/admin/tool/moodlenet/tests/import_processor_test.php b/admin/tool/moodlenet/tests/import_processor_test.php new file mode 100644 index 00000000000..ecfcec4ef75 --- /dev/null +++ b/admin/tool/moodlenet/tests/import_processor_test.php @@ -0,0 +1,156 @@ +. + +/** + * Unit tests for the import_processor class. + * + * @package tool_moodlenet + * @category test + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_moodlenet\local\tests; + +use tool_moodlenet\local\import_handler_registry; +use tool_moodlenet\local\import_processor; +use tool_moodlenet\local\import_strategy_file; +use tool_moodlenet\local\import_strategy_link; +use tool_moodlenet\local\remote_resource; +use tool_moodlenet\local\url; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class tool_moodlenet_import_processor_testcase, providing test cases for the import_processor class. + */ +class tool_moodlenet_import_processor_testcase extends \advanced_testcase { + + /** + * An integration test, this confirms the ability to construct an import processor and run the import for the current user. + */ + public function test_process_valid_resource() { + $this->resetAfterTest(); + + // Set up a user as a teacher in a course. + $course = $this->getDataGenerator()->create_course(); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); + $section = 0; + $this->setUser($teacher); + + // Set up the import, using a mod_resource handler for the html extension. + $resourceurl = $this->getExternalTestFileUrl('/test.html'); + $remoteresource = new remote_resource( + new \curl(), + new url($resourceurl), + (object) [ + 'name' => 'Resource name', + 'description' => 'Resource description' + ] + ); + $handlerregistry = new import_handler_registry($course, $teacher); + $handlerinfo = $handlerregistry->get_resource_handler_for_mod_and_strategy($remoteresource, 'resource', + new import_strategy_file()); + $importproc = new import_processor($course, $section, $remoteresource, $handlerinfo, $handlerregistry); + + // Import the file. + $importproc->process(); + + // Verify there is a new mod_resource created with correct name, description and containing the test.html file. + $modinfo = get_fast_modinfo($course, $teacher->id); + $cms = $modinfo->get_instances(); + $this->assertArrayHasKey('resource', $cms); + $cminfo = array_shift($cms['resource']); + $this->assertEquals('Resource name', $cminfo->get_formatted_name()); + $cm = get_coursemodule_from_id('', $cminfo->id, 0, false, MUST_EXIST); + list($cm, $context, $module, $data, $cw) = get_moduleinfo_data($cminfo, $course); + $this->assertEquals($remoteresource->get_description(), $data->intro); + $fs = get_file_storage(); + $files = $fs->get_area_files(\context_module::instance($cminfo->id)->id, 'mod_resource', 'content', false, + 'sortorder DESC, id ASC', false); + $file = reset($files); + $this->assertEquals('test.html', $file->get_filename()); + $this->assertEquals('text/html', $file->get_mimetype()); + } + + /** + * Test confirming that an exception is thrown when trying to process a resource which does not exist. + */ + public function test_process_invalid_resource() { + $this->resetAfterTest(); + + // Set up a user as a teacher in a course. + $course = $this->getDataGenerator()->create_course(); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); + $section = 0; + $this->setUser($teacher); + + // Set up the import, using a mod_resource handler for the html extension. + $resourceurl = $this->getExternalTestFileUrl('/test.htmlzz'); + $remoteresource = new remote_resource( + new \curl(), + new url($resourceurl), + (object) [ + 'name' => 'Resource name', + 'description' => 'Resource description' + ] + ); + $handlerregistry = new import_handler_registry($course, $teacher); + $handlerinfo = $handlerregistry->get_resource_handler_for_mod_and_strategy($remoteresource, 'resource', + new import_strategy_file()); + $importproc = new import_processor($course, $section, $remoteresource, $handlerinfo, $handlerregistry); + + // Import the file. + $this->expectException(\coding_exception::class); + $importproc->process(); + } + + /** + * Test confirming that imports can be completed using alternative import strategies. + */ + public function test_process_alternative_import_strategies() { + $this->resetAfterTest(); + + // Set up a user as a teacher in a course. + $course = $this->getDataGenerator()->create_course(); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); + $section = 0; + $this->setUser($teacher); + + // Set up the import, using a mod_url handler and the link import strategy. + $remoteresource = new remote_resource( + new \curl(), + new url('http://example.com/cats.pdf'), + (object) [ + 'name' => 'Resource name', + 'description' => 'Resource description' + ] + ); + $handlerregistry = new import_handler_registry($course, $teacher); + $handlerinfo = $handlerregistry->get_resource_handler_for_mod_and_strategy($remoteresource, 'url', + new import_strategy_link()); + $importproc = new import_processor($course, $section, $remoteresource, $handlerinfo, $handlerregistry); + + // Import the resource as a link. + $importproc->process(); + + // Verify there is a new mod_url created with name 'cats' and containing the URL of the resource. + $modinfo = get_fast_modinfo($course, $teacher->id); + $cms = $modinfo->get_instances(); + $this->assertArrayHasKey('url', $cms); + $cminfo = array_shift($cms['url']); + $this->assertEquals('Resource name', $cminfo->get_formatted_name()); + } +} diff --git a/admin/tool/moodlenet/tests/lib_test.php b/admin/tool/moodlenet/tests/lib_test.php new file mode 100644 index 00000000000..d63afe2eb84 --- /dev/null +++ b/admin/tool/moodlenet/tests/lib_test.php @@ -0,0 +1,80 @@ +. + +/** + * Unit tests for tool_moodlenet lib + * + * @package tool_moodlenet + * @copyright 2020 Peter Dias + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/admin/tool/moodlenet/lib.php'); + +/** + * Test moodlenet functions + */ +class tool_moodlenet_lib_testcase extends advanced_testcase { + + /** + * Test the generate_mnet_endpoint function + * + * @dataProvider get_endpoints_provider + * @param string $profileurl + * @param int $course + * @param int $section + * @param string $expected + */ + public function test_generate_mnet_endpoint($profileurl, $course, $section, $expected) { + $endpoint = generate_mnet_endpoint($profileurl, $course, $section); + $this->assertEquals($expected, $endpoint); + } + + /** + * Dataprovider for test_generate_mnet_endpoint + * + * @return array + */ + public function get_endpoints_provider() { + global $CFG; + return [ + [ + '@name@domain.name', + 1, + 2, + 'https://domain.name/' . MOODLENET_DEFAULT_ENDPOINT . '?site=' . urlencode($CFG->wwwroot) + . '&course=1§ion=2' + ], + [ + '@profile@name@domain.name', + 1, + 2, + 'https://domain.name/' . MOODLENET_DEFAULT_ENDPOINT . '?site=' . urlencode($CFG->wwwroot) + . '&course=1§ion=2' + ], + [ + 'https://domain.name', + 1, + 2, + 'https://domain.name/' . MOODLENET_DEFAULT_ENDPOINT . '?site=' . urlencode($CFG->wwwroot) + . '&course=1§ion=2' + ] + ]; + } +} diff --git a/admin/tool/moodlenet/tests/profile_manager_test.php b/admin/tool/moodlenet/tests/profile_manager_test.php new file mode 100644 index 00000000000..9485fbf90a4 --- /dev/null +++ b/admin/tool/moodlenet/tests/profile_manager_test.php @@ -0,0 +1,142 @@ +. + +/** + * Unit tests for the profile manager + * + * @package tool_moodlenet + * @category test + * @copyright 2020 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +/** + * Class profile_manager tests + */ +class tool_moodlenet_profile_manager_testcase extends advanced_testcase { + + /** + * Test that on this site we use the user table to hold moodle net profile information. + */ + public function test_official_profile_exists() { + $this->assertTrue(\tool_moodlenet\profile_manager::official_profile_exists()); + } + + /** + * Test a null is returned when the user's mnet profile field is not set. + */ + public function test_get_moodlenet_user_profile_no_profile_set() { + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + + $result = \tool_moodlenet\profile_manager::get_moodlenet_user_profile($user->id); + $this->assertNull($result); + } + + /** + * Test a null is returned when the user's mnet profile field is not set. + */ + public function test_moodlenet_user_profile_creation_no_profile_set() { + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + + $this->expectException(moodle_exception::class); + $this->expectExceptionMessage(get_string('invalidmoodlenetprofile', 'tool_moodlenet')); + $result = new \tool_moodlenet\moodlenet_user_profile("", $user->id); + } + + /** + * Test the return of a moodle net profile. + */ + public function test_get_moodlenet_user_profile() { + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(['moodlenetprofile' => '@matt@hq.mnet']); + + $result = \tool_moodlenet\profile_manager::get_moodlenet_user_profile($user->id); + $this->assertEquals($user->moodlenetprofile, $result->get_profile_name()); + } + + /** + * Test the creation of a user profile category. + */ + public function test_create_user_profile_category() { + global $DB; + $this->resetAfterTest(); + + $basecategoryname = get_string('pluginname', 'tool_moodlenet'); + + \tool_moodlenet\profile_manager::create_user_profile_category(); + $categoryname = \tool_moodlenet\profile_manager::get_category_name(); + $this->assertEquals($basecategoryname, $categoryname); + \tool_moodlenet\profile_manager::create_user_profile_category(); + + $recordcount = $DB->count_records('user_info_category', ['name' => $basecategoryname]); + $this->assertEquals(1, $recordcount); + + // Test the duplication of categories to ensure a unique name is always used. + $categoryname = \tool_moodlenet\profile_manager::get_category_name(); + $this->assertEquals($basecategoryname . 1, $categoryname); + \tool_moodlenet\profile_manager::create_user_profile_category(); + $categoryname = \tool_moodlenet\profile_manager::get_category_name(); + $this->assertEquals($basecategoryname . 2, $categoryname); + } + + /** + * Test the creating of the custom user profile field to hold the moodle net profile. + */ + public function test_create_user_profile_text_field() { + global $DB; + $this->resetAfterTest(); + + $shortname = 'mnetprofile'; + + $categoryid = \tool_moodlenet\profile_manager::create_user_profile_category(); + \tool_moodlenet\profile_manager::create_user_profile_text_field($categoryid); + + $record = $DB->get_record('user_info_field', ['shortname' => $shortname]); + $this->assertEquals($shortname, $record->shortname); + $this->assertEquals($categoryid, $record->categoryid); + + // Test for a unique name if 'mnetprofile' is already in use. + \tool_moodlenet\profile_manager::create_user_profile_text_field($categoryid); + $profilename = \tool_moodlenet\profile_manager::get_profile_field_name(); + $this->assertEquals($shortname . 1, $profilename); + \tool_moodlenet\profile_manager::create_user_profile_text_field($categoryid); + $profilename = \tool_moodlenet\profile_manager::get_profile_field_name(); + $this->assertEquals($shortname . 2, $profilename); + } + + /** + * Test that the user moodlenet profile is saved. + */ + public function test_save_moodlenet_user_profile() { + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(); + $profilename = '@matt@hq.mnet'; + + $moodlenetprofile = new \tool_moodlenet\moodlenet_user_profile($profilename, $user->id); + + \tool_moodlenet\profile_manager::save_moodlenet_user_profile($moodlenetprofile); + + $userdata = \core_user::get_user($user->id); + $this->assertEquals($profilename, $userdata->moodlenetprofile); + } +} diff --git a/admin/tool/moodlenet/tests/remote_resource_test.php b/admin/tool/moodlenet/tests/remote_resource_test.php new file mode 100644 index 00000000000..2e108742bf6 --- /dev/null +++ b/admin/tool/moodlenet/tests/remote_resource_test.php @@ -0,0 +1,115 @@ +. + +/** + * Unit tests for the remote_resource class. + * + * @package tool_moodlenet + * @category test + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_moodlenet\local\tests; + +use tool_moodlenet\local\remote_resource; +use tool_moodlenet\local\url; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class tool_moodlenet_remote_resource_testcase, providing test cases for the remote_resource class. + */ +class tool_moodlenet_remote_resource_testcase extends \advanced_testcase { + + /** + * Test getters. + * + * @dataProvider remote_resource_data_provider + * @param string $url the url of the resource. + * @param string $metadata the resource metadata like name, description, etc. + * @param string $expectedextension the extension we expect to find when querying the remote resource. + */ + public function test_getters($url, $metadata, $expectedextension) { + $this->resetAfterTest(); + + $remoteres = new remote_resource(new \curl(), new url($url), $metadata); + + $this->assertEquals(new url($url), $remoteres->get_url()); + $this->assertEquals($metadata->name, $remoteres->get_name()); + $this->assertEquals($metadata->description, $remoteres->get_description()); + $this->assertEquals($expectedextension, $remoteres->get_extension()); + } + + /** + * Data provider generating remote urls. + * + * @return array + */ + public function remote_resource_data_provider() { + return [ + 'With filename and extension' => [ + $this->getExternalTestFileUrl('/test.html'), + (object) [ + 'name' => 'Test html file', + 'description' => 'Full description of the html file' + ], + 'html' + ], + 'With filename only' => [ + 'http://example.com/path/file', + (object) [ + 'name' => 'Test html file', + 'description' => 'Full description of the html file' + ], + '' + ] + ]; + } + + /** + * Test confirming the network based operations of a remote_resource. + */ + public function test_network_features() { + $url = $this->getExternalTestFileUrl('/test.html'); + $nonexistenturl = $this->getExternalTestFileUrl('/test.htmlzz'); + + $remoteres = new remote_resource( + new \curl(), + new url($url), + (object) [ + 'name' => 'Test html file', + 'description' => 'Some description' + ] + ); + $nonexistentremoteres = new remote_resource( + new \curl(), + new url($nonexistenturl), + (object) [ + 'name' => 'Test html file', + 'description' => 'Some description' + ] + ); + + $this->assertGreaterThan(0, $remoteres->get_download_size()); + [$path, $name] = $remoteres->download_to_requestdir(); + $this->assertIsString($path); + $this->assertEquals('test.html', $name); + $this->assertFileExists($path . '/' . $name); + + $this->expectException(\coding_exception::class); + $nonexistentremoteres->get_download_size(); + } +} diff --git a/admin/tool/moodlenet/tests/url_test.php b/admin/tool/moodlenet/tests/url_test.php new file mode 100644 index 00000000000..74a7349bec4 --- /dev/null +++ b/admin/tool/moodlenet/tests/url_test.php @@ -0,0 +1,103 @@ +. + +/** + * Unit tests for the url class. + * + * @package tool_moodlenet + * @category test + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_moodlenet\local\tests; + +use tool_moodlenet\local\url; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class tool_moodlenet_url_testcase, providing test cases for the url class. + */ +class tool_moodlenet_url_testcase extends \advanced_testcase { + + /** + * Test the parsing to host + path components. + * + * @dataProvider url_provider + * @param string $urlstring The full URL string + * @param string $host the expected host component of the URL. + * @param string $path the expected path component of the URL. + * @param bool $exception whether or not an exception is expected during construction. + */ + public function test_parsing($urlstring, $host, $path, $exception) { + if ($exception) { + $this->expectException(\coding_exception::class); + $url = new url($urlstring); + return; + } + + $url = new url($urlstring); + $this->assertEquals($urlstring, $url->get_value()); + $this->assertEquals($host, $url->get_host()); + $this->assertEquals($path, $url->get_path()); + } + + /** + * Data provider. + * + * @return array + */ + public function url_provider() { + return [ + 'No path' => [ + 'url' => 'https://example.moodle.net', + 'host' => 'example.moodle.net', + 'path' => null, + 'exception' => false, + ], + 'Slash path' => [ + 'url' => 'https://example.moodle.net/', + 'host' => 'example.moodle.net', + 'path' => '/', + 'exception' => false, + ], + 'Path includes file and extension' => [ + 'url' => 'https://example.moodle.net/uploads/123456789/pic.png', + 'host' => 'example.moodle.net', + 'path' => '/uploads/123456789/pic.png', + 'exception' => false, + ], + 'Path includes file, extension and params' => [ + 'url' => 'https://example.moodle.net/uploads/123456789/pic.png?option=1&option2=test', + 'host' => 'example.moodle.net', + 'path' => '/uploads/123456789/pic.png', + 'exception' => false, + ], + 'Malformed - invalid' => [ + 'url' => 'invalid', + 'host' => null, + 'path' => null, + 'exception' => true, + ], + 'Direct, non-encoded utf8 - invalid' => [ + 'url' => 'http://москва.рф/services/', + 'host' => 'москва.рф', + 'path' => '/services/', + 'exception' => true, + ], + ]; + } +} diff --git a/admin/tool/moodlenet/version.php b/admin/tool/moodlenet/version.php new file mode 100644 index 00000000000..d6a7b6e7daf --- /dev/null +++ b/admin/tool/moodlenet/version.php @@ -0,0 +1,30 @@ +. + +/** + * Version file for tool_moodlenet + * + * @package tool_moodlenet + * @copyright 2020 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'tool_moodlenet'; +$plugin->version = 2020060500; +$plugin->requires = 2020022800.01; +$plugin->maturity = MATURITY_ALPHA; diff --git a/course/templates/local/activitychooser/help.mustache b/course/templates/local/activitychooser/help.mustache index 9ddacd44496..471dffc380f 100644 --- a/course/templates/local/activitychooser/help.mustache +++ b/course/templates/local/activitychooser/help.mustache @@ -45,7 +45,7 @@ {{^showFooter}} -
+ {{/showFooter}} diff --git a/theme/boost/scss/moodle/core.scss b/theme/boost/scss/moodle/core.scss index 411f762413e..32a17d6717f 100644 --- a/theme/boost/scss/moodle/core.scss +++ b/theme/boost/scss/moodle/core.scss @@ -1639,6 +1639,7 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview { .modchooser .modal-footer { height: 70px; + background: $modal-content-bg; .moodlenet-logo { .icon { height: 2.5rem; diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index b2d48fe4e66..f0ec2361252 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -11002,7 +11002,8 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview { margin: 1em auto; } .modchooser .modal-footer { - height: 70px; } + height: 70px; + background: #fff; } .modchooser .modal-footer .moodlenet-logo .icon { height: 2.5rem; width: 6rem; diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index e2c098d5e91..316f966a9ac 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -11209,7 +11209,8 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview { margin: 1em auto; } .modchooser .modal-footer { - height: 70px; } + height: 70px; + background: #fff; } .modchooser .modal-footer .moodlenet-logo .icon { height: 2.5rem; width: 6rem;