X7ROOT File Manager
Current Path:
/home/cbholdings/pasukulu/course/format/amd/src/local/courseeditor
home
/
cbholdings
/
pasukulu
/
course
/
format
/
amd
/
src
/
local
/
courseeditor
/
📁
..
📄
contenttree.js
(6.4 KB)
📄
courseeditor.js
(12.27 KB)
📄
dndcmitem.js
(4.07 KB)
📄
dndsection.js
(6.18 KB)
📄
dndsectionitem.js
(4.58 KB)
📄
exporter.js
(7.42 KB)
📄
fileuploader.js
(18.14 KB)
📄
mutations.js
(25.12 KB)
Editing: fileuploader.js
// 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 <http://www.gnu.org/licenses/>. /** * The course file uploader. * * This module is used to upload files directly into the course. * * @module core_courseformat/local/courseeditor/fileuploader * @copyright 2022 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * @typedef {Object} Handler * @property {String} extension the handled extension or * for any * @property {String} message the handler message * @property {String} module the module name */ import Config from 'core/config'; import ModalFactory from 'core/modal_factory'; import ModalEvents from 'core/modal_events'; import Templates from 'core/templates'; import {getFirst} from 'core/normalise'; import {prefetchStrings} from 'core/prefetch'; import {get_string as getString, get_strings as getStrings} from 'core/str'; import {getCourseEditor} from 'core_courseformat/courseeditor'; import {processMonitor} from 'core/process_monitor'; import {debounce} from 'core/utils'; // Uploading url. const UPLOADURL = Config.wwwroot + '/course/dndupload.php'; const DEBOUNCETIMER = 500; const USERCANIGNOREFILESIZELIMITS = -1; /** @var {ProcessQueue} uploadQueue the internal uploadQueue instance. */ let uploadQueue = null; /** @var {Object} handlerManagers the courseId indexed loaded handler managers. */ let handlerManagers = {}; /** @var {Map} courseUpdates the pending course sections updates. */ let courseUpdates = new Map(); /** @var {Object} errors the error messages. */ let errors = null; // Load global strings. prefetchStrings('moodle', ['addresourceoractivity', 'upload']); prefetchStrings('core_error', ['dndmaxbytes', 'dndread', 'dndupload', 'dndunkownfile']); /** * Class to upload a file into the course. * @private */ class FileUploader { /** * Class constructor. * * @param {number} courseId the course id * @param {number} sectionId the section id * @param {number} sectionNum the section number * @param {File} fileInfo the file information object * @param {Handler} handler the file selected file handler */ constructor(courseId, sectionId, sectionNum, fileInfo, handler) { this.courseId = courseId; this.sectionId = sectionId; this.sectionNum = sectionNum; this.fileInfo = fileInfo; this.handler = handler; } /** * Execute the file upload and update the state in the given process. * * @param {LoadingProcess} process the process to store the upload result */ execute(process) { const fileInfo = this.fileInfo; const xhr = this._createXhrRequest(process); const formData = this._createUploadFormData(); // Try reading the file to check it is not a folder, before sending it to the server. const reader = new FileReader(); reader.onload = function() { // File was read OK - send it to the server. xhr.open("POST", UPLOADURL, true); xhr.send(formData); }; reader.onerror = function() { // Unable to read the file (it is probably a folder) - display an error message. process.setError(errors.dndread); }; if (fileInfo.size > 0) { // If this is a non-empty file, try reading the first few bytes. // This will trigger reader.onerror() for folders and reader.onload() for ordinary, readable files. reader.readAsText(fileInfo.slice(0, 5)); } else { // If you call slice() on a 0-byte folder, before calling readAsText, then Firefox triggers reader.onload(), // instead of reader.onerror(). // So, for 0-byte files, just call readAsText on the whole file (and it will trigger load/error functions as expected). reader.readAsText(fileInfo); } } /** * Returns the bind version of execute function. * * This method is used to queue the process into a ProcessQueue instance. * * @returns {Function} the bind function to execute the process */ getExecutionFunction() { return this.execute.bind(this); } /** * Generate a upload XHR file request. * * @param {LoadingProcess} process the current process * @return {XMLHttpRequest} the XHR request */ _createXhrRequest(process) { const xhr = new XMLHttpRequest(); // Update the progress bar as the file is uploaded. xhr.upload.addEventListener( 'progress', (event) => { if (event.lengthComputable) { const percent = Math.round((event.loaded * 100) / event.total); process.setPercentage(percent); } }, false ); // Wait for the AJAX call to complete. xhr.onreadystatechange = () => { if (xhr.readyState == 1) { // Add a 1% just to indicate that it is uploading. process.setPercentage(1); } // State 4 is DONE. Otherwise the connection is still ongoing. if (xhr.readyState != 4) { return; } if (xhr.status == 200) { var result = JSON.parse(xhr.responseText); if (result && result.error == 0) { // All OK. this._finishProcess(process); } else { process.setError(result.error); } } else { process.setError(errors.dndupload); } }; return xhr; } /** * Upload a file into the course. * * @return {FormData|null} the new form data object */ _createUploadFormData() { const formData = new FormData(); try { formData.append('repo_upload_file', this.fileInfo); } catch (error) { throw Error(error.dndread); } formData.append('sesskey', Config.sesskey); formData.append('course', this.courseId); formData.append('section', this.sectionNum); formData.append('module', this.handler.module); formData.append('type', 'Files'); return formData; } /** * Finishes the current process. * @param {LoadingProcess} process the process */ _finishProcess(process) { addRefreshSection(this.courseId, this.sectionId); process.setPercentage(100); process.finish(); } } /** * The file handler manager class. * * @private */ class HandlerManager { /** @var {Object} lastHandlers the last handlers selected per each file extension. */ lastHandlers = {}; /** @var {Handler[]|null} allHandlers all the available handlers. */ allHandlers = null; /** * Class constructor. * * @param {Number} courseId */ constructor(courseId) { this.courseId = courseId; this.lastUploadId = 0; this.courseEditor = getCourseEditor(courseId); if (!this.courseEditor) { throw Error('Unkown course editor'); } this.maxbytes = this.courseEditor.get('course')?.maxbytes ?? 0; } /** * Load the course file handlers. */ async loadHandlers() { this.allHandlers = await this.courseEditor.getFileHandlersPromise(); } /** * Extract the file extension from a fileInfo. * * @param {File} fileInfo * @returns {String} the file extension or an empty string. */ getFileExtension(fileInfo) { let extension = ''; const dotpos = fileInfo.name.lastIndexOf('.'); if (dotpos != -1) { extension = fileInfo.name.substring(dotpos + 1, fileInfo.name.length).toLowerCase(); } return extension; } /** * Check if the file is valid. * * @param {File} fileInfo the file info */ validateFile(fileInfo) { if (this.maxbytes !== USERCANIGNOREFILESIZELIMITS && fileInfo.size > this.maxbytes) { throw Error(errors.dndmaxbytes); } } /** * Get the file handlers of an specific file. * * @param {File} fileInfo the file indo * @return {Array} Array of handlers */ filterHandlers(fileInfo) { const extension = this.getFileExtension(fileInfo); return this.allHandlers.filter(handler => handler.extension == '*' || handler.extension == extension); } /** * Get the Handler to upload a specific file. * * It will ask the used if more than one handler is available. * * @param {File} fileInfo the file info * @returns {Promise<Handler|null>} the selected handler or null if the user cancel */ async getFileHandler(fileInfo) { const fileHandlers = this.filterHandlers(fileInfo); if (fileHandlers.length == 0) { throw Error(errors.dndunkownfile); } let fileHandler = null; if (fileHandlers.length == 1) { fileHandler = fileHandlers[0]; } else { fileHandler = await this.askHandlerToUser(fileHandlers, fileInfo); } return fileHandler; } /** * Ask the user to select a specific handler. * * @param {Handler[]} fileHandlers * @param {File} fileInfo the file info * @return {Promise<Handler>} the selected handler */ async askHandlerToUser(fileHandlers, fileInfo) { const extension = this.getFileExtension(fileInfo); // Build the modal parameters from the event data. const modalParams = { title: getString('addresourceoractivity', 'moodle'), body: Templates.render( 'core_courseformat/fileuploader', this.getModalData( fileHandlers, fileInfo, this.lastHandlers[extension] ?? null ) ), type: ModalFactory.types.SAVE_CANCEL, saveButtonText: getString('upload', 'moodle'), }; // Create the modal. const modal = await this.modalBodyRenderedPromise(modalParams); const selectedHandler = await this.modalUserAnswerPromise(modal, fileHandlers); // Cancel action. if (selectedHandler === null) { return null; } // Save last selected handler. this.lastHandlers[extension] = selectedHandler.module; return selectedHandler; } /** * Generated the modal template data. * * @param {Handler[]} fileHandlers * @param {File} fileInfo the file info * @param {String|null} defaultModule the default module if any * @return {Object} the modal template data. */ getModalData(fileHandlers, fileInfo, defaultModule) { const data = { filename: fileInfo.name, uploadid: ++this.lastUploadId, handlers: [], }; let hasDefault = false; fileHandlers.forEach((handler, index) => { const isDefault = (defaultModule == handler.module); data.handlers.push({ ...handler, selected: isDefault, labelid: `fileuploader_${data.uploadid}`, value: index, }); hasDefault = hasDefault || isDefault; }); if (!hasDefault && data.handlers.length > 0) { const lastHandler = data.handlers.pop(); lastHandler.selected = true; data.handlers.push(lastHandler); } return data; } /** * Get the user handler choice. * * Wait for the user answer in the modal and resolve with the selected index. * * @param {Modal} modal the modal instance * @param {Handler[]} fileHandlers the availabvle file handlers * @return {Promise} with the option selected by the user. */ modalUserAnswerPromise(modal, fileHandlers) { const modalBody = getFirst(modal.getBody()); return new Promise((resolve, reject) => { modal.getRoot().on( ModalEvents.save, event => { // Get the selected option. const index = modalBody.querySelector('input:checked').value; event.preventDefault(); modal.destroy(); if (!fileHandlers[index]) { reject('Invalid handler selected'); } resolve(fileHandlers[index]); } ); modal.getRoot().on( ModalEvents.cancel, () => { resolve(null); } ); }); } /** * Create a new modal and return a Promise to the body rendered. * * @param {Object} modalParams the modal params * @returns {Promise} the modal body rendered promise */ modalBodyRenderedPromise(modalParams) { return new Promise((resolve, reject) => { ModalFactory.create(modalParams).then((modal) => { modal.setRemoveOnClose(true); // Handle body loading event. modal.getRoot().on(ModalEvents.bodyRendered, () => { resolve(modal); }); // Configure some extra modal params. if (modalParams.saveButtonText !== undefined) { modal.setSaveButtonText(modalParams.saveButtonText); } modal.show(); return; }).catch(() => { reject(`Cannot load modal content`); }); }); } } /** * Add a section to refresh. * * @param {number} courseId the course id * @param {number} sectionId the seciton id */ function addRefreshSection(courseId, sectionId) { let refresh = courseUpdates.get(courseId); if (!refresh) { refresh = new Set(); } refresh.add(sectionId); courseUpdates.set(courseId, refresh); refreshCourseEditors(); } /** * Debounced processing all pending course refreshes. * @private */ const refreshCourseEditors = debounce( () => { const refreshes = courseUpdates; courseUpdates = new Map(); refreshes.forEach((sectionIds, courseId) => { const courseEditor = getCourseEditor(courseId); if (!courseEditor) { return; } courseEditor.dispatch('sectionState', [...sectionIds]); }); }, DEBOUNCETIMER ); /** * Load and return the course handler manager instance. * * @param {Number} courseId the course Id to load * @returns {Promise<HandlerManager>} promise of the the loaded handleManager */ async function loadCourseHandlerManager(courseId) { if (handlerManagers[courseId] !== undefined) { return handlerManagers[courseId]; } try { const handlerManager = new HandlerManager(courseId); await handlerManager.loadHandlers(); handlerManagers[courseId] = handlerManager; } catch (error) { throw error; } return handlerManagers[courseId]; } /** * Load all the erros messages at once in the module "errors" variable. * @param {Number} courseId the course id */ async function loadErrorStrings(courseId) { if (errors !== null) { return; } const courseEditor = getCourseEditor(courseId); const maxbytestext = courseEditor.get('course')?.maxbytestext ?? '0'; errors = {}; const allStrings = [ {key: 'dndmaxbytes', component: 'core_error', param: {size: maxbytestext}}, {key: 'dndread', component: 'core_error'}, {key: 'dndupload', component: 'core_error'}, {key: 'dndunkownfile', component: 'core_error'}, ]; window.console.log(allStrings); const loadedStrings = await getStrings(allStrings); allStrings.forEach(({key}, index) => { errors[key] = loadedStrings[index]; }); } /** * Start a batch file uploading into the course. * * @private * @param {number} courseId the course id. * @param {number} sectionId the section id. * @param {number} sectionNum the section number. * @param {File} fileInfo the file information object * @param {HandlerManager} handlerManager the course handler manager */ const queueFileUpload = async function(courseId, sectionId, sectionNum, fileInfo, handlerManager) { let handler; uploadQueue = await processMonitor.createProcessQueue(); try { handlerManager.validateFile(fileInfo); handler = await handlerManager.getFileHandler(fileInfo); } catch (error) { uploadQueue.addError(fileInfo.name, error.message); return; } // If we don't have a handler means the user cancel the upload. if (!handler) { return; } const fileProcessor = new FileUploader(courseId, sectionId, sectionNum, fileInfo, handler); uploadQueue.addPending(fileInfo.name, fileProcessor.getExecutionFunction()); }; /** * Upload a file to the course. * * This method will show any necesary modal to handle the request. * * @param {number} courseId the course id * @param {number} sectionId the section id * @param {number} sectionNum the section number * @param {Array} files and array of files */ export const uploadFilesToCourse = async function(courseId, sectionId, sectionNum, files) { // Get the course handlers. let handlerManager; try { handlerManager = await loadCourseHandlerManager(courseId); await loadErrorStrings(courseId); } catch (error) { throw error; } for (let index = 0; index < files.length; index++) { const fileInfo = files[index]; await queueFileUpload(courseId, sectionId, sectionNum, fileInfo, handlerManager); } };
Upload File
Create Folder