diff --git a/classes/editor_ajax.php b/classes/editor_ajax.php index ef207649..2b22943c 100644 --- a/classes/editor_ajax.php +++ b/classes/editor_ajax.php @@ -65,7 +65,8 @@ public function getLatestLibraryVersions() { return $DB->get_records_sql(" SELECT hl4.id, hl4.machine_name, hl4.title, hl4.major_version, - hl4.minor_version, hl4.patch_version, hl4.has_icon, hl4.restricted + hl4.minor_version, hl4.patch_version, hl4.has_icon, hl4.restricted, + 0 AS patch_version_in_folder_name FROM {hvp_libraries} hl4 JOIN ({$maxminorversionsql}) hl3 ON hl4.machine_name = hl3.machine_name diff --git a/classes/file_storage.php b/classes/file_storage.php index 9fa73ef4..4be1a132 100644 --- a/classes/file_storage.php +++ b/classes/file_storage.php @@ -544,7 +544,7 @@ private static function readFileTree($source, $options, $archive = null, $relati if (empty($archive) && $exportzip) { $archive = new \ZipArchive(); $path = tempnam(get_request_storage_directory(),'libdir'); - $archive->open($path, \ZipArchive::CREATE || \ZipArchive::OVERWRITE); + $archive->open($path, \ZipArchive::CREATE | \ZipArchive::OVERWRITE); // Set recursion flag. $top = true; } else { diff --git a/classes/framework.php b/classes/framework.php index 2b7c92eb..e218499c 100644 --- a/classes/framework.php +++ b/classes/framework.php @@ -1521,23 +1521,27 @@ public function loadLibrary($machinename, $majorversion, $minorversion) { 'minor_version' => $minorversion )); - $librarydata = array( - 'libraryId' => $library->id, - 'machineName' => $library->machine_name, - 'title' => $library->title, - 'majorVersion' => $library->major_version, - 'minorVersion' => $library->minor_version, - 'patchVersion' => $library->patch_version, - 'embedTypes' => $library->embed_types, - 'preloadedJs' => $library->preloaded_js, - 'preloadedCss' => $library->preloaded_css, - 'dropLibraryCss' => $library->drop_library_css, - 'fullscreen' => $library->fullscreen, - 'runnable' => $library->runnable, - 'semantics' => $library->semantics, - 'restricted' => $library->restricted, - 'hasIcon' => $library->has_icon - ); + if ($library) { + $librarydata = array( + 'libraryId' => $library->id, + 'machineName' => $library->machine_name, + 'title' => $library->title, + 'majorVersion' => $library->major_version, + 'minorVersion' => $library->minor_version, + 'patchVersion' => $library->patch_version, + 'embedTypes' => $library->embed_types, + 'preloadedJs' => $library->preloaded_js, + 'preloadedCss' => $library->preloaded_css, + 'dropLibraryCss' => $library->drop_library_css, + 'fullscreen' => $library->fullscreen, + 'runnable' => $library->runnable, + 'semantics' => $library->semantics, + 'restricted' => $library->restricted, + 'hasIcon' => $library->has_icon + ); + } else { + return []; + } $dependencies = $DB->get_records_sql( 'SELECT hl.id, hl.machine_name, hl.major_version, hl.minor_version, hll.dependency_type diff --git a/export_libraries.php b/export_libraries.php new file mode 100644 index 00000000..4d4f22f9 --- /dev/null +++ b/export_libraries.php @@ -0,0 +1,163 @@ +<?php +// 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/>. + +/** + * Export all libraries script. + * + * @package mod_hvp + * @author Rossco Hellmans <rosscohellmans@catalyst-au.net> + * @copyright Catalyst IT, 2021 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../../config.php'); +require_once($CFG->libdir . '/adminlib.php'); +require_once('locallib.php'); + +admin_externalpage_setup('h5plibraries'); + +$COURSE = $SITE; + +$interface = mod_hvp\framework::instance('interface'); +$core = mod_hvp\framework::instance('core'); +$exporter = new H5PExport($interface, $core); + +$libraries = $core->h5pF->loadLibraries(); +$tmppath = make_request_directory(); + +$exportedlibraries = []; + +// Add libraries to h5p. +foreach ($libraries as $versions) { + try { + // We only want to export the latest version. + $libraryinfo = array_pop($versions); + $library = $interface->loadLibrary( + $libraryinfo->machine_name, + $libraryinfo->major_version, + $libraryinfo->minor_version + ); + + exportlibrary($library, $exporter, $tmppath, $exportedlibraries); + } catch (Exception $e) { + throw new moodle_exception('exportlibrarieserror', 'hvp', '', null, $e->getMessage()); + } +} + +$files = []; +populatefilelist($tmppath, $files); + +// Get path to temporary export target file. +$tmpfile = tempnam(get_request_storage_directory(), 'hvplibs'); + +// Create new zip instance. +$zip = new ZipArchive(); +$zip->open($tmpfile, ZipArchive::CREATE | ZipArchive::OVERWRITE); + +// Add all the files from the tmp dir. +foreach ($files as $file) { + // Please note that the zip format has no concept of folders, we must + // use forward slashes to separate our directories. + if (file_exists(realpath($file->absolutePath))) { + $zip->addFile(realpath($file->absolutePath), $file->relativePath); + } +} + +// Close zip. +$zip->close(); + +$filename = $SITE->shortname . '_site_libraries.h5p'; +try { + // Save export. + $exporter->h5pC->fs->saveExport($tmpfile, $filename); +} catch (Exception $e) { + throw new moodle_exception('exportlibrarieserror', 'hvp', '', null, $e->getMessage()); +} + +// Now send the stored export to user. +$context = \context_course::instance($COURSE->id); +$fs = get_file_storage(); +$file = $fs->get_file($context->id, 'mod_hvp', 'exports', 0, '/', $filename); +send_stored_file($file); + +/** + * Exports a library. + * + * @param array $library + * @param H5PExport $exporter + * @param string $tmppath + * @param array $exportedlibraries + */ +function exportlibrary($library, $exporter, $tmppath, &$exportedlibraries) { + if (in_array($library['libraryId'], $exportedlibraries)) { + // Already exported. + return; + } + + $exportfolder = null; + + // Determine path of export library. + if (isset($exporter->h5pC) && isset($exporter->h5pC->h5pD)) { + + // Tries to find library in development folder. + $isdevlibrary = $exporter->h5pC->h5pD->getLibrary( + $library['machineName'], + $library['majorVersion'], + $library['minorVersion'] + ); + + if ($isdevlibrary !== null && isset($library['path'])) { + $exportfolder = "/" . $library['path']; + } + } + + // Export library. + $exporter->h5pC->fs->exportLibrary($library, $tmppath, $exportfolder); + $exportedlibraries[] = $library['libraryId']; + + // Now export the dependancies. + $dependencies = []; + $exporter->h5pC->findLibraryDependencies($dependencies, $library); + + foreach ($dependencies as $dependency) { + exportlibrary($dependency['library'], $exporter, $tmppath, $exportedlibraries); + } +} + +/** + * Populates an array with a list of files to zip up. + * + * @param string $dir + * @param array $files + * @param string $relative + */ +function populatefilelist($dir, &$files, $relative = '') { + $strip = strlen($dir) + 1; + $contents = glob($dir . '/' . '*'); + if (!empty($contents)) { + foreach ($contents as $file) { + $rel = $relative . substr($file, $strip); + if (is_dir($file)) { + populatefilelist($file, $files, $rel . '/'); + } else { + $files[] = (object)[ + 'absolutePath' => $file, + 'relativePath' => $rel, + ]; + } + } + } +} diff --git a/js/h5p-content-upgrade-bulk.js b/js/h5p-content-upgrade-bulk.js new file mode 100644 index 00000000..26046487 --- /dev/null +++ b/js/h5p-content-upgrade-bulk.js @@ -0,0 +1,455 @@ +/* global H5PAdminIntegration H5PUtils */ +/* Copied from library/js/h5p-content-upgrade.js and adjusted to handle multiple library upgrades */ + +(function ($, Version) { + var commonInfo, $log, $statusWatcher, librariesCache = {}, scriptsCache = {}; + + // Initialize + $(document).ready(function () { + // Get library infos + commonInfo = H5PAdminIntegration.commonInfo; + var libraries = H5PAdminIntegration.libraryInfo; + + // Get and reset container + const $wrapper = $('#h5p-admin-container').html(''); + $log = $('<ul class="content-upgrade-log"></ul>').appendTo($wrapper); + $statusWatcher = new StatusWatcher(libraries.length); + + libraries.forEach((library) => { + container = $('<div></div>').appendTo($wrapper); + new ContentUpgrade(library.upgradeTo, library, container); + }); + }); + + /** + * Watched statuses to check when all libraries have finished, + * then displays a return button. + * + * @param {Number} total + * @param {Element} container + * @returns {_L1.Throbber} + */ + function StatusWatcher(total) { + var finished = 0; + + /** + * Makes it possible to set the progress. + * + * @param {String} progress + */ + this.increaseFinished = function () { + finished++; + if (finished == total) { + $('#h5p-admin-return-button').show(); + } + }; + } + + /** + * Displays a throbber in the status field. + * + * @param {String} msg + * @param {Element} container + * @returns {_L1.Throbber} + */ + function Throbber(msg, container) { + var $throbber = H5PUtils.throbber(msg); + container.html('').append($throbber); + + /** + * Makes it possible to set the progress. + * + * @param {String} progress + */ + this.setProgress = function (progress) { + $throbber.text(msg + ' ' + progress); + }; + } + + /** + * Start a new content upgrade. + * + * @param {Number} libraryId + * @param {Object} fromLibrary + * @param {Element} container + * @returns {_L1.ContentUpgrade} + */ + function ContentUpgrade(libraryId, fromLibrary, container) { + var self = this; + + // Set info and libraryId as self vars (important for doing multiple libraries). + self.libraryId = libraryId; + self.info = fromLibrary; + self.container = container; + + // Get selected version + self.version = new Version(self.info.versions[libraryId]); + self.version.libraryId = libraryId; + + // Create throbber with loading text and progress + self.throbber = new Throbber(self.info.inProgress.replace('%ver', self.version), container); + + self.started = new Date().getTime(); + self.io = 0; + + // Track number of working + self.working = 0; + + var start = function () { + // Get the next batch + self.nextBatch({ + libraryId: self.libraryId, + token: self.info.token + }); + }; + + if (window.Worker !== undefined) { + // Prepare our workers + self.initWorkers(); + start(); + } + else { + // No workers, do the job ourselves + self.loadScript(commonInfo.scriptBaseUrl + '/h5p-content-upgrade-process.js' + commonInfo.buster, start); + } + } + + /** + * Initialize workers + */ + ContentUpgrade.prototype.initWorkers = function () { + var self = this; + + // Determine number of workers (defaults to 4) + var numWorkers = (window.navigator !== undefined && window.navigator.hardwareConcurrency ? window.navigator.hardwareConcurrency : 4); + self.workers = new Array(numWorkers); + + // Register message handlers + var messageHandlers = { + done: function (result) { + self.workDone(result.id, result.params, this); + }, + error: function (error) { + self.printError(error.err); + self.workDone(error.id, null, this); + }, + loadLibrary: function (details) { + var worker = this; + self.loadLibrary(details.name, new Version(details.version), function (err, library) { + if (err) { + // Reset worker? + return; + } + + worker.postMessage({ + action: 'libraryLoaded', + library: library + }); + }); + } + }; + + for (var i = 0; i < numWorkers; i++) { + self.workers[i] = new Worker(commonInfo.scriptBaseUrl + '/h5p-content-upgrade-worker.js' + commonInfo.buster); + self.workers[i].onmessage = function (event) { + if (event.data.action !== undefined && messageHandlers[event.data.action]) { + messageHandlers[event.data.action].call(this, event.data); + } + }; + } + }; + + /** + * Get the next batch and start processing it. + * + * @param {Object} outData + */ + ContentUpgrade.prototype.nextBatch = function (outData) { + var self = this; + + // Track time spent on IO + var start = new Date().getTime(); + $.post(self.info.infoUrl, outData, function (inData) { + self.io += new Date().getTime() - start; + if (!(inData instanceof Object)) { + // Print errors from backend + return self.setStatus(inData); + } + if (inData.left === 0) { + var total = new Date().getTime() - self.started; + + if (window.console && console.log) { + console.log('The upgrade process took ' + (total / 1000) + ' seconds. (' + (Math.round((self.io / (total / 100)) * 100) / 100) + ' % IO)' ); + } + + // Terminate workers + self.terminate(); + + // Nothing left to process + return self.setStatus(self.info.done); + } + + self.left = inData.left; + self.token = inData.token; + + // Start processing + self.processBatch(inData.params, inData.skipped); + }); + }; + + /** + * Set current status message. + * + * @param {String} msg + */ + ContentUpgrade.prototype.setStatus = function (msg) { + this.container.html(msg); + $statusWatcher.increaseFinished(); + }; + + /** + * Process the given parameters. + * + * @param {Object} parameters + */ + ContentUpgrade.prototype.processBatch = function (parameters, skipped) { + var self = this; + + // Track upgraded params + self.upgraded = {}; + self.skipped = skipped; + + // Track current batch + self.parameters = parameters; + + // Create id mapping + self.ids = []; + for (var id in parameters) { + if (parameters.hasOwnProperty(id)) { + self.ids.push(id); + } + } + + // Keep track of current content + self.current = -1; + + if (self.workers !== undefined) { + // Assign each worker content to upgrade + for (var i = 0; i < self.workers.length; i++) { + self.assignWork(self.workers[i]); + } + } + else { + + self.assignWork(); + } + }; + + /** + * + */ + ContentUpgrade.prototype.assignWork = function (worker) { + var self = this; + + var id = self.ids[self.current + 1]; + if (id === undefined) { + return false; // Out of work + } + self.current++; + self.working++; + + if (worker) { + worker.postMessage({ + action: 'newJob', + id: id, + name: self.info.library.name, + oldVersion: self.info.library.version, + newVersion: self.version.toString(), + params: self.parameters[id] + }); + } + else { + new H5P.ContentUpgradeProcess(self.info.library.name, new Version(self.info.library.version), self.version, self.parameters[id], id, function loadLibrary(name, version, next) { + self.loadLibrary(name, version, function (err, library) { + if (library.upgradesScript) { + self.loadScript(library.upgradesScript, function (err) { + if (err) { + err = commonInfo.errorScript.replace('%lib', name + ' ' + version); + } + next(err, library); + }); + } + else { + next(null, library); + } + }); + + }, function done(err, result) { + if (err) { + self.printError(err); + result = null; + } + + self.workDone(id, result); + }); + } + }; + + /** + * + */ + ContentUpgrade.prototype.workDone = function (id, result, worker) { + var self = this; + + self.working--; + if (result === null) { + self.skipped.push(id); + } + else { + self.upgraded[id] = result; + } + + // Update progress message + self.throbber.setProgress(Math.round((self.info.total - self.left + self.current) / (self.info.total / 100)) + ' %'); + + // Assign next job + if (self.assignWork(worker) === false && self.working === 0) { + // All workers have finsihed. + self.nextBatch({ + libraryId: self.version.libraryId, + token: self.token, + skipped: JSON.stringify(self.skipped), + params: JSON.stringify(self.upgraded) + }); + } + }; + + /** + * + */ + ContentUpgrade.prototype.terminate = function () { + var self = this; + + if (self.workers) { + // Stop all workers + for (var i = 0; i < self.workers.length; i++) { + self.workers[i].terminate(); + } + } + }; + + var librariesLoadedCallbacks = {}; + + /** + * Load library data needed for content upgrade. + * + * @param {String} name + * @param {Version} version + * @param {Function} next + */ + ContentUpgrade.prototype.loadLibrary = function (name, version, next) { + var self = this; + + var key = name + '/' + version.major + '/' + version.minor; + + if (librariesCache[key] === true) { + // Library is being loaded, que callback + if (librariesLoadedCallbacks[key] === undefined) { + librariesLoadedCallbacks[key] = [next]; + return; + } + librariesLoadedCallbacks[key].push(next); + return; + } + else if (librariesCache[key] !== undefined) { + // Library has been loaded before. Return cache. + next(null, librariesCache[key]); + return; + } + + // Track time spent loading + var start = new Date().getTime(); + librariesCache[key] = true; + $.ajax({ + dataType: 'json', + cache: true, + url: commonInfo.libraryBaseUrl + '/' + key + }).fail(function () { + self.io += new Date().getTime() - start; + next(commonInfo.errorData.replace('%lib', name + ' ' + version)); + }).done(function (library) { + self.io += new Date().getTime() - start; + librariesCache[key] = library; + next(null, library); + + if (librariesLoadedCallbacks[key] !== undefined) { + for (var i = 0; i < librariesLoadedCallbacks[key].length; i++) { + librariesLoadedCallbacks[key][i](null, library); + } + } + delete librariesLoadedCallbacks[key]; + }); + }; + + /** + * Load script with upgrade hooks. + * + * @param {String} url + * @param {Function} next + */ + ContentUpgrade.prototype.loadScript = function (url, next) { + var self = this; + + if (scriptsCache[url] !== undefined) { + next(); + return; + } + + // Track time spent loading + var start = new Date().getTime(); + $.ajax({ + dataType: 'script', + cache: true, + url: url + }).fail(function () { + self.io += new Date().getTime() - start; + next(true); + }).done(function () { + scriptsCache[url] = true; + self.io += new Date().getTime() - start; + next(); + }); + }; + + /** + * + */ + ContentUpgrade.prototype.printError = function (error) { + var self = this; + + switch (error.type) { + case 'errorParamsBroken': + error = commonInfo.errorContent.replace('%id', error.id) + ' ' + commonInfo.errorParamsBroken; + break; + + case 'libraryMissing': + error = commonInfo.errorLibrary.replace('%lib', error.library); + break; + + case 'scriptMissing': + error = commonInfo.errorScript.replace('%lib', error.library); + break; + + case 'errorTooHighVersion': + error = commonInfo.errorContent.replace('%id', error.id) + ' ' + commonInfo.errorTooHighVersion.replace('%used', error.used).replace('%supported', error.supported); + break; + + case 'errorNotSupported': + error = commonInfo.errorContent.replace('%id', error.id) + ' ' + commonInfo.errorNotSupported.replace('%used', error.used); + break; + } + + $('<li>' + commonInfo.error + '<br/>' + error + '</li>').appendTo($log); + }; + +})(H5P.jQuery, H5P.Version); diff --git a/lang/en/hvp.php b/lang/en/hvp.php index 2c9eeab7..04c94970 100644 --- a/lang/en/hvp.php +++ b/lang/en/hvp.php @@ -675,3 +675,16 @@ $string['mobileapp:settings:mobiledebugging_help'] = 'For the bundled mobile render method only. A comma separated list of user ids who should have their hvp mobile javascript logs send back to error_log. Note users entered here will not see any visible difference. Ensure the user has purged the apps cache if this was recently enabled for them.'; $string['mobileoptions'] = 'Mobile app options'; +// Update all libraries. +$string['updatealllibraries'] = 'Update libraries'; +$string['updatealllibrariesconfirm'] = 'Do you wish to update all libraries (exluding restricted libraries)?'; + +// Upgrade all content. +$string['upgradebulkcontent'] = 'Upgrade all content'; +$string['upgradebulkcontentconfirm'] = 'Do you wish to upgrade all content to their latest libraries?'; +$string['upgradebulkinprogress'] = 'Upgrading {$a->from} to {$a->to}...'; +$string['upgradebulkdone'] = 'You have successfully upgraded {$a->count} content instance(s) for {$a->from}.'; + +// Export libraries. +$string['exportlibraries'] = 'Export libraries'; +$string['exportlibrarieserror'] = 'An error occured while export the libraries.'; diff --git a/library_list.php b/library_list.php index ef2a3748..c8c56411 100644 --- a/library_list.php +++ b/library_list.php @@ -62,6 +62,8 @@ $numnotfiltered = $core->h5pF->getNumNotFiltered(); $libraries = $core->h5pF->loadLibraries(); +$upgradesavailable = false; + // Add settings for each library. $settings = array(); $i = 0; @@ -99,6 +101,9 @@ 'deleteUrl' => null // Not implemented in Moodle. ); + $upgradeable = $upgradeurl && $core->h5pF->getNumContent($library->id) > 0; + $upgradesavailable = $upgradesavailable || $upgradeable; + $i++; } } @@ -140,6 +145,20 @@ // Installed Libraries List. echo '<h3 class="h5p-admin-header">' . get_string('installedlibraries', 'hvp') . '</h3>'; +echo $OUTPUT->box_start(); +if ($hubon) { + $url = new moodle_url('/mod/hvp/update_libraries.php'); + echo $OUTPUT->single_button($url, get_string('updatealllibraries', 'hvp')); +} +$url = new moodle_url('/mod/hvp/upgrade_all_content.php'); +$options = []; +if (!$upgradesavailable) { + $options['disabled'] = true; +} +echo $OUTPUT->single_button($url, get_string('upgradebulkcontent', 'hvp'), 'post', $options); +$url = new moodle_url('/mod/hvp/export_libraries.php'); +echo $OUTPUT->single_button($url, get_string('exportlibraries', 'hvp')); +echo $OUTPUT->box_end(); echo '<div id="h5p-admin-container"></div>'; echo $OUTPUT->footer(); diff --git a/update_libraries.php b/update_libraries.php new file mode 100644 index 00000000..9f04fcd5 --- /dev/null +++ b/update_libraries.php @@ -0,0 +1,140 @@ +<?php +// 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/>. + +/** + * Upgrade all libraries script. + * + * @package mod_hvp + * @author Rossco Hellmans <rosscohellmans@catalyst-au.net> + * @copyright Catalyst IT, 2021 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('NO_OUTPUT_BUFFERING', true); + +require_once('../../config.php'); +require_once($CFG->libdir . '/adminlib.php'); +require_once('locallib.php'); + +$confirm = optional_param('confirm', null, PARAM_INT); + +$returnurl = new moodle_url('/mod/hvp/library_list.php'); +$pageurl = new moodle_url('/mod/hvp/update_libraries.php'); +$PAGE->set_url($pageurl); + +admin_externalpage_setup('h5plibraries'); + +$PAGE->set_title("{$SITE->shortname}: " . get_string('libraries', 'hvp')); +echo $OUTPUT->header(); + +if ($confirm && confirm_sesskey()) { + echo $OUTPUT->heading(get_string('updatealllibraries', 'hvp')); + + // We may need extra execution time and memory. + core_php_time_limit::raise(HOURSECS); + raise_memory_limit(MEMORY_EXTRA); + + $progressbar = new progress_bar(); + $progressbar->create(); + $progressbar->update(0, 1, 'Finding libraries with updates'); + + $editor = mod_hvp\framework::instance('editor'); + $ajax = $editor->ajax; + $token = \H5PCore::createToken('editorajax'); + + // Update the hub cache first so we have the latest version info. + $ajax->core->updateContentTypeCache(); + + $sql = "SELECT DISTINCT lhc.machine_name, lhc.title, lhc.major_version, lhc.minor_version + FROM {hvp_libraries_hub_cache} lhc + JOIN {hvp_libraries} l + ON lhc.machine_name = l.machine_name + WHERE l.restricted = ?"; + $libraries = $DB->get_records_sql($sql, [0]); + $libraries = array_filter($libraries, function($library) { + global $DB; + // Find local library with same major + minor. + return !$DB->record_exists('hvp_libraries', [ + 'machine_name' => $library->machine_name, + 'major_version' => $library->major_version, + 'minor_version' => $library->minor_version, + ]); + }); + + $total = count($libraries); + $counter = 0; + + foreach ($libraries as $library) { + $progressbar->update($counter, $total, "Updating {$library->title}"); + $counter++; + + $machinename = $library->machine_name; + + // Look up content type to ensure it's valid(and to check permissions). + $contenttype = $editor->ajaxInterface->getContentTypeCache($machinename); + if (!$contenttype) { + echo $OUTPUT->notification("Unable to update {$library->title}: INVALID_CONTENT_TYPE", 'error'); + break; + } + + // Override core permission check. + $ajax->core->mayUpdateLibraries(true); + + // Retrieve content type from hub endpoint. + $endpoint = H5PHubEndpoints::CONTENT_TYPES . $machinename; + $url = H5PHubEndpoints::createURL($endpoint); + $path = $ajax->core->h5pF->getUploadedH5pPath(); + $response = $ajax->core->h5pF->fetchExternalData($url, null, true, empty($path) ? true : $path); + if (!$response) { + echo $OUTPUT->notification("Unable to update {$library->title}: DOWNLOAD_FAILED", 'error'); + break; + }; + + // Validate package. + $validator = new H5PValidator($ajax->core->h5pF, $ajax->core); + if (!$validator->isValidPackage(true, true)) { + $ajax->storage->removeTemporarilySavedFiles($path); + echo $OUTPUT->notification("Unable to update {$library->title}: VALIDATION_FAILED", 'error'); + break; + } + + // Save H5P. + $storage = new H5PStorage($ajax->core->h5pF, $ajax->core); + $storage->savePackage(null, null, true); + + // Clean up. + $ajax->storage->removeTemporarilySavedFiles($path); + } + + // Refresh content types. + $librariescache = $ajax->editor->getLatestGlobalLibrariesData(); + + $progressbar->update(1, 1, get_string('completed')); + echo $OUTPUT->single_button($returnurl, get_string('upgradereturn', 'hvp')); + $upgradeurl = new moodle_url('/mod/hvp/upgrade_all_content.php'); + echo $OUTPUT->single_button($upgradeurl, get_string('upgradebulkcontent', 'hvp')); +} else { + echo $OUTPUT->heading(get_string('confirmation', 'admin')); + $params = [ + 'confirm' => 1, + 'contextId' => context_course::instance(SITEID)->id, + ]; + $formcontinue = new single_button(new moodle_url('/mod/hvp/update_libraries.php', $params), get_string('yes')); + $formcancel = new single_button($returnurl, get_string('no')); + echo $OUTPUT->confirm(get_string('updatealllibrariesconfirm', 'hvp'), $formcontinue, $formcancel); +} + +echo $OUTPUT->footer(); diff --git a/upgrade_all_content.php b/upgrade_all_content.php new file mode 100644 index 00000000..254fe878 --- /dev/null +++ b/upgrade_all_content.php @@ -0,0 +1,134 @@ +<?php +// 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/>. + +/** + * Upgrade all content to latest libraries script. + * + * @package mod_hvp + * @author Rossco Hellmans <rosscohellmans@catalyst-au.net> + * @copyright Catalyst IT, 2021 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../../config.php'); +require_once($CFG->libdir . '/adminlib.php'); +require_once('locallib.php'); + +$confirm = optional_param('confirm', null, PARAM_INT); + +$returnurl = new moodle_url('/mod/hvp/library_list.php'); +$pageurl = new moodle_url('/mod/hvp/update_libraries.php'); +$PAGE->set_url($pageurl); + +admin_externalpage_setup('h5plibraries'); + +$PAGE->set_title("{$SITE->shortname}: " . get_string('libraries', 'hvp')); + +if ($confirm && confirm_sesskey()) { + + // We may need extra execution time and memory. + core_php_time_limit::raise(HOURSECS); + raise_memory_limit(MEMORY_EXTRA); + + $editor = mod_hvp\framework::instance('editor'); + $ajax = $editor->ajax; + $core = $ajax->core; + + // Grab all the libraries and grab any that can be upgraded. + $settings = [ + 'commonInfo' => [ + 'error' => get_string('upgradeerror', 'hvp'), + 'errorData' => get_string('upgradeerrordata', 'hvp'), + 'errorScript' => get_string('upgradeerrorscript', 'hvp'), + 'errorContent' => get_string('upgradeerrorcontent', 'hvp'), + 'errorParamsBroken' => get_string('upgradeerrorparamsbroken', 'hvp'), + 'errorLibrary' => get_string('upgradeerrormissinglibrary', 'hvp'), + 'errorTooHighVersion' => get_string('upgradeerrortoohighversion', 'hvp'), + 'errorNotSupported' => get_string('upgradeerrornotsupported', 'hvp'), + 'libraryBaseUrl' => (new moodle_url('/mod/hvp/ajax.php', + ['action' => 'getlibrarydataforupgrade']))->out(false) . '&library=', + 'scriptBaseUrl' => (new moodle_url('/lib/javascript.php/' . get_jsrev() . '/mod/hvp/library/js'))->out(false), + 'buster' => '', + ], + 'libraryInfo' => [], + ]; + $libraries = $ajax->core->h5pF->loadLibraries(); + foreach ($libraries as $versions) { + foreach ($versions as $library) { + $restricted = isset($library->restricted) && $library->restricted == 1; + if (!$restricted && $library->runnable) { + $upgrades = $core->getUpgrades($library, $versions); + $numcontents = $core->h5pF->getNumContent($library->id); + if (!empty($upgrades) && $numcontents > 0) { + $fromver = $library->major_version . '.' . $library->minor_version . '.' . $library->patch_version; + $tover = end($upgrades); + $a = (object)[ + 'from' => $library->title . ' (' . $fromver . ')', + 'to' => $library->title . ' (' . $tover . ')', + 'count' => $numcontents, + ]; + $settings['libraryInfo'][] = [ + 'message' => get_string('upgrademessage', 'hvp', $numcontents), + 'inProgress' => get_string('upgradebulkinprogress', 'hvp', $a), + 'done' => get_string('upgradebulkdone', 'hvp', $a), + 'library' => [ + 'name' => $library->machine_name, + 'version' => $library->major_version . '.' . $library->minor_version, + ], + 'versions' => $upgrades, + 'contents' => $numcontents, + 'infoUrl' => (new moodle_url('/mod/hvp/ajax.php', ['action' => 'libraryupgradeprogress', + 'library_id' => $library->id]))->out(false), + 'total' => $numcontents, + 'token' => \H5PCore::createToken('contentupgrade'), + 'upgradeTo' => array_key_last($upgrades), + ]; + } + } + } + } + + // Add JavaScripts. + hvp_admin_add_generic_css_and_js($PAGE, $settings); + $PAGE->requires->js('/mod/hvp/library/js/h5p-version.js', true); + $PAGE->requires->js('/mod/hvp/js/h5p-content-upgrade-bulk.js', true); + + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('upgradebulkcontent', 'hvp')); + + $returnbutton = $OUTPUT->single_button($returnurl, get_string('upgradereturn', 'hvp')); + if (empty($settings['libraryInfo'])) { + echo get_string('upgradenothingtodo', 'hvp'); + echo $OUTPUT->box_start(); + echo $returnbutton; + echo $OUTPUT->box_end(); + } else { + echo html_writer::tag('div', get_string('enablejavascript', 'hvp'), ['id' => 'h5p-admin-container']); + echo html_writer::tag('div', $returnbutton, ['id' => 'h5p-admin-return-button', 'style' => 'display: none;']); + } +} else { + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('confirmation', 'admin')); + $params = [ + 'confirm' => 1, + 'contextId' => context_course::instance(SITEID)->id, + ]; + $formcontinue = new single_button(new moodle_url('/mod/hvp/upgrade_all_content.php', $params), get_string('yes')); + $formcancel = new single_button($returnurl, get_string('no')); + echo $OUTPUT->confirm(get_string('upgradebulkcontentconfirm', 'hvp'), $formcontinue, $formcancel); +} + +echo $OUTPUT->footer();