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}}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');
+ }
}