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();