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  /**
  18   * H5P player class.
  19   *
  20   * @package    core_h5p
  21   * @copyright  2019 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  defined('MOODLE_INTERNAL') || die();
  28  
  29  use core_h5p\local\library\autoloader;
  30  use core_xapi\handler;
  31  use core_xapi\local\state;
  32  use core_xapi\local\statement\item_activity;
  33  use core_xapi\local\statement\item_agent;
  34  use core_xapi\xapi_exception;
  35  
  36  /**
  37   * H5P player class, for displaying any local H5P content.
  38   *
  39   * @package    core_h5p
  40   * @copyright  2019 Sara Arjona <sara@moodle.com>
  41   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  42   */
  43  class player {
  44  
  45      /**
  46       * @var string The local H5P URL containing the .h5p file to display.
  47       */
  48      private $url;
  49  
  50      /**
  51       * @var core The H5PCore object.
  52       */
  53      private $core;
  54  
  55      /**
  56       * @var int H5P DB id.
  57       */
  58      private $h5pid;
  59  
  60      /**
  61       * @var array JavaScript requirements for this H5P.
  62       */
  63      private $jsrequires = [];
  64  
  65      /**
  66       * @var array CSS requirements for this H5P.
  67       */
  68      private $cssrequires = [];
  69  
  70      /**
  71       * @var array H5P content to display.
  72       */
  73      private $content;
  74  
  75      /**
  76       * @var string optional component name to send xAPI statements.
  77       */
  78      private $component;
  79  
  80      /**
  81       * @var string Type of embed object, div or iframe.
  82       */
  83      private $embedtype;
  84  
  85      /**
  86       * @var context The context object where the .h5p belongs.
  87       */
  88      private $context;
  89  
  90      /**
  91       * @var factory The \core_h5p\factory object.
  92       */
  93      private $factory;
  94  
  95      /**
  96       * @var stdClass The error, exception and info messages, raised while preparing and running the player.
  97       */
  98      private $messages;
  99  
 100      /**
 101       * @var bool Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions.
 102       */
 103      private $preventredirect;
 104  
 105      /**
 106       * Inits the H5P player for rendering the content.
 107       *
 108       * @param string $url Local URL of the H5P file to display.
 109       * @param \stdClass $config Configuration for H5P buttons.
 110       * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions
 111       * @param string $component optional moodle component to sent xAPI tracking
 112       * @param bool $skipcapcheck Whether capabilities should be checked or not to get the pluginfile URL because sometimes they
 113       *     might be controlled before calling this method.
 114       */
 115      public function __construct(string $url, \stdClass $config, bool $preventredirect = true, string $component = '',
 116              bool $skipcapcheck = false) {
 117          if (empty($url)) {
 118              throw new \moodle_exception('h5pinvalidurl', 'core_h5p');
 119          }
 120          $this->url = new \moodle_url($url);
 121          $this->preventredirect = $preventredirect;
 122  
 123          $this->factory = new \core_h5p\factory();
 124  
 125          $this->messages = new \stdClass();
 126  
 127          $this->component = $component;
 128  
 129          // Create \core_h5p\core instance.
 130          $this->core = $this->factory->get_core();
 131  
 132          // Get the H5P identifier linked to this URL.
 133          list($file, $this->h5pid) = api::create_content_from_pluginfile_url(
 134              $url,
 135              $config,
 136              $this->factory,
 137              $this->messages,
 138              $this->preventredirect,
 139              $skipcapcheck
 140          );
 141          if ($file) {
 142              $this->context = \context::instance_by_id($file->get_contextid());
 143              if ($this->h5pid) {
 144                  // Load the content of the H5P content associated to this $url.
 145                  $this->content = $this->core->loadContent($this->h5pid);
 146  
 147                  // Get the embedtype to use for displaying the H5P content.
 148                  $this->embedtype = core::determineEmbedType($this->content['embedType'], $this->content['library']['embedTypes']);
 149              }
 150          }
 151      }
 152  
 153      /**
 154       * Get the encoded URL for embeding this H5P content.
 155       *
 156       * @param string $url Local URL of the H5P file to display.
 157       * @param stdClass $config Configuration for H5P buttons.
 158       * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions
 159       * @param string $component optional moodle component to sent xAPI tracking
 160       * @param bool $displayedit Whether the edit button should be displayed below the H5P content.
 161       *
 162       * @return string The embedable code to display a H5P file.
 163       */
 164      public static function display(string $url, \stdClass $config, bool $preventredirect = true,
 165              string $component = '', bool $displayedit = false): string {
 166          global $OUTPUT, $CFG;
 167  
 168          $params = [
 169                  'url' => $url,
 170                  'preventredirect' => $preventredirect,
 171                  'component' => $component,
 172              ];
 173  
 174          $optparams = ['frame', 'export', 'embed', 'copyright'];
 175          foreach ($optparams as $optparam) {
 176              if (!empty($config->$optparam)) {
 177                  $params[$optparam] = $config->$optparam;
 178              }
 179          }
 180          $fileurl = new \moodle_url('/h5p/embed.php', $params);
 181  
 182          $template = new \stdClass();
 183          $template->embedurl = $fileurl->out(false);
 184  
 185          if ($displayedit) {
 186              list($originalfile, $h5p) = api::get_original_content_from_pluginfile_url($url, $preventredirect, true);
 187              if ($originalfile) {
 188                  // Check if the user can edit this content.
 189                  if (api::can_edit_content($originalfile)) {
 190                      $template->editurl = $CFG->wwwroot . '/h5p/edit.php?url=' . $url;
 191                  }
 192              }
 193          }
 194  
 195          $result = $OUTPUT->render_from_template('core_h5p/h5pembed', $template);
 196          $result .= self::get_resize_code();
 197          return $result;
 198      }
 199  
 200      /**
 201       * Get the error messages stored in our H5P framework.
 202       *
 203       * @return stdClass with framework error messages.
 204       */
 205      public function get_messages(): \stdClass {
 206          return helper::get_messages($this->messages, $this->factory);
 207      }
 208  
 209      /**
 210       * Create the H5PIntegration variable that will be included in the page. This variable is used as the
 211       * main H5P config variable.
 212       */
 213      public function add_assets_to_page() {
 214          global $PAGE, $USER;
 215  
 216          $cid = $this->get_cid();
 217          $systemcontext = \context_system::instance();
 218  
 219          $disable = array_key_exists('disable', $this->content) ? $this->content['disable'] : core::DISABLE_NONE;
 220          $displayoptions = $this->core->getDisplayOptionsForView($disable, $this->h5pid);
 221  
 222          $contenturl = \moodle_url::make_pluginfile_url($systemcontext->id, \core_h5p\file_storage::COMPONENT,
 223              \core_h5p\file_storage::CONTENT_FILEAREA, $this->h5pid, null, null);
 224          $exporturl = $this->get_export_settings($displayoptions[ core::DISPLAY_OPTION_DOWNLOAD ]);
 225          $xapiobject = item_activity::create_from_id($this->context->id);
 226  
 227          $contentsettings = [
 228              'library'         => core::libraryToString($this->content['library']),
 229              'fullScreen'      => $this->content['library']['fullscreen'],
 230              'exportUrl'       => ($exporturl instanceof \moodle_url) ? $exporturl->out(false) : '',
 231              'embedCode'       => $this->get_embed_code($this->url->out(),
 232                  $displayoptions[ core::DISPLAY_OPTION_EMBED ]),
 233              'resizeCode'      => self::get_resize_code(),
 234              'title'           => $this->content['slug'],
 235              'displayOptions'  => $displayoptions,
 236              'url'             => $xapiobject->get_data()->id,
 237              'contentUrl'      => $contenturl->out(),
 238              'metadata'        => $this->content['metadata'],
 239              'contentUserData' => [0 => ['state' => $this->get_state_data($xapiobject)]],
 240          ];
 241          // Get the core H5P assets, needed by the H5P classes to render the H5P content.
 242          $settings = $this->get_assets();
 243          $settings['contents'][$cid] = array_merge($settings['contents'][$cid], $contentsettings);
 244  
 245          // Print JavaScript settings to page.
 246          $PAGE->requires->data_for_js('H5PIntegration', $settings, true);
 247      }
 248  
 249      /**
 250       * Get the stored xAPI state to use as user data.
 251       *
 252       * @param item_activity $xapiobject
 253       * @return string The state data to pass to the player frontend
 254       */
 255      private function get_state_data(item_activity $xapiobject): string {
 256          global $USER;
 257  
 258          // Initialize the H5P content with the saved state (if it's enabled and the user has some stored state).
 259          $emptystatedata = '{}';
 260          $savestate = (bool) get_config($this->component, 'enablesavestate');
 261          if (!$savestate) {
 262              return $emptystatedata;
 263          }
 264  
 265          $xapihandler = handler::create($this->component);
 266          if (!$xapihandler) {
 267              return $emptystatedata;
 268          }
 269  
 270          // The component implements the xAPI handler, so the state can be loaded.
 271          $state = new state(
 272              item_agent::create_from_user($USER),
 273              $xapiobject,
 274              'state',
 275              null,
 276              null
 277          );
 278          try {
 279              $state = $xapihandler->load_state($state);
 280              if (!$state) {
 281                  // Check if the state has been restored from a backup for the current user.
 282                  $state = new state(
 283                      item_agent::create_from_user($USER),
 284                      $xapiobject,
 285                      'restored',
 286                      null,
 287                      null
 288                  );
 289                  $state = $xapihandler->load_state($state);
 290                  if ($state && !is_null($state->get_state_data())) {
 291                      // A restored state has been found. It will be replaced with one with the proper stateid and statedata.
 292                      $xapihandler->delete_state($state);
 293                      $state = new state(
 294                          item_agent::create_from_user($USER),
 295                          $xapiobject,
 296                          'state',
 297                          $state->jsonSerialize(),
 298                          null
 299                      );
 300                      $xapihandler->save_state($state);
 301                  }
 302              }
 303  
 304              if (!$state) {
 305                  return $emptystatedata;
 306              }
 307  
 308              if (is_null($state->get_state_data())) {
 309                  // The state content should be reset because, for instance, the content has changed.
 310                  return 'RESET';
 311              }
 312  
 313              $statedata = $state->jsonSerialize();
 314              if (is_null($statedata)) {
 315                  return $emptystatedata;
 316              }
 317  
 318              if (property_exists($statedata, 'h5p')) {
 319                  // As the H5P state doesn't always use JSON, we have added this h5p object to jsonize it.
 320                  return $statedata->h5p;
 321              }
 322          } catch (xapi_exception $exception) {
 323              return $emptystatedata;
 324          }
 325  
 326          return $emptystatedata;
 327      }
 328  
 329      /**
 330       * Outputs H5P wrapper HTML.
 331       *
 332       * @return string The HTML code to display this H5P content.
 333       */
 334      public function output(): string {
 335          global $OUTPUT, $USER;
 336  
 337          $template = new \stdClass();
 338          $template->h5pid = $this->h5pid;
 339          if ($this->embedtype === 'div') {
 340              $h5phtml = $OUTPUT->render_from_template('core_h5p/h5pdiv', $template);
 341          } else {
 342              $h5phtml = $OUTPUT->render_from_template('core_h5p/h5piframe', $template);
 343          }
 344  
 345          // Trigger capability_assigned event.
 346          \core_h5p\event\h5p_viewed::create([
 347              'objectid' => $this->h5pid,
 348              'userid' => $USER->id,
 349              'context' => $this->get_context(),
 350              'other' => [
 351                  'url' => $this->url->out(),
 352                  'time' => time()
 353              ]
 354          ])->trigger();
 355  
 356          return $h5phtml;
 357      }
 358  
 359      /**
 360       * Get the title of the H5P content to display.
 361       *
 362       * @return string the title
 363       */
 364      public function get_title(): string {
 365          return $this->content['title'];
 366      }
 367  
 368      /**
 369       * Get the context where the .h5p file belongs.
 370       *
 371       * @return context The context.
 372       */
 373      public function get_context(): \context {
 374          return $this->context;
 375      }
 376  
 377      /**
 378       * Delete an H5P package.
 379       *
 380       * @param stdClass $content The H5P package to delete.
 381       */
 382      private function delete_h5p(\stdClass $content) {
 383          $h5pstorage = $this->factory->get_storage();
 384          // Add an empty slug to the content if it's not defined, because the H5P library requires this field exists.
 385          // It's not used when deleting a package, so the real slug value is not required at this point.
 386          $content->slug = $content->slug ?? '';
 387          $h5pstorage->deletePackage( (array) $content);
 388      }
 389  
 390      /**
 391       * Export path for settings
 392       *
 393       * @param bool $downloadenabled Whether the option to export the H5P content is enabled.
 394       *
 395       * @return \moodle_url|null The URL of the exported file.
 396       */
 397      private function get_export_settings(bool $downloadenabled): ?\moodle_url {
 398  
 399          if (!$downloadenabled) {
 400              return null;
 401          }
 402  
 403          $systemcontext = \context_system::instance();
 404          $slug = $this->content['slug'] ? $this->content['slug'] . '-' : '';
 405          $filename = "{$slug}{$this->content['id']}.h5p";
 406          // We have to build the right URL.
 407          // Depending the request was made through webservice/pluginfile.php or pluginfile.php.
 408          if (strpos($this->url, '/webservice/pluginfile.php')) {
 409              $url  = \moodle_url::make_webservice_pluginfile_url(
 410                  $systemcontext->id,
 411                  \core_h5p\file_storage::COMPONENT,
 412                  \core_h5p\file_storage::EXPORT_FILEAREA,
 413                  '',
 414                  '',
 415                  $filename
 416              );
 417          } else {
 418              // If the request is made by tokenpluginfile.php we need to indicates to generate a token for current user.
 419              $includetoken = false;
 420              if (strpos($this->url, '/tokenpluginfile.php')) {
 421                  $includetoken = true;
 422              }
 423              $url  = \moodle_url::make_pluginfile_url(
 424                  $systemcontext->id,
 425                  \core_h5p\file_storage::COMPONENT,
 426                  \core_h5p\file_storage::EXPORT_FILEAREA,
 427                  '',
 428                  '',
 429                  $filename,
 430                  false,
 431                  $includetoken
 432              );
 433          }
 434  
 435          // Get the required info from the export file to be able to get the export file by third apps.
 436          $file = helper::get_export_info($filename, $url);
 437          if ($file) {
 438              $url->param('modified', $file['timemodified']);
 439          }
 440          return $url;
 441      }
 442  
 443      /**
 444       * Get the identifier for the H5P content, to be used in the arrays as index.
 445       *
 446       * @return string The identifier.
 447       */
 448      private function get_cid(): string {
 449          return 'cid-' . $this->h5pid;
 450      }
 451  
 452      /**
 453       * Get the core H5P assets, including all core H5P JavaScript and CSS.
 454       *
 455       * @return Array core H5P assets.
 456       */
 457      private function get_assets(): array {
 458          // Get core assets.
 459          $settings = helper::get_core_assets($this->component);
 460          // Added here because in the helper we don't have the h5p content id.
 461          $settings['moodleLibraryPaths'] = $this->core->get_dependency_roots($this->h5pid);
 462          // Add also the Moodle component where the results will be tracked.
 463          $settings['moodleComponent'] = $this->component;
 464          if (!empty($settings['moodleComponent'])) {
 465              $settings['reportingIsEnabled'] = true;
 466          }
 467  
 468          $cid = $this->get_cid();
 469          // The filterParameters function should be called before getting the dependencyfiles because it rebuild content
 470          // dependency cache and export file.
 471          $settings['contents'][$cid]['jsonContent'] = $this->get_filtered_parameters();
 472  
 473          $files = $this->get_dependency_files();
 474          if ($this->embedtype === 'div') {
 475              $systemcontext = \context_system::instance();
 476              $h5ppath = "/pluginfile.php/{$systemcontext->id}/core_h5p";
 477  
 478              // Schedule JavaScripts for loading through Moodle.
 479              foreach ($files['scripts'] as $script) {
 480                  $url = $script->path . $script->version;
 481  
 482                  // Add URL prefix if not external.
 483                  $isexternal = strpos($script->path, '://');
 484                  if ($isexternal === false) {
 485                      $url = $h5ppath . $url;
 486                  }
 487                  $settings['loadedJs'][] = $url;
 488                  $this->jsrequires[] = new \moodle_url($isexternal ? $url : $CFG->wwwroot . $url);
 489              }
 490  
 491              // Schedule stylesheets for loading through Moodle.
 492              foreach ($files['styles'] as $style) {
 493                  $url = $style->path . $style->version;
 494  
 495                  // Add URL prefix if not external.
 496                  $isexternal = strpos($style->path, '://');
 497                  if ($isexternal === false) {
 498                      $url = $h5ppath . $url;
 499                  }
 500                  $settings['loadedCss'][] = $url;
 501                  $this->cssrequires[] = new \moodle_url($isexternal ? $url : $CFG->wwwroot . $url);
 502              }
 503  
 504          } else {
 505              // JavaScripts and stylesheets will be loaded through h5p.js.
 506              $settings['contents'][$cid]['scripts'] = $this->core->getAssetsUrls($files['scripts']);
 507              $settings['contents'][$cid]['styles']  = $this->core->getAssetsUrls($files['styles']);
 508          }
 509          return $settings;
 510      }
 511  
 512      /**
 513       * Get filtered parameters, modifying them by the renderer if the theme implements the h5p_alter_filtered_parameters function.
 514       *
 515       * @return string Filtered parameters.
 516       */
 517      private function get_filtered_parameters(): string {
 518          global $PAGE;
 519  
 520          $safeparams = $this->core->filterParameters($this->content);
 521          $decodedparams = json_decode($safeparams);
 522          $h5poutput = $PAGE->get_renderer('core_h5p');
 523          $h5poutput->h5p_alter_filtered_parameters(
 524              $decodedparams,
 525              $this->content['library']['name'],
 526              $this->content['library']['majorVersion'],
 527              $this->content['library']['minorVersion']
 528          );
 529          $safeparams = json_encode($decodedparams);
 530  
 531          return $safeparams;
 532      }
 533  
 534      /**
 535       * Finds library dependencies of view
 536       *
 537       * @return array Files that the view has dependencies to
 538       */
 539      private function get_dependency_files(): array {
 540          global $PAGE;
 541  
 542          $preloadeddeps = $this->core->loadContentDependencies($this->h5pid, 'preloaded');
 543          $files = $this->core->getDependenciesFiles($preloadeddeps);
 544  
 545          // Add additional asset files if required.
 546          $h5poutput = $PAGE->get_renderer('core_h5p');
 547          $h5poutput->h5p_alter_scripts($files['scripts'], $preloadeddeps, $this->embedtype);
 548          $h5poutput->h5p_alter_styles($files['styles'], $preloadeddeps, $this->embedtype);
 549  
 550          return $files;
 551      }
 552  
 553      /**
 554       * Resizing script for settings
 555       *
 556       * @return string The HTML code with the resize script.
 557       */
 558      private static function get_resize_code(): string {
 559          global $OUTPUT;
 560  
 561          $template = new \stdClass();
 562          $template->resizeurl = autoloader::get_h5p_core_library_url('js/h5p-resizer.js');
 563  
 564          return $OUTPUT->render_from_template('core_h5p/h5presize', $template);
 565      }
 566  
 567      /**
 568       * Embed code for settings
 569       *
 570       * @param string $url The URL of the .h5p file.
 571       * @param bool $embedenabled Whether the option to embed the H5P content is enabled.
 572       *
 573       * @return string The HTML code to reuse this H5P content in a different place.
 574       */
 575      private function get_embed_code(string $url, bool $embedenabled): string {
 576          global $OUTPUT;
 577  
 578          if ( ! $embedenabled) {
 579              return '';
 580          }
 581  
 582          $template = new \stdClass();
 583          $template->embedurl = self::get_embed_url($url, $this->component)->out(false);
 584  
 585          return $OUTPUT->render_from_template('core_h5p/h5pembed', $template);
 586      }
 587  
 588      /**
 589       * Get the encoded URL for embeding this H5P content.
 590       * @param  string $url The URL of the .h5p file.
 591       * @param string $component optional Moodle component to send xAPI tracking
 592       *
 593       * @return \moodle_url The embed URL.
 594       */
 595      public static function get_embed_url(string $url, string $component = ''): \moodle_url {
 596          $params = ['url' => $url];
 597          if (!empty($component)) {
 598              // If component is not empty, it will be passed too, in order to allow tracking too.
 599              $params['component'] = $component;
 600          }
 601  
 602          return new \moodle_url('/h5p/embed.php', $params);
 603      }
 604  
 605      /**
 606       * Return the info export file for Mobile App.
 607       *
 608       * @return array or null
 609       */
 610      public function get_export_file(): ?array {
 611          // Get the export url.
 612          $exporturl = $this->get_export_settings(true);
 613          // Get the filename of the export url.
 614          $path = $exporturl->out_as_local_url();
 615          // Check if the URL has parameters.
 616          $parts = explode('?', $path);
 617          $path = array_shift($parts);
 618          $parts = explode('/', $path);
 619          $filename = array_pop($parts);
 620          // Get the required info from the export file to be able to get the export file by third apps.
 621          $file = helper::get_export_info($filename, $exporturl);
 622  
 623          return $file;
 624      }
 625  }