diff --git a/public/files/converter/googledrive/lib.php b/public/files/converter/googledrive/lib.php index 72b3a746119..7a5f00980d5 100644 --- a/public/files/converter/googledrive/lib.php +++ b/public/files/converter/googledrive/lib.php @@ -24,6 +24,8 @@ defined('MOODLE_INTERNAL') || die(); +require_once($CFG->libdir . '/google/lib.php'); + /** * Callback to get the required scopes for system account. * @@ -32,7 +34,7 @@ defined('MOODLE_INTERNAL') || die(); */ function fileconverter_googledrive_oauth2_system_scopes(\core\oauth2\issuer $issuer) { if ($issuer->get('id') == get_config('fileconverter_googledrive', 'issuerid')) { - return 'https://www.googleapis.com/auth/drive'; + return Google_Service_Drive::DRIVE_FILE; } return ''; } diff --git a/public/lang/en/repository.php b/public/lang/en/repository.php index 62ff53560c8..1f0f9d73635 100644 --- a/public/lang/en/repository.php +++ b/public/lang/en/repository.php @@ -244,6 +244,7 @@ $string['type'] = 'Type'; $string['typenotvisible'] = 'Type not visible'; $string['unknownoriginal'] = 'Unknown'; $string['upload'] = 'Upload this file'; +$string['uploaderror'] = 'File upload error'; $string['uploading'] = 'Uploading...'; $string['uploadsucc'] = 'The file has been uploaded successfully'; $string['unknownsource'] = 'Unknown source'; diff --git a/public/lib/templates/filemanager_modal_generallayout.mustache b/public/lib/templates/filemanager_modal_generallayout.mustache index 3657e112262..120c555fb22 100644 --- a/public/lib/templates/filemanager_modal_generallayout.mustache +++ b/public/lib/templates/filemanager_modal_generallayout.mustache @@ -56,6 +56,11 @@ {{#pix}}a/setting{{/pix}} +
+ + {{#pix}}i/upload{{/pix}} + +
{{#pix}}a/help{{/pix}} diff --git a/public/repository/filepicker.js b/public/repository/filepicker.js index 1e1f9077188..4db058a35d5 100644 --- a/public/repository/filepicker.js +++ b/public/repository/filepicker.js @@ -1632,6 +1632,8 @@ M.core_filepicker.init = function(Y, options) { this.active_repo.message = (data.message || ''); this.active_repo.help = data.help?data.help:null; this.active_repo.manage = data.manage?data.manage:null; + this.active_repo.uploadfile = data.uploadfile ? data.uploadfile : null; + this.active_repo.uploadevent = data.uploadevent ? data.uploadevent : null; // Warning message related to the file reference option, if applicable to the given repository. this.active_repo.filereferencewarning = data.filereferencewarning ? data.filereferencewarning : null; this.print_header(); @@ -2003,6 +2005,28 @@ M.core_filepicker.init = function(Y, options) { managelnk.simulate('click') }); + // Repository upload. + // If repository supports upload, it needs to provide 'uploadevent' in its response to 'list' command. + // This event will be used to trigger the upload process. + // Repository will need to subscribe to this event and launch its own upload process. + toolbar.one('.fp-tb-uploadfile').one('a,button').on('click', (e) => { + if (this.active_repo.uploadevent) { + e.preventDefault(); + require(['core/pubsub'], (PubSub) => { + PubSub.publish(this.active_repo.uploadevent, { + repoId: this.active_repo.id, + contextId: this.options.context.id, + callback: () => { + // Refresh the file list after upload is done. + if (!this.active_repo.norefresh) { + this.list({path: this.currentpath}); + } + } + }); + }); + } + }, this); + // same with .fp-tb-help var helplnk = Y.Node.create(''). setAttrs({id:'fp-tb-help-'+client_id+'-link', target:'_blank'}). @@ -2079,6 +2103,9 @@ M.core_filepicker.init = function(Y, options) { enable_tb_control(toolbar.one('.fp-tb-manage'), r.manage); Y.one('#fp-tb-manage-'+client_id+'-link').set('href', r.manage); + // Upload file. + enable_tb_control(toolbar.one('.fp-tb-uploadfile'), r.uploadfile); + // help url enable_tb_control(toolbar.one('.fp-tb-help'), r.help); Y.one('#fp-tb-help-'+client_id+'-link').set('href', r.help); diff --git a/public/repository/googledocs/amd/build/upload.min.js b/public/repository/googledocs/amd/build/upload.min.js new file mode 100644 index 00000000000..4165865db19 --- /dev/null +++ b/public/repository/googledocs/amd/build/upload.min.js @@ -0,0 +1,10 @@ +define("repository_googledocs/upload",["exports","core/pubsub","core/modal_save_cancel","core/str","core/templates","core/modal_events","core/dropzone","core/config","core/notification"],(function(_exports,_pubsub,_modal_save_cancel,_str,_templates,_modal_events,_dropzone,config,_notification){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +/** + * Upload module for Google Docs repository. + * + * @module repository_googledocs/upload + * @copyright 2025 Huong Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modal_save_cancel=_interopRequireDefault(_modal_save_cancel),_templates=_interopRequireDefault(_templates),_modal_events=_interopRequireDefault(_modal_events),_dropzone=_interopRequireDefault(_dropzone),config=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(config),_notification=_interopRequireDefault(_notification);let listenersRegistered=!1,droppedFiles=[];const initDropzone=modal=>{const $body=modal.getBody(),dropzoneContainer=$body.find(".repository_googledocs_dropzone_container").get(0),fileListContainer=$body.find("ul.repository_googledocs_files_list").get(0),dz=new _dropzone.default(dropzoneContainer,"*",(files=>{droppedFiles.push(...files);let fileListHTML="";for(let i=0;i"+droppedFiles[i].name+"";fileListContainer.innerHTML=fileListHTML}));(0,_str.getString)("dropfiles","repository").then((label=>{dz.setLabel(label)})),dz.init()},commitFiles=(repoId,contextId,callback,files,modal)=>{const saveButton=modal.getFooter().find('[data-action="save"]'),formData=new FormData;formData.append("action","upload"),formData.append("repo_id",repoId),formData.append("contextid",contextId),formData.append("sesskey",config.sesskey);for(let i=0;i{listenersRegistered||((0,_pubsub.subscribe)("repository_googledocs_upload",(data=>{(data=>{_templates.default.render("repository_googledocs/upload_dialogue",{}).then((function(bodyHtml){return _modal_save_cancel.default.create({title:(0,_str.getString)("upload"),body:bodyHtml,large:!0}).then((function(modal){return modal.getRoot().on(_modal_events.default.shown,(()=>{droppedFiles=[],initDropzone(modal)})),modal.getRoot().on(_modal_events.default.hidden,(()=>{modal.destroy()})),modal.getRoot().on(_modal_events.default.save,(e=>{e.preventDefault(),commitFiles(data.repoId,data.contextId,data.callback,droppedFiles,modal)})),modal.getRoot().on(_modal_events.default.cancel,(()=>{modal.hide()})),modal.show(),modal}))}))})(data)})),listenersRegistered=!0)};_exports.init=()=>{registerEventListeners()}})); + +//# sourceMappingURL=upload.min.js.map \ No newline at end of file diff --git a/public/repository/googledocs/amd/build/upload.min.js.map b/public/repository/googledocs/amd/build/upload.min.js.map new file mode 100644 index 00000000000..2aa8f9e5daf --- /dev/null +++ b/public/repository/googledocs/amd/build/upload.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"upload.min.js","sources":["../src/upload.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Upload module for Google Docs repository.\n *\n * @module repository_googledocs/upload\n * @copyright 2025 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {subscribe} from 'core/pubsub';\nimport SaveCancelModal from 'core/modal_save_cancel';\nimport {getString} from 'core/str';\nimport Templates from 'core/templates';\nimport ModalEvents from 'core/modal_events';\nimport Dropzone from 'core/dropzone';\nimport * as config from 'core/config';\nimport Notification from 'core/notification';\n\nlet listenersRegistered = false;\nlet droppedFiles = [];\n\n/**\n * Open the upload modal.\n *\n * @param {object} data Data passed from the event\n */\nconst openUploadModal = (data) => {\n Templates.render('repository_googledocs/upload_dialogue', {}).then(function(bodyHtml) {\n return SaveCancelModal.create({\n title: getString('upload'),\n body: bodyHtml,\n large: true,\n }).then(function(modal) {\n modal.getRoot().on(ModalEvents.shown, () => {\n droppedFiles = [];\n initDropzone(modal);\n });\n modal.getRoot().on(ModalEvents.hidden, () => {\n modal.destroy();\n });\n modal.getRoot().on(ModalEvents.save, (e) => {\n e.preventDefault();\n commitFiles(data.repoId, data.contextId, data.callback, droppedFiles, modal);\n });\n modal.getRoot().on(ModalEvents.cancel, () => {\n modal.hide();\n });\n modal.show();\n\n return modal;\n });\n });\n};\n\n/**\n * Initialize the dropzone inside the modal.\n *\n * @param {Modal} modal Modal instance\n */\nconst initDropzone = (modal) => {\n const $body = modal.getBody();\n const dropzoneContainer = $body.find('.repository_googledocs_dropzone_container').get(0);\n const fileListContainer = $body.find('ul.repository_googledocs_files_list').get(0);\n const dz = new Dropzone(dropzoneContainer, '*', (files) => {\n droppedFiles.push(...files);\n let fileListHTML = '';\n for (let i = 0; i < droppedFiles.length; i++) {\n fileListHTML += '
  • ' + droppedFiles[i].name + '
  • ';\n }\n fileListContainer.innerHTML = fileListHTML;\n });\n\n getString('dropfiles', 'repository').then((label) => {\n dz.setLabel(label);\n });\n\n dz.init();\n};\n\n/**\n * Upload files to server.\n *\n * @param {Integer} repoId Repository ID\n * @param {Integer} contextId Context ID\n * @param {function} callback Callback function\n * @param {array} files Files to be uploaded\n * @param {Modal} modal Modal instance\n */\nconst commitFiles = (repoId, contextId, callback, files, modal) => {\n const saveButton = modal.getFooter().find('[data-action=\"save\"]');\n const formData = new FormData();\n formData.append('action', 'upload');\n formData.append('repo_id', repoId);\n formData.append('contextid', contextId);\n formData.append('sesskey', config.sesskey);\n for (let i = 0; i < files.length; i++) {\n formData.append(\"files[]\", files[i]);\n }\n const xhr = new XMLHttpRequest();\n xhr.open('POST', config.wwwroot + '/repository/googledocs/repository_ajax.php', false);\n xhr.onload = function() {\n const response = JSON.parse(xhr.responseText);\n if (response.error) {\n saveButton.removeAttr('disabled');\n Notification.alert(\n getString('uploaderror', 'repository'),\n response.error,\n getString('close', 'repository'),\n );\n } else {\n saveButton.removeAttr('disabled');\n modal.hide();\n callback();\n }\n };\n xhr.send(formData);\n};\n\n/**\n * Register events.\n */\nconst registerEventListeners = () => {\n if (!listenersRegistered) {\n subscribe('repository_googledocs_upload', (data) => {\n openUploadModal(data);\n });\n listenersRegistered = true;\n }\n};\n\n/**\n * Initializes the upload module.\n */\nexport const init = () => {\n registerEventListeners();\n};\n"],"names":["listenersRegistered","droppedFiles","initDropzone","modal","$body","getBody","dropzoneContainer","find","get","fileListContainer","dz","Dropzone","files","push","fileListHTML","i","length","name","innerHTML","then","label","setLabel","init","commitFiles","repoId","contextId","callback","saveButton","getFooter","formData","FormData","append","config","sesskey","xhr","XMLHttpRequest","open","wwwroot","onload","response","JSON","parse","responseText","error","removeAttr","alert","hide","send","registerEventListeners","data","render","bodyHtml","SaveCancelModal","create","title","body","large","getRoot","on","ModalEvents","shown","hidden","destroy","save","e","preventDefault","cancel","show","openUploadModal"],"mappings":";;;;;;;k/BAgCIA,qBAAsB,EACtBC,aAAe,SAwCbC,aAAgBC,cACZC,MAAQD,MAAME,UACdC,kBAAoBF,MAAMG,KAAK,6CAA6CC,IAAI,GAChFC,kBAAoBL,MAAMG,KAAK,uCAAuCC,IAAI,GAC1EE,GAAK,IAAIC,kBAASL,kBAAmB,KAAMM,QAC7CX,aAAaY,QAAQD,WACjBE,aAAe,OACd,IAAIC,EAAI,EAAGA,EAAId,aAAae,OAAQD,IACrCD,cAAgB,OAASb,aAAac,GAAGE,KAAO,QAEpDR,kBAAkBS,UAAYJ,mCAGxB,YAAa,cAAcK,MAAMC,QACvCV,GAAGW,SAASD,UAGhBV,GAAGY,QAYDC,YAAc,CAACC,OAAQC,UAAWC,SAAUd,MAAOT,eAC/CwB,WAAaxB,MAAMyB,YAAYrB,KAAK,wBACpCsB,SAAW,IAAIC,SACrBD,SAASE,OAAO,SAAU,UAC1BF,SAASE,OAAO,UAAWP,QAC3BK,SAASE,OAAO,YAAaN,WAC7BI,SAASE,OAAO,UAAWC,OAAOC,aAC7B,IAAIlB,EAAI,EAAGA,EAAIH,MAAMI,OAAQD,IAC9Bc,SAASE,OAAO,UAAWnB,MAAMG,UAE/BmB,IAAM,IAAIC,eAChBD,IAAIE,KAAK,OAAQJ,OAAOK,QAAU,8CAA8C,GAChFH,IAAII,OAAS,iBACHC,SAAWC,KAAKC,MAAMP,IAAIQ,cAC5BH,SAASI,OACThB,WAAWiB,WAAW,kCACTC,OACT,kBAAU,cAAe,cACzBN,SAASI,OACT,kBAAU,QAAS,iBAGvBhB,WAAWiB,WAAW,YACtBzC,MAAM2C,OACNpB,aAGRQ,IAAIa,KAAKlB,WAMPmB,uBAAyB,KACtBhD,4CACS,gCAAiCiD,OAjG1BA,CAAAA,0BACXC,OAAO,wCAAyC,IAAI/B,MAAK,SAASgC,iBACjEC,2BAAgBC,OAAO,CAC1BC,OAAO,kBAAU,UACjBC,KAAMJ,SACNK,OAAO,IACRrC,MAAK,SAAShB,cACbA,MAAMsD,UAAUC,GAAGC,sBAAYC,OAAO,KAClC3D,aAAe,GACfC,aAAaC,UAEjBA,MAAMsD,UAAUC,GAAGC,sBAAYE,QAAQ,KACnC1D,MAAM2D,aAEV3D,MAAMsD,UAAUC,GAAGC,sBAAYI,MAAOC,IAClCA,EAAEC,iBACF1C,YAAY0B,KAAKzB,OAAQyB,KAAKxB,UAAWwB,KAAKvB,SAAUzB,aAAcE,UAE1EA,MAAMsD,UAAUC,GAAGC,sBAAYO,QAAQ,KACnC/D,MAAM2C,UAEV3C,MAAMgE,OAEChE,aA2EPiE,CAAgBnB,SAEpBjD,qBAAsB,kBAOV,KAChBgD"} \ No newline at end of file diff --git a/public/repository/googledocs/amd/src/upload.js b/public/repository/googledocs/amd/src/upload.js new file mode 100644 index 00000000000..0fcabde01f1 --- /dev/null +++ b/public/repository/googledocs/amd/src/upload.js @@ -0,0 +1,150 @@ +// 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 . + +/** + * Upload module for Google Docs repository. + * + * @module repository_googledocs/upload + * @copyright 2025 Huong Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {subscribe} from 'core/pubsub'; +import SaveCancelModal from 'core/modal_save_cancel'; +import {getString} from 'core/str'; +import Templates from 'core/templates'; +import ModalEvents from 'core/modal_events'; +import Dropzone from 'core/dropzone'; +import * as config from 'core/config'; +import Notification from 'core/notification'; + +let listenersRegistered = false; +let droppedFiles = []; + +/** + * Open the upload modal. + * + * @param {object} data Data passed from the event + */ +const openUploadModal = (data) => { + Templates.render('repository_googledocs/upload_dialogue', {}).then(function(bodyHtml) { + return SaveCancelModal.create({ + title: getString('upload'), + body: bodyHtml, + large: true, + }).then(function(modal) { + modal.getRoot().on(ModalEvents.shown, () => { + droppedFiles = []; + initDropzone(modal); + }); + modal.getRoot().on(ModalEvents.hidden, () => { + modal.destroy(); + }); + modal.getRoot().on(ModalEvents.save, (e) => { + e.preventDefault(); + commitFiles(data.repoId, data.contextId, data.callback, droppedFiles, modal); + }); + modal.getRoot().on(ModalEvents.cancel, () => { + modal.hide(); + }); + modal.show(); + + return modal; + }); + }); +}; + +/** + * Initialize the dropzone inside the modal. + * + * @param {Modal} modal Modal instance + */ +const initDropzone = (modal) => { + const $body = modal.getBody(); + const dropzoneContainer = $body.find('.repository_googledocs_dropzone_container').get(0); + const fileListContainer = $body.find('ul.repository_googledocs_files_list').get(0); + const dz = new Dropzone(dropzoneContainer, '*', (files) => { + droppedFiles.push(...files); + let fileListHTML = ''; + for (let i = 0; i < droppedFiles.length; i++) { + fileListHTML += '
  • ' + droppedFiles[i].name + '
  • '; + } + fileListContainer.innerHTML = fileListHTML; + }); + + getString('dropfiles', 'repository').then((label) => { + dz.setLabel(label); + }); + + dz.init(); +}; + +/** + * Upload files to server. + * + * @param {Integer} repoId Repository ID + * @param {Integer} contextId Context ID + * @param {function} callback Callback function + * @param {array} files Files to be uploaded + * @param {Modal} modal Modal instance + */ +const commitFiles = (repoId, contextId, callback, files, modal) => { + const saveButton = modal.getFooter().find('[data-action="save"]'); + const formData = new FormData(); + formData.append('action', 'upload'); + formData.append('repo_id', repoId); + formData.append('contextid', contextId); + formData.append('sesskey', config.sesskey); + for (let i = 0; i < files.length; i++) { + formData.append("files[]", files[i]); + } + const xhr = new XMLHttpRequest(); + xhr.open('POST', config.wwwroot + '/repository/googledocs/repository_ajax.php', false); + xhr.onload = function() { + const response = JSON.parse(xhr.responseText); + if (response.error) { + saveButton.removeAttr('disabled'); + Notification.alert( + getString('uploaderror', 'repository'), + response.error, + getString('close', 'repository'), + ); + } else { + saveButton.removeAttr('disabled'); + modal.hide(); + callback(); + } + }; + xhr.send(formData); +}; + +/** + * Register events. + */ +const registerEventListeners = () => { + if (!listenersRegistered) { + subscribe('repository_googledocs_upload', (data) => { + openUploadModal(data); + }); + listenersRegistered = true; + } +}; + +/** + * Initializes the upload module. + */ +export const init = () => { + registerEventListeners(); +}; diff --git a/public/repository/googledocs/classes/googledocs_content_search.php b/public/repository/googledocs/classes/googledocs_content_search.php index 37b2a3d3ed9..56b7d5a2434 100644 --- a/public/repository/googledocs/classes/googledocs_content_search.php +++ b/public/repository/googledocs/classes/googledocs_content_search.php @@ -48,17 +48,11 @@ class googledocs_content_search extends googledocs_content { 'q' => $q, 'fields' => $fields, 'spaces' => 'drive', + 'supportsAllDrives' => 'true', + 'includeItemsFromAllDrives' => 'true', + 'corpora' => 'allDrives', ]; - // If shared drives exist, include the additional required parameters in order to extend the content search - // into the shared drives area as well. - $response = helper::request($this->service, 'shared_drives_list', []); - if (!empty($response->drives)) { - $params['supportsAllDrives'] = 'true'; - $params['includeItemsFromAllDrives'] = 'true'; - $params['corpora'] = 'allDrives'; - } - // Request the content through the API call. $response = helper::request($this->service, 'list', $params); diff --git a/public/repository/googledocs/classes/local/browser/googledocs_drive_content.php b/public/repository/googledocs/classes/local/browser/googledocs_drive_content.php index d0aa240719a..c23a0a5f494 100644 --- a/public/repository/googledocs/classes/local/browser/googledocs_drive_content.php +++ b/public/repository/googledocs/classes/local/browser/googledocs_drive_content.php @@ -54,18 +54,11 @@ class googledocs_drive_content extends googledocs_content { 'q' => $q, 'fields' => $fields, 'spaces' => 'drive', + 'supportsAllDrives' => 'true', + 'includeItemsFromAllDrives' => 'true', + 'corpora' => 'allDrives', ]; - // Check whether there are any shared drives. - $response = helper::request($this->service, 'shared_drives_list', []); - if (!empty($response->drives)) { - // To be able to include content from shared drives, we need to enable 'supportsAllDrives' and - // 'includeItemsFromAllDrives'. The Google Drive API requires explicit request for inclusion of content from - // shared drives and also a confirmation that the application is designed to handle files on shared drives. - $params['supportsAllDrives'] = 'true'; - $params['includeItemsFromAllDrives'] = 'true'; - } - // Request the content through the API call. $response = helper::request($this->service, 'list', $params); diff --git a/public/repository/googledocs/classes/local/browser/googledocs_root_content.php b/public/repository/googledocs/classes/local/browser/googledocs_root_content.php index 4272a949efa..84f410977e1 100644 --- a/public/repository/googledocs/classes/local/browser/googledocs_root_content.php +++ b/public/repository/googledocs/classes/local/browser/googledocs_root_content.php @@ -17,7 +17,6 @@ namespace repository_googledocs\local\browser; use repository_googledocs\googledocs_content; -use repository_googledocs\helper; /** * Utility class for browsing the content within the googledocs repository root. @@ -41,7 +40,7 @@ class googledocs_root_content extends googledocs_content { */ protected function get_contents(string $query): array { // Add 'My drive' folder into the displayed contents. - $contents = [ + return [ (object)[ 'id' => \repository_googledocs::MY_DRIVE_ROOT_ID, 'name' => get_string('mydrive', 'repository_googledocs'), @@ -49,19 +48,5 @@ class googledocs_root_content extends googledocs_content { 'modifiedTime' => '', ], ]; - - // If shared drives exists, include 'Shared drives' folder to the displayed contents. - $response = helper::request($this->service, 'shared_drives_list', []); - - if (!empty($response->drives)) { - $contents[] = (object)[ - 'id' => \repository_googledocs::SHARED_DRIVES_ROOT_ID, - 'name' => get_string('shareddrives', 'repository_googledocs'), - 'mimeType' => 'application/vnd.google-apps.folder', - 'modifiedTime' => '', - ]; - } - - return $contents; } } diff --git a/public/repository/googledocs/classes/rest.php b/public/repository/googledocs/classes/rest.php index cc42ac38ea3..2a25fd2f003 100644 --- a/public/repository/googledocs/classes/rest.php +++ b/public/repository/googledocs/classes/rest.php @@ -54,43 +54,37 @@ class rest extends \core\oauth2\rest { 'spaces' => PARAM_RAW, 'supportsAllDrives' => PARAM_RAW, 'includeItemsFromAllDrives' => PARAM_RAW, - 'corpora' => PARAM_RAW + 'corpora' => PARAM_RAW, ], - 'response' => 'json' + 'response' => 'json', ], 'get' => [ 'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}', 'method' => 'get', 'args' => [ 'fields' => PARAM_RAW, - 'fileid' => PARAM_RAW + 'fileid' => PARAM_RAW, + 'supportsAllDrives' => PARAM_RAW, ], - 'response' => 'json' - ], - 'copy' => [ - 'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}/copy', - 'method' => 'post', - 'args' => [ - 'fields' => PARAM_RAW, - 'fileid' => PARAM_RAW - ], - 'response' => 'json' + 'response' => 'json', ], 'delete' => [ 'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}', 'method' => 'delete', 'args' => [ - 'fileid' => PARAM_RAW + 'fileid' => PARAM_RAW, + 'supportsAllDrives' => PARAM_RAW, ], - 'response' => 'json' + 'response' => 'json', ], 'create' => [ 'endpoint' => 'https://www.googleapis.com/drive/v3/files', 'method' => 'post', 'args' => [ - 'fields' => PARAM_RAW + 'fields' => PARAM_RAW, + 'supportsAllDrives' => PARAM_RAW, ], - 'response' => 'json' + 'response' => 'json', ], 'update' => [ 'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}', @@ -99,9 +93,10 @@ class rest extends \core\oauth2\rest { 'fileid' => PARAM_RAW, 'fields' => PARAM_RAW, 'addParents' => PARAM_RAW, - 'removeParents' => PARAM_RAW + 'removeParents' => PARAM_RAW, + 'supportsAllDrives' => PARAM_RAW, ], - 'response' => 'json' + 'response' => 'json', ], 'create_permission' => [ 'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}/permissions', @@ -111,8 +106,9 @@ class rest extends \core\oauth2\rest { 'emailMessage' => PARAM_RAW, 'sendNotificationEmail' => PARAM_RAW, 'transferOwnership' => PARAM_RAW, + 'supportsAllDrives' => PARAM_RAW, ], - 'response' => 'json' + 'response' => 'json', ], 'update_permission' => [ 'endpoint' => 'https://www.googleapis.com/drive/v3/files/{fileid}/permissions/{permissionid}', @@ -123,17 +119,25 @@ class rest extends \core\oauth2\rest { 'emailMessage' => PARAM_RAW, 'sendNotificationEmail' => PARAM_RAW, 'transferOwnership' => PARAM_RAW, + 'supportsAllDrives' => PARAM_RAW, ], - 'response' => 'json' + 'response' => 'json', ], - 'shared_drives_list' => [ - 'endpoint' => 'https://www.googleapis.com/drive/v3/drives', - 'method' => 'get', + 'upload' => [ + 'endpoint' => 'https://www.googleapis.com/upload/drive/v3/files', + 'method' => 'post', 'args' => [ - 'pageSize' => PARAM_INT, - 'pageToken' => PARAM_RAW, - 'q' => PARAM_RAW, - 'useDomainAdminAccess' => PARAM_RAW, + 'uploadType' => PARAM_RAW, + 'fields' => PARAM_RAW, + 'supportsAllDrives' => PARAM_RAW, + ], + 'response' => 'headers', + ], + 'upload_content' => [ + 'endpoint' => '{uploadurl}', + 'method' => 'put', + 'args' => [ + 'uploadurl' => PARAM_URL, ], 'response' => 'json', ], diff --git a/public/repository/googledocs/lib.php b/public/repository/googledocs/lib.php index 0c810ae71b4..a1a078e4c62 100644 --- a/public/repository/googledocs/lib.php +++ b/public/repository/googledocs/lib.php @@ -27,6 +27,7 @@ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/repository/lib.php'); require_once($CFG->libdir . '/filebrowser/file_browser.php'); +require_once($CFG->libdir . '/google/lib.php'); use repository_googledocs\helper; use repository_googledocs\googledocs_content_search; @@ -53,11 +54,6 @@ class repository_googledocs extends repository { */ private $issuer = null; - /** - * Additional scopes required for drive. - */ - const SCOPES = 'https://www.googleapis.com/auth/drive'; - /** @var string Defines the path node identifier for the repository root. */ const REPOSITORY_ROOT_ID = 'repository_root'; @@ -80,6 +76,7 @@ class repository_googledocs extends repository { * @return void */ public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array(), $readonly = 0) { + global $PAGE; parent::__construct($repositoryid, $context, $options, $readonly = 0); try { @@ -91,6 +88,8 @@ class repository_googledocs extends repository { if ($this->issuer && !$this->issuer->get('enabled')) { $this->disabled = true; } + + $PAGE->requires->js_call_amd('repository_googledocs/upload', 'init'); } /** @@ -99,7 +98,7 @@ class repository_googledocs extends repository { * @param moodle_url $overrideurl - Use this url instead of the repo callback. * @return \core\oauth2\client */ - protected function get_user_oauth_client($overrideurl = false) { + public function get_user_oauth_client($overrideurl = false) { if ($this->client) { return $this->client; } @@ -112,7 +111,7 @@ class repository_googledocs extends repository { $returnurl->param('sesskey', sesskey()); } - $this->client = \core\oauth2\api::get_user_oauth_client($this->issuer, $returnurl, self::SCOPES, true); + $this->client = \core\oauth2\api::get_user_oauth_client($this->issuer, $returnurl, Google_Service_Drive::DRIVE_FILE, true); return $this->client; } @@ -270,6 +269,13 @@ class repository_googledocs extends repository { $path = helper::build_node_path('repository_root', $pluginname); } + // Make sure the current path points to "My Drive" before listing files. + $mydrive = get_string('mydrive', 'repository_googledocs'); + $mydrivepath = helper::build_node_path('root', $mydrive); + if (!str_contains($path, $mydrivepath)) { + $path = $path . '/' . $mydrivepath; + } + if (!$this->issuer->get('enabled')) { // Empty list of files for disabled repository. return [ @@ -306,7 +312,11 @@ class repository_googledocs extends repository { 'defaultreturntype' => $this->default_returntype(), 'path' => $contentobj->get_navigation(), 'list' => $contentobj->get_content_nodes($query, [$this, 'filter']), - 'manage' => 'https://drive.google.com/', + 'uploadfile' => true, + 'uploadevent' => 'repository_googledocs_upload', + 'repo_id' => $this->id, + 'contextid' => $this->context->id, + 'sesskey' => sesskey(), ]; } @@ -708,10 +718,10 @@ class repository_googledocs extends repository { $storedfile->get_filename(), $forcedownload); $url->param('sesskey', sesskey()); - $param = ($options['embed'] == true) ? false : $url; + $param = (isset($options['embed']) && $options['embed'] == true) ? false : $url; $userauth = $this->get_user_oauth_client($param); if (!$userauth->is_logged_in()) { - if ($options['embed'] == true) { + if (isset($options['embed']) && $options['embed'] == true) { // Due to Same-origin policy, we cannot redirect to googledocs login page. // If the requested file is embed and the user is not logged in, add option to log in using a popup. $this->print_login_popup(['style' => 'margin-top: 250px']); @@ -798,7 +808,7 @@ class repository_googledocs extends repository { * @return stdClass */ protected function get_file_summary(\repository_googledocs\rest $client, $fileid) { - $fields = "id,name,owners,parents"; + $fields = "id,name,owners,parents,webContentLink,webViewLink"; $params = [ 'fileid' => $fileid, 'fields' => $fields @@ -806,35 +816,6 @@ class repository_googledocs extends repository { return $client->call('get', $params); } - /** - * Copy a file and return the new file details. A side effect of the copy - * is that the owner will be the account authenticated with this oauth client. - * - * @param \repository_googledocs\rest $client Authenticated client. - * @param string $fileid The file we are copying. - * @param string $name The original filename (don't change it). - * - * @return stdClass file details. - */ - protected function copy_file(\repository_googledocs\rest $client, $fileid, $name) { - $fields = "id,name,mimeType,webContentLink,webViewLink,size,thumbnailLink,iconLink"; - $params = [ - 'fileid' => $fileid, - 'fields' => $fields, - ]; - // Keep the original name (don't put copy at the end of it). - $copyinfo = []; - if (!empty($name)) { - $copyinfo = [ 'name' => $name ]; - } - $fileinfo = $client->call('copy', $params, json_encode($copyinfo)); - if (empty($fileinfo->id)) { - $details = 'Cannot copy file:' . $fileid; - throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details); - } - return $fileinfo; - } - /** * Add a writer to the permissions on the file (temporary). * @@ -854,7 +835,7 @@ class repository_googledocs extends repository { 'type' => 'user', 'expirationTime' => $expires->format(DateTime::RFC3339) ]; - $params = ['fileid' => $fileid, 'sendNotificationEmail' => 'false']; + $params = ['fileid' => $fileid, 'sendNotificationEmail' => 'false', 'supportsAllDrives' => 'true']; $response = $client->call('create_permission', $params, json_encode($updateeditor)); if (empty($response->id)) { $details = 'Cannot add user ' . $email . ' as a writer for document: ' . $fileid; @@ -878,7 +859,7 @@ class repository_googledocs extends repository { 'role' => 'writer', 'type' => 'user' ]; - $params = ['fileid' => $fileid, 'sendNotificationEmail' => 'false']; + $params = ['fileid' => $fileid, 'sendNotificationEmail' => 'false', 'supportsAllDrives' => 'true']; $response = $client->call('create_permission', $params, json_encode($updateeditor)); if (empty($response->id)) { $details = 'Cannot add user ' . $email . ' as a writer for document: ' . $fileid; @@ -944,7 +925,7 @@ class repository_googledocs extends repository { 'role' => 'reader', 'allowFileDiscovery' => 'false' ]; - $params = ['fileid' => $fileid]; + $params = ['fileid' => $fileid, 'supportsAllDrives' => 'true']; $response = $client->call('create_permission', $params, json_encode($updateread)); if (empty($response->id) || $response->id != 'anyoneWithLink') { $details = 'Cannot update link sharing for the document: ' . $fileid; @@ -1008,7 +989,6 @@ class repository_googledocs extends repository { $userservice = new repository_googledocs\rest($userauth); $systemservice = new repository_googledocs\rest($systemauth); - // Add Moodle as writer. $this->add_writer_to_file($userservice, $source->id, $systemuseremail); // Now move it to a sensible folder. @@ -1063,8 +1043,25 @@ class repository_googledocs extends repository { } } - // Copy the file so we get a snapshot file owned by Moodle. - $newsource = $this->copy_file($systemservice, $source->id, $source->name); + $originalfile = $this->get_file_summary($userservice, $source->id); + // Use the user service to download the file. + $downloadedfile = $this->download_file( + $userservice, + $source->id, + $originalfile->name, + ); + // Upload the user file to the system drive. + $uploaded = $this->upload_file( + $systemservice, + $downloadedfile['path'], + $downloadedfile['newfilename'], + $source->exportformat, + $parentid, + ); + // Add the original file owner as a writer to the file. + $this->add_writer_to_file($systemservice, $uploaded->id, $originalfile->owners[0]->emailAddress); + $newsource = $this->get_file_summary($systemservice, $uploaded->id); + // Move the copied file to the correct folder. $this->move_file_from_root_to_folder($systemservice, $newsource->id, $parentid); @@ -1084,6 +1081,81 @@ class repository_googledocs extends repository { return $reference; } + /** + * Uploads a file to Google Docs using the provided REST client. + * + * @param \repository_googledocs\rest $client The REST client for Google Docs API communication. + * @param string $filepath The local path to the file to be uploaded. + * @param string $filename The name to assign to the uploaded file. + * @param string $exportformat The export format for the file (e.g., 'pdf', 'docx'). + * @param string $parentid The ID of the parent folder in Google Drive where the file will be uploaded. + * @return stdClass|coding_exception Returns the response from the Google Docs API after uploading the file. + */ + public function upload_file( + \repository_googledocs\rest $client, + string $filepath, + string $filename, + string $exportformat, + string $parentid + ): stdClass { + $fileinfo = [ + 'name' => $filename, + 'mimeType' => $exportformat, + 'parents' => [$parentid], // We will move it later. + ]; + $params = [ + 'supportsAllDrives' => 'true', // Support shared drives. + 'uploadType' => 'resumable', // Use resumable upload. + ]; + + $headers = $client->call('upload', $params, json_encode($fileinfo)); + + $uploadurl = ''; + // Google returns a location header with the location for the upload. + foreach ($headers as $header) { + if (stripos($header, 'Location:') === 0) { + $uploadurl = trim(substr($header, strpos($header, ':') + 1)); + } + } + + $params = ['uploadurl' => $uploadurl]; + return $client->call('upload_content', $params, file_get_contents($filepath), mime_content_type($filepath)); + } + + /** + * Downloads a file from Google Docs using the provided user service. + * + * @param \repository_googledocs\rest $userservice The user service instance for Google Docs REST API. + * @param string $fileid The ID of the file to download. + * @param string $originalfilename The file original name + * @return array|repository_exception The downloaded file content or relevant response. + */ + protected function download_file( + \repository_googledocs\rest $userservice, + string $fileid, + string $originalfilename + ): array|repository_exception { + global $CFG; + + $client = $this->get_user_oauth_client(); + $base = 'https://www.googleapis.com/drive/v3'; + $params = ['alt' => 'media']; + $sourceurl = new moodle_url($base . '/files/' . $fileid, $params); + + $path = $this->prepare_file($originalfilename); + $options = ['filepath' => $path, 'timeout' => $CFG->repositorygetfiletimeout, 'followlocation' => true, 'maxredirs' => 5]; + $success = $client->download_one($sourceurl->out(false), null, $options); + if ($success) { + @chmod($path, $CFG->filepermissions); + return [ + 'path' => $path, + 'newfilename' => $originalfilename, + ]; + } + + throw new repository_exception('cannotdownload', 'repository'); + } + /** * Get human readable file info from a the reference. * @@ -1223,7 +1295,7 @@ class repository_googledocs extends repository { */ function repository_googledocs_oauth2_system_scopes(\core\oauth2\issuer $issuer) { if ($issuer->get('id') == get_config('googledocs', 'issuerid')) { - return 'https://www.googleapis.com/auth/drive'; + return Google_Service_Drive::DRIVE_FILE; } return ''; } diff --git a/public/repository/googledocs/repository_ajax.php b/public/repository/googledocs/repository_ajax.php new file mode 100644 index 00000000000..55e53c4e4e0 --- /dev/null +++ b/public/repository/googledocs/repository_ajax.php @@ -0,0 +1,85 @@ +. + +/** + * The Web service script that is called from the filepicker upload. + * + * @package repository_googledocs + * @copyright 2025 Raquel Ortega + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('AJAX_SCRIPT', true); + +require(__DIR__ . '/../../config.php'); +require_once($CFG->dirroot . '/lib/filelib.php'); +require_once($CFG->dirroot . '/repository/lib.php'); + + +$action = required_param('action', PARAM_ALPHA); +$repoid = optional_param('repo_id', 0, PARAM_INT); +$contextid = optional_param('contextid', 0, PARAM_INT); +$itemid = optional_param('itemid', 0, PARAM_INT); + +require_login(); +if (!confirm_sesskey()) { + die(json_encode(['error' => get_string('invalidsesskey', 'error')])); +} + +$context = context::instance_by_id($contextid, true); + +$repo = repository::get_repository_by_id($repoid, $contextid); +if (!$repo) { + die(json_encode(['error' => get_string('invalidrepositoryid', 'repository')])); +} +// Check permissions. +$repo->check_capability(); +$repo->check_login(); + +switch ($action) { + case 'upload': + $files = $_FILES['files']; + $tmp = make_request_directory(); + $savedfiles = []; + if (is_array($files['name'])) { + // Multiple files. + foreach ($files['name'] as $idx => $name) { + $dest = $tmp . '/' . basename($name); + move_uploaded_file($files['tmp_name'][$idx], $dest); + $savedfiles[] = $dest; + } + } else { + // Single file. + $dest = $tmp . '/' . basename($files['name']); + move_uploaded_file($files['tmp_name'], $dest); + $savedfiles[] = $dest; + } + + // Upload the file to Google Drive repository. + $ha = new repository_googledocs($repoid, $context); + $userauth = $ha->get_user_oauth_client(); + $userservice = new repository_googledocs\rest($userauth); + foreach ($savedfiles as $file) { + $filename = basename($file); + $ha->upload_file($userservice, $file, $filename, 'download', "root"); + } + + echo json_encode(['success' => true]); + break; + + default: + echo json_encode(['error' => get_string('error', 'repository')]); +} diff --git a/public/repository/googledocs/templates/upload_dialogue.mustache b/public/repository/googledocs/templates/upload_dialogue.mustache new file mode 100644 index 00000000000..17cac5ada52 --- /dev/null +++ b/public/repository/googledocs/templates/upload_dialogue.mustache @@ -0,0 +1,39 @@ +{{! + 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 repository_googledocs/upload_dialogue + + Google repository upload dialogue. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Example context (json): + { + + } +}} +
    +
    +
    + {{#str}} attachedfiles, repository {{/str}} +
    +
      +
      diff --git a/public/repository/googledocs/tests/googledocs_search_content_test.php b/public/repository/googledocs/tests/googledocs_search_content_test.php index 3c60a23be3c..39bf8fb2a1e 100644 --- a/public/repository/googledocs/tests/googledocs_search_content_test.php +++ b/public/repository/googledocs/tests/googledocs_search_content_test.php @@ -34,16 +34,21 @@ final class googledocs_search_content_test extends \googledocs_content_testcase /** * Test get_content_nodes(). * + * @covers \repository_googledocs\googledocs_content_search * @dataProvider get_content_nodes_provider * @param string $query The query string * @param bool $sortcontent Whether the contents should be sorted in alphabetical order * @param array $filterextensions The array containing file extensions that should be disallowed (filtered) - * @param array $shareddrives The array containing the existing shared drives * @param array $searccontents The array containing the fetched google drive contents that match the search criteria * @param array $expected The expected array which contains the generated repository content nodes */ - public function test_get_content_nodes(string $query, bool $sortcontent, array $filterextensions, - array $shareddrives, array $searccontents, array $expected): void { + public function test_get_content_nodes( + string $query, + bool $sortcontent, + array $filterextensions, + array $searccontents, + array $expected + ): void { // Mock the service object. $servicemock = $this->createMock(rest::class); @@ -52,45 +57,27 @@ final class googledocs_search_content_test extends \googledocs_content_testcase 'q' => "fullText contains '" . str_replace("'", "\'", $query) . "' AND trashed = false", 'fields' => 'files(id,name,mimeType,webContentLink,webViewLink,fileExtension,modifiedTime,size,iconLink)', 'spaces' => 'drive', + 'supportsAllDrives' => 'true', + 'includeItemsFromAllDrives' => 'true', + 'corpora' => 'allDrives', ]; - if (!empty($shareddrives)) { - $searchparams['supportsAllDrives'] = 'true'; - $searchparams['includeItemsFromAllDrives'] = 'true'; - $searchparams['corpora'] = 'allDrives'; - } - - // Assert that the call() method is being called twice with the given arguments consecutively. In the first - // instance it is being called to fetch the shared drives (shared_drives_list), while in the second instance - // to fetch the relevant drive contents (list) that match the search criteria. Also, define the returned - // data objects by these calls. - $callinvocations = $this->exactly(2); + // Assert that the call() method is being called to fetch the relevant drive contents (list), + // that match the search criteria. Also, define the returned data objects by these calls. + // This simulates the more restricted drive.file scope behavior. + $callinvocations = $this->exactly(1); $servicemock->expects($callinvocations) ->method('call') ->willReturnCallback(function(string $method, array $params) use ( - $callinvocations, - $shareddrives, $searccontents, $searchparams, ) { - switch (self::getInvocationCount($callinvocations)) { - case 1: - $this->assertEquals('shared_drives_list', $method); + $this->assertEquals('list', $method); + $this->assertEquals($searchparams, $params); - $this->assertEmpty($params); - return (object) [ - 'kind' => 'drive#driveList', - 'nextPageToken' => 'd838181f30b0f5', - 'drives' => $shareddrives, - ]; - case 2: - $this->assertEquals('list', $method); - $this->assertEquals($searchparams, $params); - - return (object) [ - 'files' => $searccontents, - ]; - } + return (object) [ + 'files' => $searccontents, + ]; }); // Construct the node path. @@ -118,14 +105,11 @@ final class googledocs_search_content_test extends \googledocs_content_testcase $searchforstring = get_string('searchfor', 'repository_googledocs'); return [ - 'Folders and files match the search criteria; shared drives exist; ordering applied.' => + 'Folders and files match the search criteria; ordering applied.' => [ 'test', true, [], - [ - self::create_google_drive_shared_drive_object('d85b21c0f86cb5', 'Shared Drive 1'), - ], [ self::create_google_drive_file_object('d85b21c0f86cb0', 'Test file 3.pdf', 'application/pdf', 'pdf', '1000', '', @@ -148,12 +132,11 @@ final class googledocs_search_content_test extends \googledocs_content_testcase 'https://drive.google.com/uc?id=d85b21c0f86cb0&export=download', 'download'), ], ], - 'Only folders match the search criteria; shared drives do not exist; ordering not applied.' => + 'Only folders match the search criteria; ordering not applied.' => [ 'testing', false, [], - [], [ self::create_google_drive_folder_object('0c4ad262c65333', 'Testing folder 3'), self::create_google_drive_folder_object('d85b21c0f86cb0', 'Testing folder 1'), @@ -168,14 +151,11 @@ final class googledocs_search_content_test extends \googledocs_content_testcase "{$rootid}|Google+Drive/{$searchnodeid}|" . urlencode("{$searchforstring} 'testing'")), ], ], - 'Only files match the search criteria; shared drives exist; ordering not applied; filter .doc and .txt.' => + 'Only files match the search criteria; ordering not applied; filter .doc and .txt.' => [ 'root', false, ['doc', 'txt'], - [ - self::create_google_drive_shared_drive_object('d85b21c0f86cb5', 'Shared Drive 1'), - ], [ self::create_google_drive_file_object('d85b21c0f86cb0', 'Testing file 3.pdf', 'application/pdf', 'pdf', '1000'), @@ -190,14 +170,13 @@ final class googledocs_search_content_test extends \googledocs_content_testcase 'https://googleusercontent.com/type/application/pdf', '', 'download'), ], ], - 'No content that matches the search criteria; shared drives do not exist.' => + 'No content that matches the search criteria.' => [ 'root', false, [], [], [], - [], ], ]; } diff --git a/public/repository/googledocs/tests/local/browser/googledocs_drive_content_test.php b/public/repository/googledocs/tests/local/browser/googledocs_drive_content_test.php index 8dadb1b37d9..9258a73b408 100644 --- a/public/repository/googledocs/tests/local/browser/googledocs_drive_content_test.php +++ b/public/repository/googledocs/tests/local/browser/googledocs_drive_content_test.php @@ -16,6 +16,8 @@ namespace repository_googledocs\local\browser; +use Google_Service_Drive; + defined('MOODLE_INTERNAL') || die(); global $CFG; @@ -53,46 +55,67 @@ final class googledocs_drive_content_test extends \googledocs_content_testcase { 'q' => "'" . str_replace("'", "\'", $query) . "' in parents AND trashed = false", 'fields' => 'files(id,name,mimeType,webContentLink,webViewLink,fileExtension,modifiedTime,size,iconLink)', 'spaces' => 'drive', + 'supportsAllDrives' => 'true', + 'includeItemsFromAllDrives' => 'true', + 'corpora' => 'allDrives', ]; - if (!empty($shareddrives)) { - $listparams['supportsAllDrives'] = 'true'; - $listparams['includeItemsFromAllDrives'] = 'true'; + if (!$this->shared_drives_supported()) { + $callinvocations = $this->exactly(1); + $servicemock->expects($callinvocations) + ->method('call') + ->willReturnCallback(function ( + string $method, + array $params + ) use ( + $listparams, + $drivecontents, + ) { + $this->assertEquals('list', $method); + $this->assertEquals($listparams, $params); + + return (object) [ + 'files' => $drivecontents, + ]; + }); + } else { + // Assert that the call() method is being called twice with the given arguments consecutively. In the first + // instance it is being called to fetch the shared drives (shared_drives_list), while in the second instance + // to fetch the relevant drive contents (list). Also, define the returned data objects by these calls. + $callinvocations = $this->exactly(2); + $servicemock->expects($callinvocations) + ->method('call') + ->willReturnCallback(function ( + string $method, + array $params + ) use ( + $callinvocations, + $shareddrives, + $listparams, + $drivecontents, + ) { + switch (self::getInvocationCount($callinvocations)) { + case 1: + $this->assertEquals('shared_drives_list', $method); + $this->assertEquals([], $params); + + return (object) [ + 'kind' => 'drive#driveList', + 'nextPageToken' => 'd838181f30b0f5', + 'drives' => $shareddrives, + ]; + case 2: + $this->assertEquals('list', $method); + $this->assertEquals($listparams, $params); + return (object)[ + 'files' => $drivecontents, + ]; + default: + $this->fail('Unexpected call to the call() method.'); + } + }); } - // Assert that the call() method is being called twice with the given arguments consecutively. In the first - // instance it is being called to fetch the shared drives (shared_drives_list), while in the second instance - // to fetch the relevant drive contents (list). Also, define the returned data objects by these calls. - $callinvocations = $this->exactly(2); - $servicemock->expects($callinvocations) - ->method('call') - ->willReturnCallback(function(string $method, array $params) use ( - $callinvocations, - $shareddrives, - $listparams, - $drivecontents, - ) { - switch (self::getInvocationCount($callinvocations)) { - case 1: - $this->assertEquals('shared_drives_list', $method); - $this->assertEquals([], $params); - - return (object) [ - 'kind' => 'drive#driveList', - 'nextPageToken' => 'd838181f30b0f5', - 'drives' => $shareddrives, - ]; - case 2: - $this->assertEquals('list', $method); - $this->assertEquals($listparams, $params); - return (object)[ - 'files' => $drivecontents, - ]; - default: - $this->fail('Unexpected call to the call() method.'); - } - }); - // Set the disallowed file types (extensions). $this->disallowedextensions = $filterextensions; $drivebrowser = new googledocs_drive_content($servicemock, $path, $sortcontent); @@ -272,4 +295,16 @@ final class googledocs_drive_content_test extends \googledocs_content_testcase { ], ]; } + + /** + * Determines whether shared drives are supported under the current Google Drive scope. + * + * @return bool + */ + private function shared_drives_supported(): bool { + $scopes = Google_Service_Drive::DRIVE_FILE; + + // Full access is needed for shared drives (not just drive.file). + return str_contains($scopes, 'auth/drive') && !str_contains($scopes, 'auth/drive.file'); + } } diff --git a/public/repository/googledocs/tests/local/browser/googledocs_root_content_test.php b/public/repository/googledocs/tests/local/browser/googledocs_root_content_test.php index 462bcb60320..32b9e178c3e 100644 --- a/public/repository/googledocs/tests/local/browser/googledocs_root_content_test.php +++ b/public/repository/googledocs/tests/local/browser/googledocs_root_content_test.php @@ -16,6 +16,8 @@ namespace repository_googledocs\local\browser; +use Google_Service_Drive; + defined('MOODLE_INTERNAL') || die(); global $CFG; @@ -36,24 +38,43 @@ final class googledocs_root_content_test extends \googledocs_content_testcase { * @dataProvider get_content_nodes_provider * @param array $shareddrives The array containing the existing shared drives * @param array $expected The expected array which contains the generated repository content nodes + * @param bool $expectshared Whether shared drives should be tested + * @covers \repository_googledocs */ - public function test_get_content_nodes(array $shareddrives, array $expected): void { + public function test_get_content_nodes(array $shareddrives, array $expected, bool $expectshared): void { + $scopessupportshared = $this->shared_drives_supported(); + + if ($expectshared && !$scopessupportshared) { + $this->markTestSkipped('Shared drives not supported in current OAuth scope.'); + } + // Mock the service object. $servicemock = $this->createMock(\repository_googledocs\rest::class); - // Assert that the call() method is being called only once with the given arguments to fetch the existing - // shared drives. Define the returned data object by this call. - $servicemock->expects($this->once()) - ->method('call') - ->with('shared_drives_list', []) - ->willReturn((object)[ - 'kind' => 'drive#driveList', - 'nextPageToken' => 'd838181f30b0f5', - 'drives' => $shareddrives, - ]); + if ($expectshared && $scopessupportshared) { + // Expect shared drives API to be called. + // Assert that the call() method is being called only once with the given arguments to fetch the existing + // shared drives. Define the returned data object by this call. + $servicemock->expects($this->once()) + ->method('call') + ->with('shared_drives_list', []) + ->willReturn((object)[ + 'kind' => 'drive#driveList', + 'nextPageToken' => 'd838181f30b0f5', + 'drives' => $shareddrives, + ]); + } else { + // If shared drives are not expected or not supported, call() should not be invoked. + $servicemock->expects($this->never()) + ->method('call'); + } - $rootbrowser = new googledocs_root_content($servicemock, - \repository_googledocs::REPOSITORY_ROOT_ID . '|Google+Drive', false); + $showshared = $expectshared && $scopessupportshared; + $rootbrowser = new googledocs_root_content( + $servicemock, + \repository_googledocs::REPOSITORY_ROOT_ID . '|Google+Drive', + $showshared + ); $contentnodes = $rootbrowser->get_content_nodes('', [$this, 'filter']); // Assert that the returned array of repository content nodes is equal to the expected one. @@ -84,6 +105,7 @@ final class googledocs_root_content_test extends \googledocs_content_testcase { get_string('shareddrives', 'repository_googledocs'), "{$rootid}|Google+Drive"), ], + true, // Expect shared drives. ], 'Shared drives do not exist.' => [ [], @@ -92,7 +114,20 @@ final class googledocs_root_content_test extends \googledocs_content_testcase { get_string('mydrive', 'repository_googledocs'), "{$rootid}|Google+Drive"), ], + false, // Do not expect shared drives. ], ]; } + + /** + * Determines whether shared drives are supported under the current Google Drive scope. + * + * @return bool + */ + private function shared_drives_supported(): bool { + $scopes = Google_Service_Drive::DRIVE_FILE; + + // Full access is needed for shared drives (not just drive.file). + return str_contains($scopes, 'auth/drive') && !str_contains($scopes, 'auth/drive.file'); + } }