Search moodle.org's
Developer Documentation

See Release Notes

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

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

   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  use core_h5p\local\library\autoloader;
  18  use core_h5p\core;
  19  use core_h5p\player;
  20  use core_h5p\factory;
  21  use core_xapi\local\statement\item_activity;
  22  
  23  /**
  24   * Generator for the core_h5p subsystem.
  25   *
  26   * @package    core_h5p
  27   * @category   test
  28   * @copyright  2019 Victor Deniz <victor@moodle.com>
  29   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  30   */
  31  class core_h5p_generator extends \component_generator_base {
  32  
  33      /** Url pointing to webservice plugin file. */
  34      public const WSPLUGINFILE = 0;
  35      /** Url pointing to token plugin file. */
  36      public const TOKENPLUGINFILE = 1;
  37      /** Url pointing to plugin file. */
  38      public const PLUGINFILE = 2;
  39  
  40      /**
  41       * Convenience function to create a file.
  42       *
  43       * @param  string $file path to a file.
  44       * @param  string $content file content.
  45       */
  46      public function create_file(string $file, string $content=''): void {
  47          $handle = fopen($file, 'w+');
  48          // File content is not relevant.
  49          if (empty($content)) {
  50              $content = hash("md5", $file);
  51          }
  52          fwrite($handle, $content);
  53          fclose($handle);
  54      }
  55  
  56      /**
  57       * Creates the file record. Currently used for the cache tests.
  58       *
  59       * @param string $type    Either 'scripts' or 'styles'.
  60       * @param string $path    Path to the file in the file system.
  61       * @param string $version Not really needed at the moment.
  62       */
  63      protected function add_libfile_to_array(string $type, string $path, string $version, &$files): void {
  64          $files[$type][] = (object)[
  65              'path' => $path,
  66              'version' => "?ver=$version"
  67          ];
  68      }
  69  
  70      /**
  71       * Create the necessary files and return an array structure for a library.
  72       *
  73       * @param  string $uploaddirectory Base directory for the library.
  74       * @param  int    $libraryid       Library id.
  75       * @param  string $machinename     Name for this library.
  76       * @param  int    $majorversion    Major version (any number will do).
  77       * @param  int    $minorversion    Minor version (any number will do).
  78       * @param  array  $langs           Languages to be included into the library.
  79       * @return array A list of library data and files that the core API will understand.
  80       */
  81      public function create_library(string $uploaddirectory, int $libraryid, string $machinename, int $majorversion,
  82              int $minorversion, ?array $langs = []): array {
  83          // Array $files used in the cache tests.
  84          $files = ['scripts' => [], 'styles' => [], 'language' => []];
  85  
  86          check_dir_exists($uploaddirectory . '/' . 'scripts');
  87          check_dir_exists($uploaddirectory . '/' . 'styles');
  88          if (!empty($langs)) {
  89              check_dir_exists($uploaddirectory . '/' . 'language');
  90          }
  91  
  92          $jsonfile = $uploaddirectory . '/' . 'library.json';
  93          $jsfile = $uploaddirectory . '/' . 'scripts/testlib.min.js';
  94          $cssfile = $uploaddirectory . '/' . 'styles/testlib.min.css';
  95          $this->create_file($jsonfile);
  96          $this->create_file($jsfile);
  97          $this->create_file($cssfile);
  98          foreach ($langs as $lang => $value) {
  99              $jsonfile = $uploaddirectory . '/' . 'language/' . $lang . '.json';
 100              $this->create_file($jsonfile, $value);
 101          }
 102  
 103          $lib = [
 104              'title' => 'Test lib',
 105              'description' => 'Test library description',
 106              'majorVersion' => $majorversion,
 107              'minorVersion' => $minorversion,
 108              'patchVersion' => 2,
 109              'machineName' => $machinename,
 110              'preloadedJs' => [
 111                  [
 112                      'path' => 'scripts' . '/' . 'testlib.min.js'
 113                  ]
 114              ],
 115              'preloadedCss' => [
 116                  [
 117                      'path' => 'styles' . '/' . 'testlib.min.css'
 118                  ]
 119              ],
 120              'uploadDirectory' => $uploaddirectory,
 121              'libraryId' => $libraryid
 122          ];
 123  
 124          $version = "{$majorversion}.{$minorversion}.2";
 125          $libname = "{$machinename}-{$majorversion}.{$minorversion}";
 126          $path = '/' . 'libraries' . '/' . $libraryid . '/' . $libname . '/' . 'scripts' . '/' . 'testlib.min.js';
 127          $this->add_libfile_to_array('scripts', $path, $version, $files);
 128          $path = '/' . 'libraries' . '/' . $libraryid .'/' . $libname . '/' . 'styles' . '/' . 'testlib.min.css';
 129          $this->add_libfile_to_array('styles', $path, $version, $files);
 130          foreach ($langs as $lang => $notused) {
 131              $path = '/' . 'libraries' . '/' . $libraryid . '/' . $libname . '/' . 'language' . '/' . $lang . '.json';
 132              $this->add_libfile_to_array('language', $path, $version, $files);
 133          }
 134  
 135          return [$lib, $files];
 136      }
 137  
 138      /**
 139       * Save the library files on the filesystem.
 140       *
 141       * @param stdClss $lib The library data
 142       */
 143      private function save_library(stdClass $lib) {
 144          // Get a temp path.
 145          $filestorage = new \core_h5p\file_storage();
 146          $temppath = $filestorage->getTmpPath();
 147  
 148          // Create and save the library files on the filesystem.
 149          $basedirectorymain = $temppath . '/' . $lib->machinename . '-' .
 150              $lib->majorversion . '.' . $lib->minorversion;
 151  
 152          list($library, $libraryfiles) = $this->create_library($basedirectorymain, $lib->id, $lib->machinename,
 153              $lib->majorversion, $lib->minorversion);
 154  
 155          $filestorage->saveLibrary($library);
 156      }
 157  
 158      /**
 159       * Populate H5P database tables with relevant data to simulate the process of adding H5P content.
 160       *
 161       * @param bool $createlibraryfiles Whether to create and store library files on the filesystem
 162       * @param array|null $filerecord The file associated to the H5P entry.
 163       * @return stdClass An object representing the added H5P records
 164       */
 165      public function generate_h5p_data(bool $createlibraryfiles = false, ?array $filerecord = null): stdClass {
 166          // Create libraries.
 167          $mainlib = $libraries[] = $this->create_library_record('MainLibrary', 'Main Lib', 1, 0, 1, '', null,
 168              'http://tutorial.org', 'http://example.org');
 169          $lib1 = $libraries[] = $this->create_library_record('Library1', 'Lib1', 2, 0, 1, '', null, null,  'http://example.org');
 170          $lib2 = $libraries[] = $this->create_library_record('Library2', 'Lib2', 2, 1, 1, '', null, 'http://tutorial.org');
 171          $lib3 = $libraries[] = $this->create_library_record('Library3', 'Lib3', 3, 2, 1, '', null, null, null, true, 0);
 172          $lib4 = $libraries[] = $this->create_library_record('Library4', 'Lib4', 1, 1);
 173          $lib5 = $libraries[] = $this->create_library_record('Library5', 'Lib5', 1, 3);
 174  
 175          if ($createlibraryfiles) {
 176              foreach ($libraries as $lib) {
 177                  // Create and save the library files on the filesystem.
 178                  $this->save_library($lib);
 179              }
 180          }
 181  
 182          // Create h5p content.
 183          $h5p = $this->create_h5p_record($mainlib->id, null, null, $filerecord);
 184          // Create h5p content library dependencies.
 185          $this->create_contents_libraries_record($h5p, $mainlib->id);
 186          $this->create_contents_libraries_record($h5p, $lib1->id);
 187          $this->create_contents_libraries_record($h5p, $lib2->id);
 188          $this->create_contents_libraries_record($h5p, $lib3->id);
 189          $this->create_contents_libraries_record($h5p, $lib4->id);
 190          // Create library dependencies for $mainlib.
 191          $this->create_library_dependency_record($mainlib->id, $lib1->id);
 192          $this->create_library_dependency_record($mainlib->id, $lib2->id);
 193          $this->create_library_dependency_record($mainlib->id, $lib3->id);
 194          // Create library dependencies for $lib1.
 195          $this->create_library_dependency_record($lib1->id, $lib2->id);
 196          $this->create_library_dependency_record($lib1->id, $lib3->id);
 197          $this->create_library_dependency_record($lib1->id, $lib4->id);
 198          // Create library dependencies for $lib3.
 199          $this->create_library_dependency_record($lib3->id, $lib5->id);
 200  
 201          return (object) [
 202              'h5pcontent' => (object) array(
 203                  'h5pid' => $h5p,
 204                  'contentdependencies' => array($mainlib, $lib1, $lib2, $lib3, $lib4)
 205              ),
 206              'mainlib' => (object) array(
 207                  'data' => $mainlib,
 208                  'dependencies' => array($lib1, $lib2, $lib3)
 209              ),
 210              'lib1' => (object) array(
 211                  'data' => $lib1,
 212                  'dependencies' => array($lib2, $lib3, $lib4)
 213              ),
 214              'lib2' => (object) array(
 215                  'data' => $lib2,
 216                  'dependencies' => array()
 217              ),
 218              'lib3' => (object) array(
 219                  'data' => $lib3,
 220                  'dependencies' => array($lib5)
 221              ),
 222              'lib4' => (object) array(
 223                  'data' => $lib4,
 224                  'dependencies' => array()
 225              ),
 226              'lib5' => (object) array(
 227                  'data' => $lib5,
 228                  'dependencies' => array()
 229              ),
 230          ];
 231      }
 232  
 233      /**
 234       * Create a record in the h5p_libraries database table.
 235       *
 236       * @param string $machinename The library machine name
 237       * @param string $title The library's name
 238       * @param int $majorversion The library's major version
 239       * @param int $minorversion The library's minor version
 240       * @param int $patchversion The library's patch version
 241       * @param string $semantics Json describing the content structure for the library
 242       * @param string $addto The plugin configuration data
 243       * @param string $tutorial The tutorial URL
 244       * @param string $examlpe The example URL
 245       * @param bool $enabled Whether the library is enabled or not
 246       * @param int $runnable Whether the library is runnable (1) or not (0)
 247       * @return stdClass An object representing the added library record
 248       */
 249      public function create_library_record(string $machinename, string $title, int $majorversion = 1,
 250              int $minorversion = 0, int $patchversion = 1, string $semantics = '', string $addto = null,
 251              string $tutorial = null, string $example = null, bool $enabled = true, int $runnable = 1): stdClass {
 252          global $DB;
 253  
 254          $content = [
 255              'machinename' => $machinename,
 256              'title' => $title,
 257              'majorversion' => $majorversion,
 258              'minorversion' => $minorversion,
 259              'patchversion' => $patchversion,
 260              'runnable' => $runnable,
 261              'fullscreen' => 1,
 262              'preloadedjs' => 'js/example.js',
 263              'preloadedcss' => 'css/example.css',
 264              'droplibrarycss' => '',
 265              'semantics' => $semantics,
 266              'addto' => $addto,
 267              'tutorial' => $tutorial,
 268              'example' => $example,
 269              'enabled' => $enabled,
 270          ];
 271  
 272          $libraryid = $DB->insert_record('h5p_libraries', $content);
 273  
 274          return $DB->get_record('h5p_libraries', ['id' => $libraryid]);
 275      }
 276  
 277      /**
 278       * Create a record in the h5p database table.
 279       *
 280       * @param int $mainlibid The ID of the content's main library
 281       * @param string $jsoncontent The content in json format
 282       * @param string $filtered The filtered content parameters
 283       * @param array|null $filerecord The file associated to the H5P entry.
 284       * @return int The ID of the added record
 285       */
 286      public function create_h5p_record(int $mainlibid, string $jsoncontent = null, string $filtered = null,
 287              ?array $filerecord = null): int {
 288          global $DB;
 289  
 290          if (!$jsoncontent) {
 291              $jsoncontent = json_encode(
 292                  array(
 293                      'text' => '<p>Dummy text<\/p>\n',
 294                      'questions' => '<p>Test question<\/p>\n'
 295                  )
 296              );
 297          }
 298  
 299          if (!$filtered) {
 300              $filtered = json_encode(
 301                  array(
 302                      'text' => 'Dummy text',
 303                      'questions' => 'Test question'
 304                  )
 305              );
 306          }
 307  
 308          // Load the H5P file into DB.
 309          $pathnamehash = sha1('pathname');
 310          $contenthash = sha1('content');
 311          if ($filerecord) {
 312              $fs = get_file_storage();
 313              if (!$fs->get_file(
 314                      $filerecord['contextid'],
 315                      $filerecord['component'],
 316                      $filerecord['filearea'],
 317                      $filerecord['itemid'],
 318                      $filerecord['filepath'],
 319                      $filerecord['filename'])) {
 320                  $file = $fs->create_file_from_string($filerecord, $jsoncontent);
 321                  $pathnamehash = $file->get_pathnamehash();
 322                  $contenthash = $file->get_contenthash();
 323                  if (array_key_exists('addxapistate', $filerecord) && $filerecord['addxapistate']) {
 324                      // Save some xAPI state associated to this H5P content.
 325                      $params = [
 326                          'component' => $filerecord['component'],
 327                          'activity' => item_activity::create_from_id($filerecord['contextid']),
 328                      ];
 329                      global $CFG;
 330                      require_once($CFG->dirroot.'/lib/xapi/tests/helper.php');
 331                      \core_xapi\test_helper::create_state($params, true);
 332                  }
 333              }
 334          }
 335  
 336          return $DB->insert_record(
 337              'h5p',
 338              [
 339                  'jsoncontent' => $jsoncontent,
 340                  'displayoptions' => 8,
 341                  'mainlibraryid' => $mainlibid,
 342                  'timecreated' => time(),
 343                  'timemodified' => time(),
 344                  'filtered' => $filtered,
 345                  'pathnamehash' => $pathnamehash,
 346                  'contenthash' => $contenthash,
 347              ]
 348          );
 349      }
 350  
 351      /**
 352       * Create a record in the h5p_contents_libraries database table.
 353       *
 354       * @param string $h5pid The ID of the H5P content
 355       * @param int $libid The ID of the library
 356       * @param string $dependencytype The dependency type
 357       * @return int The ID of the added record
 358       */
 359      public function create_contents_libraries_record(string $h5pid, int $libid,
 360              string $dependencytype = 'preloaded'): int {
 361          global $DB;
 362  
 363          return $DB->insert_record(
 364              'h5p_contents_libraries',
 365              array(
 366                  'h5pid' => $h5pid,
 367                  'libraryid' => $libid,
 368                  'dependencytype' => $dependencytype,
 369                  'dropcss' => 0,
 370                  'weight' => 1
 371              )
 372          );
 373      }
 374  
 375      /**
 376       * Create a record in the h5p_library_dependencies database table.
 377       *
 378       * @param int $libid The ID of the library
 379       * @param int $requiredlibid The ID of the required library
 380       * @param string $dependencytype The dependency type
 381       * @return int The ID of the added record
 382       */
 383      public function create_library_dependency_record(int $libid, int $requiredlibid,
 384              string $dependencytype = 'preloaded'): int {
 385          global $DB;
 386  
 387          return $DB->insert_record(
 388              'h5p_library_dependencies',
 389              array(
 390                  'libraryid' => $libid,
 391                  'requiredlibraryid' => $requiredlibid,
 392                  'dependencytype' => $dependencytype
 393              )
 394          );
 395      }
 396  
 397      /**
 398       * Create H5P content type records in the h5p_libraries database table.
 399       *
 400       * @param array $typestonotinstall H5P content types that should not be installed
 401       * @param core $core h5p_test_core instance required to use the exttests URL
 402       * @return array Data of the content types not installed.
 403       *
 404       * @throws invalid_response_exception If request to get the latest content types fails (usually due to a transient error)
 405       */
 406      public function create_content_types(array $typestonotinstall, core $core): array {
 407          global $DB;
 408  
 409          autoloader::register();
 410  
 411          // Get info of latest content types versions.
 412          $response = $core->get_latest_content_types();
 413          if (!empty($response->error)) {
 414              throw new invalid_response_exception($response->error);
 415          }
 416  
 417          $installedtypes = 0;
 418  
 419          // Fake installation of all other H5P content types.
 420          foreach ($response->contentTypes as $contenttype) {
 421              // Don't install pending content types.
 422              if (in_array($contenttype->id, $typestonotinstall)) {
 423                  continue;
 424              }
 425              $library = [
 426                  'machinename' => $contenttype->id,
 427                  'majorversion' => $contenttype->version->major,
 428                  'minorversion' => $contenttype->version->minor,
 429                  'patchversion' => $contenttype->version->patch,
 430                  'runnable' => 1,
 431                  'coremajor' => $contenttype->coreApiVersionNeeded->major,
 432                  'coreminor' => $contenttype->coreApiVersionNeeded->minor
 433              ];
 434              $DB->insert_record('h5p_libraries', (object) $library);
 435              $installedtypes++;
 436          }
 437  
 438          return [$installedtypes, count($typestonotinstall)];
 439      }
 440  
 441      /**
 442       * Add a record on files table for a file that belongs to
 443       *
 444       * @param string $file File name and path inside the filearea.
 445       * @param string $filearea The filearea in which the file is ("editor" or "content").
 446       * @param int $contentid Id of the H5P content to which the file belongs. null if the file is in the editor.
 447       *
 448       * @return stored_file;
 449       * @throws coding_exception
 450       */
 451      public function create_content_file(string $file, string $filearea, int $contentid = 0): stored_file {
 452          global $USER;
 453  
 454          $filepath = '/'.dirname($file).'/';
 455          $filename = basename($file);
 456  
 457          if (($filearea === 'content') && ($contentid == 0)) {
 458              throw new coding_exception('Files belonging to an H5P content must specify the H5P content id');
 459          }
 460  
 461          if ($filearea === 'draft') {
 462              $usercontext = \context_user::instance($USER->id);
 463              $context = $usercontext->id;
 464              $component = 'user';
 465              $itemid = 0;
 466          } else {
 467              $systemcontext = context_system::instance();
 468              $context = $systemcontext->id;
 469              $component = \core_h5p\file_storage::COMPONENT;
 470              $itemid = $contentid;
 471          }
 472  
 473          $content = 'fake content';
 474  
 475          $filerecord = array(
 476              'contextid' => $context,
 477              'component' => $component,
 478              'filearea'  => $filearea,
 479              'itemid'    => $itemid,
 480              'filepath'  => $filepath,
 481              'filename'  => $filename,
 482          );
 483  
 484          $fs = new file_storage();
 485          return $fs->create_file_from_string($filerecord, $content);
 486      }
 487  
 488      /**
 489       * Create a fake export H5P deployed file.
 490       *
 491       * @param string $filename Name of the H5P file to deploy.
 492       * @param int $contextid Context id of the H5P activity.
 493       * @param string $component component.
 494       * @param string $filearea file area.
 495       * @param int $typeurl Type of url to create the export url plugin file.
 496       * @return array return deployed file information.
 497       */
 498      public function create_export_file(string $filename, int $contextid,
 499          string $component,
 500          string $filearea,
 501          int $typeurl = self::WSPLUGINFILE): array {
 502          global $CFG;
 503  
 504          // We need the autoloader for H5P player.
 505          autoloader::register();
 506  
 507          $path = $CFG->dirroot.'/h5p/tests/fixtures/'.$filename;
 508          $filerecord = [
 509              'contextid' => $contextid,
 510              'component' => $component,
 511              'filearea'  => $filearea,
 512              'itemid'    => 0,
 513              'filepath'  => '/',
 514              'filename'  => $filename,
 515          ];
 516          // Load the h5p file into DB.
 517          $fs = get_file_storage();
 518          if (!$fs->get_file($contextid, $component, $filearea, $filerecord['itemid'], $filerecord['filepath'], $filename)) {
 519              $fs->create_file_from_pathname($filerecord, $path);
 520          }
 521  
 522          // Make the URL to pass to the player.
 523          if ($typeurl == self::WSPLUGINFILE) {
 524              $url = \moodle_url::make_webservice_pluginfile_url(
 525                  $filerecord['contextid'],
 526                  $filerecord['component'],
 527                  $filerecord['filearea'],
 528                  $filerecord['itemid'],
 529                  $filerecord['filepath'],
 530                  $filerecord['filename']
 531              );
 532          } else {
 533              $includetoken = false;
 534              if ($typeurl == self::TOKENPLUGINFILE) {
 535                  $includetoken = true;
 536              }
 537              $url = \moodle_url::make_pluginfile_url(
 538                  $filerecord['contextid'],
 539                  $filerecord['component'],
 540                  $filerecord['filearea'],
 541                  $filerecord['itemid'],
 542                  $filerecord['filepath'],
 543                  $filerecord['filename'],
 544                  false,
 545                  $includetoken
 546              );
 547          }
 548  
 549          $config = new stdClass();
 550          $h5pplayer = new player($url->out(false), $config);
 551          // We need to add assets to page to create the export file.
 552          $h5pplayer->add_assets_to_page();
 553  
 554          // Call the method. We need the id of the new H5P content.
 555          $rc = new \ReflectionClass(player::class);
 556          $rcp = $rc->getProperty('h5pid');
 557          $rcp->setAccessible(true);
 558          $h5pid = $rcp->getValue($h5pplayer);
 559  
 560          // Get the info export file.
 561          $factory = new factory();
 562          $core = $factory->get_core();
 563          $content = $core->loadContent($h5pid);
 564          $slug = $content['slug'] ? $content['slug'] . '-' : '';
 565          $exportfilename = "{$slug}{$h5pid}.h5p";
 566          $fileh5p = $core->fs->get_export_file($exportfilename);
 567          $deployedfile = [];
 568          $deployedfile['filename'] = $fileh5p->get_filename();
 569          $deployedfile['filepath'] = $fileh5p->get_filepath();
 570          $deployedfile['mimetype'] = $fileh5p->get_mimetype();
 571          $deployedfile['filesize'] = $fileh5p->get_filesize();
 572          $deployedfile['timemodified'] = $fileh5p->get_timemodified();
 573  
 574          // Create the url depending the request was made through typeurl.
 575          if ($typeurl == self::WSPLUGINFILE) {
 576              $url  = \moodle_url::make_webservice_pluginfile_url(
 577                  $fileh5p->get_contextid(),
 578                  $fileh5p->get_component(),
 579                  $fileh5p->get_filearea(),
 580                  '',
 581                  '',
 582                  $fileh5p->get_filename()
 583              );
 584          } else {
 585              $includetoken = false;
 586              if ($typeurl == self::TOKENPLUGINFILE) {
 587                  $includetoken = true;
 588              }
 589              $url = \moodle_url::make_pluginfile_url(
 590                  $fileh5p->get_contextid(),
 591                  $fileh5p->get_component(),
 592                  $fileh5p->get_filearea(),
 593                  '',
 594                  '',
 595                  $fileh5p->get_filename(),
 596                  false,
 597                  $includetoken
 598              );
 599          }
 600          $deployedfile['fileurl'] = $url->out(false);
 601  
 602          return $deployedfile;
 603      }
 604  }