From 962300597f5e7c67099fa2ae314f19da2a07e7ae Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Mon, 22 Sep 2025 17:54:48 +0700 Subject: [PATCH] MDL-80447 repository_googledocs: Switch to use core/pubsub --- public/lang/en/repository.php | 2 - public/repository/filepicker.js | 229 ++---------------- .../googledocs/amd/build/upload.min.js | 10 + .../googledocs/amd/build/upload.min.js.map | 1 + .../repository/googledocs/amd/src/upload.js | 150 ++++++++++++ public/repository/googledocs/lib.php | 51 ++-- .../repository/googledocs/repository_ajax.php | 66 ++--- .../templates/upload_dialogue.mustache | 39 +++ 8 files changed, 255 insertions(+), 293 deletions(-) create mode 100644 public/repository/googledocs/amd/build/upload.min.js create mode 100644 public/repository/googledocs/amd/build/upload.min.js.map create mode 100644 public/repository/googledocs/amd/src/upload.js create mode 100644 public/repository/googledocs/templates/upload_dialogue.mustache diff --git a/public/lang/en/repository.php b/public/lang/en/repository.php index 0d3309f64b4..1f0f9d73635 100644 --- a/public/lang/en/repository.php +++ b/public/lang/en/repository.php @@ -117,7 +117,6 @@ $string['errordoublereference'] = 'Unable to overwrite file with a link because $string['errornotyourfile'] = 'You can only pick files which you added.'; $string['erroruniquename'] = 'Repository instance name should be unique'; $string['errorpostmaxsize'] = 'The file you tried to upload is too large for the server to process.'; -$string['erroruploadfailed'] = 'File upload failed. Please check your files and try again.'; $string['errorwhilecommunicatingwith'] = 'Error while communicating with the repository \'{$a}\'.'; $string['errorwhiledownload'] = 'An error occurred while downloading the file: {$a}'; $string['existingrepository'] = 'This repository already exists'; @@ -246,7 +245,6 @@ $string['typenotvisible'] = 'Type not visible'; $string['unknownoriginal'] = 'Unknown'; $string['upload'] = 'Upload this file'; $string['uploaderror'] = 'File upload error'; -$string['uploadfilesdrop'] = 'Drag and drop a file to upload, or click to select'; $string['uploading'] = 'Uploading...'; $string['uploadsucc'] = 'The file has been uploaded successfully'; $string['unknownsource'] = 'Unknown source'; diff --git a/public/repository/filepicker.js b/public/repository/filepicker.js index 1a705de057d..4db058a35d5 100644 --- a/public/repository/filepicker.js +++ b/public/repository/filepicker.js @@ -1633,7 +1633,7 @@ M.core_filepicker.init = function(Y, options) { 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.uploadurl = data.uploadurl ? data.uploadurl : 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(); @@ -2005,217 +2005,26 @@ M.core_filepicker.init = function(Y, options) { managelnk.simulate('click') }); - // New Upload functionality. - toolbar.one('.fp-tb-uploadfile').one('a,button').on('click', function(e) { - e.preventDefault(); - var repoId = this.active_repo.id; - var contextId = this.options.context.id; - var draftidRef = {value: null}; // Store draftid between uploads. - var uploadurl = this.active_repo.uploadurl; - - require([ - 'core/modal_factory', - 'core/modal_events', - 'core/dropzone', - 'core/notification', - 'core/str', - 'core/templates' - ], - function(ModalFactory, ModalEvents, DropZone, Notification, Str, Templates) { - // Create a Modal box with a dropzone where files can be uploaded. - function openUploadModal() { - Templates.render('core/dropzone', {}).then(function(bodyHtml) { - ModalFactory.create({ - type: ModalFactory.types.SAVE_CANCEL, - title: Str.get_string('upload'), - body: bodyHtml, - large: true - }).then(function(modal) { - modal.getRoot().on(ModalEvents.shown, function() { - initDropzone(modal); - }); - modal.getRoot().on(ModalEvents.hidden, function() { - modal.destroy(); - }); - modal.getRoot().on(ModalEvents.save, function(ev) { - commitFiles(ev, modal, toolbar); - }); - modal.getRoot().on(ModalEvents.cancel, function() { - modal.hide(); - }); - modal.show(); - }); - }); - } - openUploadModal(); - - // Create dropzone inside the modal. - function initDropzone(modal) { - const $body = modal.getBody(); - - const dropzoneContainer = $body.find('.dropzone-container').get(0); - const dz = new DropZone(dropzoneContainer, '*', function(files) { - handleDroppedFiles(files, modal); - }); - - // Asynchronously load and set the label. - Str.get_string('dropfiles', 'repository').then(function(label) { - dz.setLabel(label); - }); - - dz.init(); - } - - // Handle file upload to save it as draft. - function handleDroppedFiles(files, modal) { - var currentIndex = 0; - - function uploadNextFile() { - if (currentIndex >= files.length) { - return; + // 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}); + } } - - var file = files[currentIndex]; - currentIndex++; - - var formData = new FormData(); - formData.append('repo_upload_file', file); - formData.append('repo_id', repoId); - formData.append('contextid', contextId); - formData.append('sesskey', M.cfg.sesskey); - formData.append('action', 'upload'); - - if (draftidRef.value) { - formData.append('itemid', draftidRef.value); - } - - var xhr = new XMLHttpRequest(); - xhr.open('POST', M.cfg.wwwroot + uploadurl, true); - - xhr.onload = function() { - var response = JSON.parse(this.responseText); - if (response.error) { - Notification.alert( - Str.get_string('uploaderror', 'repository'), - response.error, - Str.get_string('close', 'repository') - ); - } else { - if (response.draftid && !draftidRef.value) { - draftidRef.value = response.draftid; - } - renderUploadedFile(modal, response); - - // Upload next file after this one finishes. - uploadNextFile(); - } - }; - - xhr.send(formData); - } - - // Start uploading files. - uploadNextFile(); - } - - // Render the uploaded file in a given modal. - function renderUploadedFile(modal, response) { - const $body = modal.getBody(); - let uploadFilesClass = 'uploaded-files'; - let $fileList = $body.find(`.${uploadFilesClass} ul`); - - if ($fileList.length === 0) { - // Load the string asynchronously. - Str.get_string('attachedfiles', 'repository').then(function(label) { - const uploadedFiles = document.createElement('div'); - uploadedFiles.className = uploadFilesClass + ' pt-4'; - const headingElement = document.createElement('h5'); - headingElement.textContent = label; - uploadedFiles.appendChild(headingElement); - uploadedFiles.appendChild(document.createElement('ul')); - $body.append(uploadedFiles); - - $fileList = $body.find(`.${uploadFilesClass} ul`); - $fileList.append(`
  • ${response.file}
  • `); - }); - } - - $fileList.append(`
  • ${response.file}
  • `); - } - - // Commit uploaded draft files when user clicks save. - function commitFiles(ev, modal, toolbar) { - ev.preventDefault(); - - if (!draftidRef.value) { - Notification.alert( - Str.get_string('uploaderror', 'repository'), - Str.get_string('nofilesattached', 'repository'), - Str.get_string('close', 'repository') - ); - return; - } - - // Show loading spinner using Moodle's core/loading template. - Templates.render('core/loading', {size: 'lg'}).then(function(html) { - // Create a full-page overlay with Moodle classes. - const loadingOverlay = document.createElement('div'); - loadingOverlay.className = 'loading-overlay'; - loadingOverlay.className = 'fixed-top w-100 h-100 d-flex justify-content-center align-items-center'; - loadingOverlay.style.zIndex = '9999'; - loadingOverlay.style.backgroundColor = 'rgba(255, 255, 255, 0.8)'; - - // Create container for the loading spinner. - const loadingContainer = document.createElement('div'); - loadingContainer.className = 'd-flex flex-column align-items-center'; - loadingContainer.innerHTML = html; - - // Add text below the spinner. - Str.get_string('uploading', 'repository').then(function(loadingText) { - const loadingTextElement = document.createElement('p'); - loadingTextElement.className = 'mt-2'; - loadingTextElement.textContent = loadingText; - loadingContainer.appendChild(loadingTextElement); - }); - - loadingOverlay.appendChild(loadingContainer); - document.body.appendChild(loadingOverlay); - - var formData = new FormData(); - formData.append('action', 'commit'); - formData.append('repo_id', repoId); - formData.append('contextid', contextId); - formData.append('sesskey', M.cfg.sesskey); - formData.append('itemid', draftidRef.value); - - var xhr = new XMLHttpRequest(); - xhr.open('POST', M.cfg.wwwroot + uploadurl, true); - - xhr.onload = function() { - // Remove loading overlay. - if (loadingOverlay && loadingOverlay.parentNode) { - loadingOverlay.parentNode.removeChild(loadingOverlay); - } - - let response = JSON.parse(xhr.responseText); - if (response.error) { - Notification.alert( - Str.get_string('uploaderror', 'repository'), - response.error, - Str.get_string('close', 'repository') - ); - } else { - modal.hide(); - var refreshButton = toolbar.one('.fp-tb-refresh').one('a,button'); - if (refreshButton) { - refreshButton.simulate('click'); - } - } - }; - xhr.send(formData); }); - } - }); + }); + } }, this); // same with .fp-tb-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/lib.php b/public/repository/googledocs/lib.php index 8cc54c60eef..1b76567dedd 100644 --- a/public/repository/googledocs/lib.php +++ b/public/repository/googledocs/lib.php @@ -76,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 { @@ -87,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'); } /** @@ -310,7 +313,7 @@ class repository_googledocs extends repository { 'path' => $contentobj->get_navigation(), 'list' => $contentobj->get_content_nodes($query, [$this, 'filter']), 'uploadfile' => true, - 'uploadurl' => '/repository/googledocs/repository_ajax.php', + 'uploadevent' => 'repository_googledocs_upload', 'repo_id' => $this->id, 'contextid' => $this->context->id, 'sesskey' => sesskey(), @@ -805,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,mimeType,webContentLink,webViewLink,size,thumbnailLink,iconLink"; + $fields = "id,name,owners,parents"; $params = [ 'fileid' => $fileid, 'fields' => $fields @@ -1041,26 +1044,11 @@ class repository_googledocs extends repository { } $originalfile = $this->get_file_summary($userservice, $source->id); - $downloadlink = ''; - if (isset($originalfile->webContentLink)) { - $downloadlink = $originalfile->webContentLink; - } else if (isset($originalfile->webViewLink)) { - $downloadlink = $originalfile->webViewLink; - } else { - // If we don't have a link, we cannot download the file. - throw new repository_exception( - 'errorwhilecommunicatingwith', - 'repository', - '', - 'Cannot download file: ' . $source->name - ); - } - + // Use the user service to download the file. $downloadedfile = $this->download_file( $userservice, $source->id, - $downloadlink, - $originalfile->name + $originalfile->name, ); // Upload the user file to the system drive. $uploaded = $this->upload_file( @@ -1068,7 +1056,7 @@ class repository_googledocs extends repository { $downloadedfile['path'], $downloadedfile['newfilename'], $source->exportformat, - $parentid + $parentid, ); // Add the original file owner as a writer to the file. $this->add_writer_to_file($systemservice, $uploaded->id, $originalfile->owners[0]->emailAddress); @@ -1139,31 +1127,28 @@ class repository_googledocs extends repository { * * @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 $downloadlink The URL to the downloaded file. * @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 $downloadlink, string $originalfilename ): array|repository_exception { global $CFG; - // Ensure the file can be downloaded without credentials. - $this->set_file_sharing_anyone_with_link_can_read($userservice, $fileid); + $client = $this->get_user_oauth_client(); + $base = 'https://www.googleapis.com/drive/v3'; + $params = ['alt' => 'media']; + $sourceurl = new moodle_url($base . '/files/' . $fileid, $params); - $tmp = make_request_directory(); - $temppath = $tmp . '/' . $fileid; - $c = new curl(); - $options = ['filepath' => $temppath, 'timeout' => $CFG->repositorygetfiletimeout]; - $result = $c->download_one($downloadlink, null, $options); - if ($result) { - @chmod($temppath, $CFG->filepermissions); + $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' => $temppath, - 'url' => $downloadlink, + 'path' => $path, 'newfilename' => $originalfilename, ]; } diff --git a/public/repository/googledocs/repository_ajax.php b/public/repository/googledocs/repository_ajax.php index 5feaafccd12..55e53c4e4e0 100644 --- a/public/repository/googledocs/repository_ajax.php +++ b/public/repository/googledocs/repository_ajax.php @@ -49,64 +49,34 @@ if (!$repo) { $repo->check_capability(); $repo->check_login(); -$fs = get_file_storage(); -$usercontext = context_user::instance($USER->id); - switch ($action) { case 'upload': - // Save the files in the draft area. - $draftid = !empty($itemid) ? $itemid : file_get_unused_draft_itemid(); - $file = $_FILES['repo_upload_file']; - - if (empty($file) || $file['error'] !== UPLOAD_ERR_OK) { - echo json_encode(['error' => get_string('erroruploadfailed', 'repository')]); - break; - } - - $filerecord = [ - 'contextid' => $usercontext->id, - 'component' => 'user', - 'filearea' => 'draft', - 'itemid' => $draftid, - 'filepath' => '/', - 'filename' => clean_param($file['name'], PARAM_FILE), - ]; - - $fs->create_file_from_pathname($filerecord, $file['tmp_name']); - - echo json_encode([ - 'draftid' => $draftid, - 'file' => $filerecord['filename'], - ]); - break; - - case 'commit': - // Upload the files to Google Drive. - if (empty($itemid)) { - die(json_encode(['erroruploadfailed' => get_string('error', 'repository')])); - } - - // Grab files from draft area with the same itemid. - $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $itemid, "id", false); - if (empty($draftfiles)) { - die(json_encode(['erroruploadfailed' => get_string('error', 'repository')])); + $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. - $tmp = make_request_directory(); $ha = new repository_googledocs($repoid, $context); $userauth = $ha->get_user_oauth_client(); $userservice = new repository_googledocs\rest($userauth); - foreach ($draftfiles as $draftfile) { - $tempfile = $tmp . '/' . rand(); - $filename = $draftfile->get_filename(); - $draftfile->copy_content_to($tempfile); - $ha->upload_file($userservice, $tempfile, $filename, 'download', "root"); + foreach ($savedfiles as $file) { + $filename = basename($file); + $ha->upload_file($userservice, $file, $filename, 'download', "root"); } - // Clear drafts after upload them to Google drive. - $fs->delete_area_files($usercontext->id, 'user', 'draft', $itemid); - echo json_encode(['success' => true]); break; 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}} +
    +
      +