Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Contains API class for the H5P area.
  19   *
  20   * @package    core_h5p
  21   * @copyright  2020 Sara Arjona <sara@moodle.com>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace core_h5p;
  26  
  27  use core\lock\lock_config;
  28  use Moodle\H5PCore;
  29  
  30  /**
  31   * Contains API class for the H5P area.
  32   *
  33   * @copyright  2020 Sara Arjona <sara@moodle.com>
  34   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class api {
  37  
  38      /**
  39       * Delete a library and also all the libraries depending on it and the H5P contents using it. For the H5P content, only the
  40       * database entries in {h5p} are removed (the .h5p files are not removed in order to let users to deploy them again).
  41       *
  42       * @param  factory   $factory The H5P factory.
  43       * @param  \stdClass $library The library to delete.
  44       */
  45      public static function delete_library(factory $factory, \stdClass $library): void {
  46          global $DB;
  47  
  48          // Get the H5P contents using this library, to remove them from DB. The .h5p files won't be removed
  49          // so they will be displayed by the player next time a user with the proper permissions accesses it.
  50          $sql = 'SELECT DISTINCT hcl.h5pid
  51                    FROM {h5p_contents_libraries} hcl
  52                   WHERE hcl.libraryid = :libraryid';
  53          $params = ['libraryid' => $library->id];
  54          $h5pcontents = $DB->get_records_sql($sql, $params);
  55          foreach ($h5pcontents as $h5pcontent) {
  56              $factory->get_framework()->deleteContentData($h5pcontent->h5pid);
  57          }
  58  
  59          $fs = $factory->get_core()->fs;
  60          $framework = $factory->get_framework();
  61          // Delete the library from the file system.
  62          $fs->delete_library(array('libraryId' => $library->id));
  63          // Delete also the cache assets to rebuild them next time.
  64          $framework->deleteCachedAssets($library->id);
  65  
  66          // Remove library data from database.
  67          $DB->delete_records('h5p_library_dependencies', array('libraryid' => $library->id));
  68          $DB->delete_records('h5p_libraries', array('id' => $library->id));
  69  
  70          // Remove the library from the cache.
  71          $libscache = \cache::make('core', 'h5p_libraries');
  72          $libarray = [
  73              'machineName' => $library->machinename,
  74              'majorVersion' => $library->majorversion,
  75              'minorVersion' => $library->minorversion,
  76          ];
  77          $libstring = H5PCore::libraryToString($libarray);
  78          $librarykey = helper::get_cache_librarykey($libstring);
  79          $libscache->delete($librarykey);
  80  
  81          // Remove the libraries using this library.
  82          $requiredlibraries = self::get_dependent_libraries($library->id);
  83          foreach ($requiredlibraries as $requiredlibrary) {
  84              self::delete_library($factory, $requiredlibrary);
  85          }
  86      }
  87  
  88      /**
  89       * Get all the libraries using a defined library.
  90       *
  91       * @param  int    $libraryid The library to get its dependencies.
  92       * @return array  List of libraryid with all the libraries required by a defined library.
  93       */
  94      public static function get_dependent_libraries(int $libraryid): array {
  95          global $DB;
  96  
  97          $sql = 'SELECT *
  98                    FROM {h5p_libraries}
  99                   WHERE id IN (SELECT DISTINCT hl.id
 100                                  FROM {h5p_library_dependencies} hld
 101                                  JOIN {h5p_libraries} hl ON hl.id = hld.libraryid
 102                                 WHERE hld.requiredlibraryid = :libraryid)';
 103          $params = ['libraryid' => $libraryid];
 104  
 105          return $DB->get_records_sql($sql, $params);
 106      }
 107  
 108      /**
 109       * Get a library from an identifier.
 110       *
 111       * @param  int    $libraryid The library identifier.
 112       * @return \stdClass The library object having the library identifier defined.
 113       * @throws dml_exception A DML specific exception is thrown if the libraryid doesn't exist.
 114       */
 115      public static function get_library(int $libraryid): \stdClass {
 116          global $DB;
 117  
 118          return $DB->get_record('h5p_libraries', ['id' => $libraryid], '*', MUST_EXIST);
 119      }
 120  
 121      /**
 122       * Returns a library as an object with properties that correspond to the fetched row's field names.
 123       *
 124       * @param array $params An associative array with the values of the machinename, majorversion and minorversion fields.
 125       * @param bool $configurable A library that has semantics so it can be configured in the editor.
 126       * @param string $fields Library attributes to retrieve.
 127       *
 128       * @return \stdClass|null An object with one attribute for each field name in $fields param.
 129       */
 130      public static function get_library_details(array $params, bool $configurable, string $fields = ''): ?\stdClass {
 131          global $DB;
 132  
 133          $select = "machinename = :machinename
 134                     AND majorversion = :majorversion
 135                     AND minorversion = :minorversion";
 136  
 137          if ($configurable) {
 138              $select .= " AND semantics IS NOT NULL";
 139          }
 140  
 141          $fields = $fields ?: '*';
 142  
 143          $record = $DB->get_record_select('h5p_libraries', $select, $params, $fields);
 144  
 145          return $record ?: null;
 146      }
 147  
 148      /**
 149       * Get all the H5P content type libraries versions.
 150       *
 151       * @param string|null $fields Library fields to return.
 152       *
 153       * @return array An array with an object for each content type library installed.
 154       */
 155      public static function get_contenttype_libraries(?string $fields = ''): array {
 156          global $DB;
 157  
 158          $libraries = [];
 159          $fields = $fields ?: '*';
 160          $select = "runnable = :runnable
 161                     AND semantics IS NOT NULL";
 162          $params = ['runnable' => 1];
 163          $sort = 'title, majorversion DESC, minorversion DESC';
 164  
 165          $records = $DB->get_records_select('h5p_libraries', $select, $params, $sort, $fields);
 166  
 167          $added = [];
 168          foreach ($records as $library) {
 169              // Remove unique index.
 170              unset($library->id);
 171  
 172              // Convert snakes to camels.
 173              $library->majorVersion = (int) $library->majorversion;
 174              unset($library->major_version);
 175              $library->minorVersion = (int) $library->minorversion;
 176              unset($library->minorversion);
 177              $library->metadataSettings = json_decode($library->metadatasettings ?? '');
 178  
 179              // If we already add this library means that it is an old version,as the previous query was sorted by version.
 180              if (isset($added[$library->name])) {
 181                  $library->isOld = true;
 182              } else {
 183                  $added[$library->name] = true;
 184              }
 185  
 186              // Add new library.
 187              $libraries[] = $library;
 188          }
 189  
 190          return $libraries;
 191      }
 192  
 193      /**
 194       * Get the H5P DB instance id for a H5P pluginfile URL. If it doesn't exist, it's not created.
 195       *
 196       * @param string $url H5P pluginfile URL.
 197       * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions
 198       * @param bool $skipcapcheck Whether capabilities should be checked or not to get the pluginfile URL because sometimes they
 199       *     might be controlled before calling this method.
 200       *
 201       * @return array of [file, stdClass|false]:
 202       *             - file local file for this $url.
 203       *             - stdClass is an H5P object or false if there isn't any H5P with this URL.
 204       */
 205      public static function get_content_from_pluginfile_url(string $url, bool $preventredirect = true,
 206          bool $skipcapcheck = false): array {
 207  
 208          global $DB;
 209  
 210          // Deconstruct the URL and get the pathname associated.
 211          if ($skipcapcheck || self::can_access_pluginfile_hash($url, $preventredirect)) {
 212              $pathnamehash = self::get_pluginfile_hash($url);
 213          }
 214  
 215          if (!$pathnamehash) {
 216              return [false, false];
 217          }
 218  
 219          // Get the file.
 220          $fs = get_file_storage();
 221          $file = $fs->get_file_by_hash($pathnamehash);
 222          if (!$file) {
 223              return [false, false];
 224          }
 225  
 226          $h5p = $DB->get_record('h5p', ['pathnamehash' => $pathnamehash]);
 227          return [$file, $h5p];
 228      }
 229  
 230      /**
 231       * Get the original file and H5P DB instance for a given H5P pluginfile URL. If it doesn't exist, it's not created.
 232       * If the file has been added as a reference, this method will return the original linked file.
 233       *
 234       * @param string $url H5P pluginfile URL.
 235       * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions.
 236       * @param bool $skipcapcheck Whether capabilities should be checked or not to get the pluginfile URL because sometimes they
 237       *     might be controlled before calling this method.
 238       *
 239       * @return array of [\stored_file|false, \stdClass|false, \stored_file|false]:
 240       *             - \stored_file: original local file for the given url (if it has been added as a reference, this method
 241       *                            will return the linked file) or false if there isn't any H5P file with this URL.
 242       *             - \stdClass: an H5P object or false if there isn't any H5P with this URL.
 243       *             - \stored_file: file associated to the given url (if it's different from original) or false when both files
 244       *                            (original and file) are the same.
 245       * @since Moodle 4.0
 246       */
 247      public static function get_original_content_from_pluginfile_url(string $url, bool $preventredirect = true,
 248          bool $skipcapcheck = false): array {
 249  
 250          $file = false;
 251          list($originalfile, $h5p) = self::get_content_from_pluginfile_url($url, $preventredirect, $skipcapcheck);
 252          if ($originalfile) {
 253              if ($reference = $originalfile->get_reference()) {
 254                  $file = $originalfile;
 255                  // If the file has been added as a reference to any other file, get it.
 256                  $fs = new \file_storage();
 257                  $referenced = \file_storage::unpack_reference($reference);
 258                  $originalfile = $fs->get_file(
 259                      $referenced['contextid'],
 260                      $referenced['component'],
 261                      $referenced['filearea'],
 262                      $referenced['itemid'],
 263                      $referenced['filepath'],
 264                      $referenced['filename']
 265                  );
 266                  $h5p = self::get_content_from_pathnamehash($originalfile->get_pathnamehash());
 267                  if (empty($h5p)) {
 268                      $h5p = false;
 269                  }
 270              }
 271          }
 272  
 273          return [$originalfile, $h5p, $file];
 274      }
 275  
 276      /**
 277       * Check if the user can edit an H5P file. It will return true in the following situations:
 278       * - The user is the author of the file.
 279       * - The component is different from user (i.e. private files).
 280       * - If the component is contentbank, the user can edit this file (calling the ContentBank API).
 281       * - If the component is mod_xxx or block_xxx, the user has the addinstance capability.
 282       * - If the component implements the can_edit_content in the h5p\canedit class and the callback to this method returns true.
 283       *
 284       * @param \stored_file $file The H5P file to check.
 285       *
 286       * @return boolean Whether the user can edit or not the given file.
 287       * @since Moodle 4.0
 288       */
 289      public static function can_edit_content(\stored_file $file): bool {
 290          global $USER;
 291  
 292          list($type, $component) = \core_component::normalize_component($file->get_component());
 293  
 294          // Private files.
 295          $currentuserisauthor = $file->get_userid() == $USER->id;
 296          $isuserfile = $component === 'user';
 297          if ($currentuserisauthor && $isuserfile) {
 298              // The user can edit the content because it's a private user file and she is the owner.
 299              return true;
 300          }
 301  
 302          // Check if the plugin where the file belongs implements the custom can_edit_content method and call it if that's the case.
 303          $classname = '\\' . $file->get_component() . '\\h5p\\canedit';
 304          $methodname = 'can_edit_content';
 305          if (method_exists($classname, $methodname)) {
 306              return $classname::{$methodname}($file);
 307          }
 308  
 309          // For mod/block files, check if the user has the addinstance capability of the component where the file belongs.
 310          if ($type === 'mod' || $type === 'block') {
 311              // For any other component, check whether the user can add/edit them.
 312              $context = \context::instance_by_id($file->get_contextid());
 313              $plugins = \core_component::get_plugin_list($type);
 314              $isvalid = array_key_exists($component, $plugins);
 315              if ($isvalid && has_capability("$type/$component:addinstance", $context)) {
 316                  // The user can edit the content because she has the capability for creating instances where the file belongs.
 317                  return true;
 318              }
 319          }
 320  
 321          // For contentbank files, use the API to check if the user has access.
 322          if ($component == 'contentbank') {
 323              $cb = new \core_contentbank\contentbank();
 324              $content = $cb->get_content_from_id($file->get_itemid());
 325              $contenttype = $content->get_content_type_instance();
 326              if ($contenttype instanceof \contenttype_h5p\contenttype) {
 327                  // Only H5P contenttypes should be considered here.
 328                  if ($contenttype->can_edit($content)) {
 329                      // The user has permissions to edit the H5P in the content bank.
 330                      return true;
 331                  }
 332              }
 333          }
 334  
 335          return false;
 336      }
 337  
 338      /**
 339       * Create, if it doesn't exist, the H5P DB instance id for a H5P pluginfile URL. If it exists:
 340       * - If the content is not the same, remove the existing content and re-deploy the H5P content again.
 341       * - If the content is the same, returns the H5P identifier.
 342       *
 343       * @param string $url H5P pluginfile URL.
 344       * @param stdClass $config Configuration for H5P buttons.
 345       * @param factory $factory The \core_h5p\factory object
 346       * @param stdClass $messages The error, exception and info messages, raised while preparing and running an H5P content.
 347       * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions
 348       * @param bool $skipcapcheck Whether capabilities should be checked or not to get the pluginfile URL because sometimes they
 349       *     might be controlled before calling this method.
 350       *
 351       * @return array of [file, h5pid]:
 352       *             - file local file for this $url.
 353       *             - h5pid is the H5P identifier or false if there isn't any H5P with this URL.
 354       */
 355      public static function create_content_from_pluginfile_url(string $url, \stdClass $config, factory $factory,
 356          \stdClass &$messages, bool $preventredirect = true, bool $skipcapcheck = false): array {
 357          global $USER;
 358  
 359          $core = $factory->get_core();
 360          list($file, $h5p) = self::get_content_from_pluginfile_url($url, $preventredirect, $skipcapcheck);
 361  
 362          if (!$file) {
 363              $core->h5pF->setErrorMessage(get_string('h5pfilenotfound', 'core_h5p'));
 364              return [false, false];
 365          }
 366  
 367          $contenthash = $file->get_contenthash();
 368          if ($h5p && $h5p->contenthash != $contenthash) {
 369              // The content exists and it is different from the one deployed previously. The existing one should be removed before
 370              // deploying the new version.
 371              self::delete_content($h5p, $factory);
 372              $h5p = false;
 373          }
 374  
 375          $context = \context::instance_by_id($file->get_contextid());
 376          if ($h5p) {
 377              // The H5P content has been deployed previously.
 378  
 379              // If the main library for this H5P content is disabled, the content won't be displayed.
 380              $mainlibrary = (object) ['id' => $h5p->mainlibraryid];
 381              if (!self::is_library_enabled($mainlibrary)) {
 382                  $core->h5pF->setErrorMessage(get_string('mainlibrarydisabled', 'core_h5p'));
 383                  return [$file, false];
 384              } else {
 385                  $displayoptions = helper::get_display_options($core, $config);
 386                  // Check if the user can set the displayoptions.
 387                  if ($displayoptions != $h5p->displayoptions && has_capability('moodle/h5p:setdisplayoptions', $context)) {
 388                      // If displayoptions has changed and user has permission to modify it, update this information in DB.
 389                      $core->h5pF->updateContentFields($h5p->id, ['displayoptions' => $displayoptions]);
 390                  }
 391                  return [$file, $h5p->id];
 392              }
 393          } else {
 394              // The H5P content hasn't been deployed previously.
 395  
 396              // Check if the user uploading the H5P content is "trustable". If the file hasn't been uploaded by a user with this
 397              // capability, the content won't be deployed and an error message will be displayed.
 398              if (!helper::can_deploy_package($file)) {
 399                  $core->h5pF->setErrorMessage(get_string('nopermissiontodeploy', 'core_h5p'));
 400                  return [$file, false];
 401              }
 402  
 403              // The H5P content can be only deployed if the author of the .h5p file can update libraries or if all the
 404              // content-type libraries exist, to avoid users without the h5p:updatelibraries capability upload malicious content.
 405              $onlyupdatelibs = !helper::can_update_library($file);
 406  
 407              // Start lock to prevent synchronous access to save the same H5P.
 408              $lockfactory = lock_config::get_lock_factory('core_h5p');
 409              $lockkey = 'core_h5p_' . $file->get_pathnamehash();
 410              if ($lock = $lockfactory->get_lock($lockkey, 10)) {
 411                  try {
 412                      // Validate and store the H5P content before displaying it.
 413                      $h5pid = helper::save_h5p($factory, $file, $config, $onlyupdatelibs, false);
 414                  } finally {
 415                      $lock->release();
 416                  }
 417              } else {
 418                  $core->h5pF->setErrorMessage(get_string('lockh5pdeploy', 'core_h5p'));
 419                  return [$file, false];
 420              };
 421  
 422              if (!$h5pid && $file->get_userid() != $USER->id && has_capability('moodle/h5p:updatelibraries', $context)) {
 423                  // The user has permission to update libraries but the package has been uploaded by a different
 424                  // user without this permission. Check if there is some missing required library error.
 425                  $missingliberror = false;
 426                  $messages = helper::get_messages($messages, $factory);
 427                  if (!empty($messages->error)) {
 428                      foreach ($messages->error as $error) {
 429                          if ($error->code == 'missing-required-library') {
 430                              $missingliberror = true;
 431                              break;
 432                          }
 433                      }
 434                  }
 435                  if ($missingliberror) {
 436                      // The message about the permissions to upload libraries should be removed.
 437                      $infomsg = "Note that the libraries may exist in the file you uploaded, but you're not allowed to upload " .
 438                          "new libraries. Contact the site administrator about this.";
 439                      if (($key = array_search($infomsg, $messages->info)) !== false) {
 440                          unset($messages->info[$key]);
 441                      }
 442  
 443                      // No library will be installed and an error will be displayed, because this content is not trustable.
 444                      $core->h5pF->setInfoMessage(get_string('notrustablefile', 'core_h5p'));
 445                  }
 446                  return [$file, false];
 447  
 448              }
 449              return [$file, $h5pid];
 450          }
 451      }
 452  
 453      /**
 454       * Delete an H5P package.
 455       *
 456       * @param stdClass $content The H5P package to delete with, at least content['id].
 457       * @param factory $factory The \core_h5p\factory object
 458       */
 459      public static function delete_content(\stdClass $content, factory $factory): void {
 460          $h5pstorage = $factory->get_storage();
 461  
 462          // Add an empty slug to the content if it's not defined, because the H5P library requires this field exists.
 463          // It's not used when deleting a package, so the real slug value is not required at this point.
 464          $content->slug = $content->slug ?? '';
 465          $h5pstorage->deletePackage( (array) $content);
 466      }
 467  
 468      /**
 469       * Delete an H5P package deployed from the defined $url.
 470       *
 471       * @param string $url pluginfile URL of the H5P package to delete.
 472       * @param factory $factory The \core_h5p\factory object
 473       */
 474      public static function delete_content_from_pluginfile_url(string $url, factory $factory): void {
 475          global $DB;
 476  
 477          // Get the H5P to delete.
 478          $pathnamehash = self::get_pluginfile_hash($url);
 479          $h5p = $DB->get_record('h5p', ['pathnamehash' => $pathnamehash]);
 480          if ($h5p) {
 481              self::delete_content($h5p, $factory);
 482          }
 483      }
 484  
 485      /**
 486       * If user can access pathnamehash from an H5P internal URL.
 487       *
 488       * @param  string $url H5P pluginfile URL poiting to an H5P file.
 489       * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions
 490       *
 491       * @return bool if user can access pluginfile hash.
 492       * @throws \moodle_exception
 493       * @throws \coding_exception
 494       * @throws \require_login_exception
 495       */
 496      protected static function can_access_pluginfile_hash(string $url, bool $preventredirect = true): bool {
 497          global $USER, $CFG;
 498  
 499          // Decode the URL before start processing it.
 500          $url = new \moodle_url(urldecode($url));
 501  
 502          // Remove params from the URL (such as the 'forcedownload=1'), to avoid errors.
 503          $url->remove_params(array_keys($url->params()));
 504          $path = $url->out_as_local_url();
 505  
 506          // We only need the slasharguments.
 507          $path = substr($path, strpos($path, '.php/') + 5);
 508          $parts = explode('/', $path);
 509  
 510          // If the request is made by tokenpluginfile.php we need to avoid userprivateaccesskey.
 511          if (strpos($url, '/tokenpluginfile.php')) {
 512              array_shift($parts);
 513          }
 514  
 515          // Get the contextid, component and filearea.
 516          $contextid = array_shift($parts);
 517          $component = array_shift($parts);
 518          $filearea = array_shift($parts);
 519  
 520          // Get the context.
 521          try {
 522              list($context, $course, $cm) = get_context_info_array($contextid);
 523          } catch (\moodle_exception $e) {
 524              throw new \moodle_exception('invalidcontextid', 'core_h5p');
 525          }
 526  
 527          // For CONTEXT_USER, such as the private files, raise an exception if the owner of the file is not the current user.
 528          if ($context->contextlevel == CONTEXT_USER && $USER->id !== $context->instanceid) {
 529              throw new \moodle_exception('h5pprivatefile', 'core_h5p');
 530          }
 531  
 532          if (!is_siteadmin($USER)) {
 533              // For CONTEXT_COURSECAT No login necessary - unless login forced everywhere.
 534              if ($context->contextlevel == CONTEXT_COURSECAT) {
 535                  if ($CFG->forcelogin) {
 536                      require_login(null, true, null, false, true);
 537                  }
 538              }
 539  
 540              // For CONTEXT_BLOCK.
 541              if ($context->contextlevel == CONTEXT_BLOCK) {
 542                  if ($context->get_course_context(false)) {
 543                      // If block is in course context, then check if user has capability to access course.
 544                      require_course_login($course, true, null, false, true);
 545                  } else if ($CFG->forcelogin) {
 546                      // No login necessary - unless login forced everywhere.
 547                      require_login(null, true, null, false, true);
 548                  } else {
 549                      // Get parent context and see if user have proper permission.
 550                      $parentcontext = $context->get_parent_context();
 551                      if ($parentcontext->contextlevel === CONTEXT_COURSECAT) {
 552                          // Check if category is visible and user can view this category.
 553                          if (!\core_course_category::get($parentcontext->instanceid, IGNORE_MISSING)) {
 554                              send_file_not_found();
 555                          }
 556                      } else if ($parentcontext->contextlevel === CONTEXT_USER && $parentcontext->instanceid != $USER->id) {
 557                          // The block is in the context of a user, it is only visible to the user who it belongs to.
 558                          send_file_not_found();
 559                      }
 560                      if ($filearea !== 'content') {
 561                          send_file_not_found();
 562                      }
 563                  }
 564              }
 565  
 566              // For CONTEXT_MODULE and CONTEXT_COURSE check if the user is enrolled in the course.
 567              // And for CONTEXT_MODULE has permissions view this .h5p file.
 568              if ($context->contextlevel == CONTEXT_MODULE ||
 569                  $context->contextlevel == CONTEXT_COURSE) {
 570                  // Require login to the course first (without login to the module).
 571                  require_course_login($course, true, null, !$preventredirect, $preventredirect);
 572  
 573                  // Now check if module is available OR it is restricted but the intro is shown on the course page.
 574                  if ($context->contextlevel == CONTEXT_MODULE) {
 575                      $cminfo = \cm_info::create($cm);
 576                      if (!$cminfo->uservisible) {
 577                          if (!$cm->showdescription || !$cminfo->is_visible_on_course_page()) {
 578                              // Module intro is not visible on the course page and module is not available, show access error.
 579                              require_course_login($course, true, $cminfo, !$preventredirect, $preventredirect);
 580                          }
 581                      }
 582                  }
 583              }
 584          }
 585  
 586          return true;
 587      }
 588  
 589      /**
 590       * Get the pathnamehash from an H5P internal URL.
 591       *
 592       * @param  string $url H5P pluginfile URL poiting to an H5P file.
 593       *
 594       * @return string|false pathnamehash for the file in the internal URL.
 595       *
 596       * @throws \moodle_exception
 597       */
 598      protected static function get_pluginfile_hash(string $url) {
 599  
 600          // Decode the URL before start processing it.
 601          $url = new \moodle_url(urldecode($url));
 602  
 603          // Remove params from the URL (such as the 'forcedownload=1'), to avoid errors.
 604          $url->remove_params(array_keys($url->params()));
 605          $path = $url->out_as_local_url();
 606  
 607          // We only need the slasharguments.
 608          $path = substr($path, strpos($path, '.php/') + 5);
 609          $parts = explode('/', $path);
 610          $filename = array_pop($parts);
 611  
 612          // If the request is made by tokenpluginfile.php we need to avoid userprivateaccesskey.
 613          if (strpos($url, '/tokenpluginfile.php')) {
 614              array_shift($parts);
 615          }
 616  
 617          // Get the contextid, component and filearea.
 618          $contextid = array_shift($parts);
 619          $component = array_shift($parts);
 620          $filearea = array_shift($parts);
 621  
 622          // Ignore draft files, because they are considered temporary files, so shouldn't be displayed.
 623          if ($filearea == 'draft') {
 624              return false;
 625          }
 626  
 627          // Get the context.
 628          try {
 629              list($context, $course, $cm) = get_context_info_array($contextid);
 630          } catch (\moodle_exception $e) {
 631              throw new \moodle_exception('invalidcontextid', 'core_h5p');
 632          }
 633  
 634          // Some components, such as mod_page or mod_resource, add the revision to the URL to prevent caching problems.
 635          // So the URL contains this revision number as itemid but a 0 is always stored in the files table.
 636          // In order to get the proper hash, a callback should be done (looking for those exceptions).
 637          $pathdata = null;
 638          if ($context->contextlevel == CONTEXT_MODULE || $context->contextlevel == CONTEXT_BLOCK) {
 639              $pathdata = component_callback($component, 'get_path_from_pluginfile', [$filearea, $parts], null);
 640          }
 641          if (null === $pathdata) {
 642              // Look for the components and fileareas which have empty itemid defined in xxx_pluginfile.
 643              $hasnullitemid = false;
 644              $hasnullitemid = $hasnullitemid || ($component === 'user' && ($filearea === 'private' || $filearea === 'profile'));
 645              $hasnullitemid = $hasnullitemid || (substr($component, 0, 4) === 'mod_' && $filearea === 'intro');
 646              $hasnullitemid = $hasnullitemid || ($component === 'course' &&
 647                      ($filearea === 'summary' || $filearea === 'overviewfiles'));
 648              $hasnullitemid = $hasnullitemid || ($component === 'coursecat' && $filearea === 'description');
 649              $hasnullitemid = $hasnullitemid || ($component === 'backup' &&
 650                      ($filearea === 'course' || $filearea === 'activity' || $filearea === 'automated'));
 651              if ($hasnullitemid) {
 652                  $itemid = 0;
 653              } else {
 654                  $itemid = array_shift($parts);
 655              }
 656  
 657              if (empty($parts)) {
 658                  $filepath = '/';
 659              } else {
 660                  $filepath = '/' . implode('/', $parts) . '/';
 661              }
 662          } else {
 663              // The itemid and filepath have been returned by the component callback.
 664              [
 665                  'itemid' => $itemid,
 666                  'filepath' => $filepath,
 667              ] = $pathdata;
 668          }
 669  
 670          $fs = get_file_storage();
 671          $pathnamehash = $fs->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
 672          return $pathnamehash;
 673      }
 674  
 675      /**
 676       * Returns the H5P content object corresponding to an H5P content file.
 677       *
 678       * @param string $pathnamehash The pathnamehash of the file associated to an H5P content.
 679       *
 680       * @return null|\stdClass H5P content object or null if not found.
 681       */
 682      public static function get_content_from_pathnamehash(string $pathnamehash): ?\stdClass {
 683          global $DB;
 684  
 685          $h5p = $DB->get_record('h5p', ['pathnamehash' => $pathnamehash]);
 686  
 687          return ($h5p) ? $h5p : null;
 688      }
 689  
 690      /**
 691       * Return the H5P export information file when the file has been deployed.
 692       * Otherwise, return null if H5P file:
 693       * i) has not been deployed.
 694       * ii) has changed the content.
 695       *
 696       * The information returned will be:
 697       * - filename, filepath, mimetype, filesize, timemodified and fileurl.
 698       *
 699       * @param int $contextid ContextId of the H5P activity.
 700       * @param factory $factory The \core_h5p\factory object.
 701       * @param string $component component
 702       * @param string $filearea file area
 703       * @return array|null Return file info otherwise null.
 704       */
 705      public static function get_export_info_from_context_id(int $contextid,
 706          factory $factory,
 707          string $component,
 708          string $filearea): ?array {
 709  
 710          $core = $factory->get_core();
 711          $fs = get_file_storage();
 712          $files = $fs->get_area_files($contextid, $component, $filearea, 0, 'id', false);
 713          $file = reset($files);
 714  
 715          if ($h5p = self::get_content_from_pathnamehash($file->get_pathnamehash())) {
 716              if ($h5p->contenthash == $file->get_contenthash()) {
 717                  $content = $core->loadContent($h5p->id);
 718                  $slug = $content['slug'] ? $content['slug'] . '-' : '';
 719                  $filename = "{$slug}{$content['id']}.h5p";
 720                  $deployedfile = helper::get_export_info($filename, null, $factory);
 721  
 722                  return $deployedfile;
 723              }
 724          }
 725  
 726          return null;
 727      }
 728  
 729      /**
 730       * Enable or disable a library.
 731       *
 732       * @param int $libraryid The id of the library to enable/disable.
 733       * @param bool $isenabled True if the library should be enabled; false otherwise.
 734       */
 735      public static function set_library_enabled(int $libraryid, bool $isenabled): void {
 736          global $DB;
 737  
 738          $library = $DB->get_record('h5p_libraries', ['id' => $libraryid], '*', MUST_EXIST);
 739          if ($library->runnable) {
 740              // For now, only runnable libraries can be enabled/disabled.
 741              $record = [
 742                  'id' => $libraryid,
 743                  'enabled' => $isenabled,
 744              ];
 745              $DB->update_record('h5p_libraries', $record);
 746          }
 747      }
 748  
 749      /**
 750       * Check whether a library is enabled or not. When machinename is passed, it will return false if any of the versions
 751       * for this machinename is disabled.
 752       * If the library doesn't exist, it will return true.
 753       *
 754       * @param \stdClass $librarydata Supported fields for library: 'id' and 'machichename'.
 755       * @return bool
 756       * @throws \moodle_exception
 757       */
 758      public static function is_library_enabled(\stdClass $librarydata): bool {
 759          global $DB;
 760  
 761          $params = [];
 762          if (property_exists($librarydata, 'machinename')) {
 763              $params['machinename'] = $librarydata->machinename;
 764          }
 765          if (property_exists($librarydata, 'id')) {
 766              $params['id'] = $librarydata->id;
 767          }
 768  
 769          if (empty($params)) {
 770              throw new \moodle_exception("Missing 'machinename' or 'id' in librarydata parameter");
 771          }
 772  
 773          $libraries = $DB->get_records('h5p_libraries', $params);
 774  
 775          // If any of the libraries with these values have been disabled, return false.
 776          foreach ($libraries as $id => $library) {
 777              if (!$library->enabled) {
 778                  return false;
 779              }
 780          }
 781  
 782          return true;
 783      }
 784  
 785      /**
 786       * Check whether an H5P package is valid or not.
 787       *
 788       * @param \stored_file $file The file with the H5P content.
 789       * @param bool $onlyupdatelibs Whether new libraries can be installed or only the existing ones can be updated
 790       * @param bool $skipcontent Should the content be skipped (so only the libraries will be saved)?
 791       * @param factory|null $factory The \core_h5p\factory object
 792       * @param bool $deletefiletree Should the temporary files be deleted before returning?
 793       * @return bool True if the H5P file is valid (expected format, valid libraries...); false otherwise.
 794       */
 795      public static function is_valid_package(\stored_file $file, bool $onlyupdatelibs, bool $skipcontent = false,
 796              ?factory $factory = null, bool $deletefiletree = true): bool {
 797  
 798          // This may take a long time.
 799          \core_php_time_limit::raise();
 800  
 801          $isvalid = false;
 802  
 803          if (empty($factory)) {
 804              $factory = new factory();
 805          }
 806          $core = $factory->get_core();
 807          $h5pvalidator = $factory->get_validator();
 808  
 809          // Set the H5P file path.
 810          $core->h5pF->set_file($file);
 811          $path = $core->fs->getTmpPath();
 812          $core->h5pF->getUploadedH5pFolderPath($path);
 813          // Add manually the extension to the file to avoid the validation fails.
 814          $path .= '.h5p';
 815          $core->h5pF->getUploadedH5pPath($path);
 816          // Copy the .h5p file to the temporary folder.
 817          $file->copy_content_to($path);
 818  
 819          if ($h5pvalidator->isValidPackage($skipcontent, $onlyupdatelibs)) {
 820              if ($skipcontent) {
 821                  $isvalid = true;
 822              } else if (!empty($h5pvalidator->h5pC->mainJsonData['mainLibrary'])) {
 823                  $mainlibrary = (object) ['machinename' => $h5pvalidator->h5pC->mainJsonData['mainLibrary']];
 824                  if (self::is_library_enabled($mainlibrary)) {
 825                      $isvalid = true;
 826                  } else {
 827                      // If the main library of the package is disabled, the H5P content will be considered invalid.
 828                      $core->h5pF->setErrorMessage(get_string('mainlibrarydisabled', 'core_h5p'));
 829                  }
 830              }
 831          }
 832  
 833          if ($deletefiletree) {
 834              // Remove temp content folder.
 835              H5PCore::deleteFileTree($path);
 836          }
 837  
 838          return $isvalid;
 839      }
 840  }