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]

   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 editor class.
  19   *
  20   * @package    core_h5p
  21   * @copyright  2020 Victor Deniz <victor@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_h5p\local\library\autoloader;
  28  use core_h5p\output\h5peditor as editor_renderer;
  29  use Moodle\H5PCore;
  30  use Moodle\H5peditor;
  31  use stdClass;
  32  use coding_exception;
  33  use MoodleQuickForm;
  34  
  35  defined('MOODLE_INTERNAL') || die();
  36  
  37  /**
  38   * H5P editor class, for editing local H5P content.
  39   *
  40   * @package    core_h5p
  41   * @copyright  2020 Victor Deniz <victor@moodle.com>
  42   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  43   */
  44  class editor {
  45  
  46      /**
  47       * @var core The H5PCore object.
  48       */
  49      private $core;
  50  
  51      /**
  52       * @var H5peditor $h5peditor The H5P Editor object.
  53       */
  54      private $h5peditor;
  55  
  56      /**
  57       * @var int Id of the H5P content from the h5p table.
  58       */
  59      private $id = null;
  60  
  61      /**
  62       * @var array Existing H5P content instance before edition.
  63       */
  64      private $oldcontent = null;
  65  
  66      /**
  67       * @var stored_file File of ane existing H5P content before edition.
  68       */
  69      private $oldfile = null;
  70  
  71      /**
  72       * @var array File area to save the file of a new H5P content.
  73       */
  74      private $filearea = null;
  75  
  76      /**
  77       * @var string H5P Library name
  78       */
  79      private $library = null;
  80  
  81      /**
  82       * Inits the H5P editor.
  83       */
  84      public function __construct() {
  85          autoloader::register();
  86  
  87          $factory = new factory();
  88          $this->h5peditor = $factory->get_editor();
  89          $this->core = $factory->get_core();
  90      }
  91  
  92      /**
  93       * Loads an existing content for edition.
  94       *
  95       * If the H5P content or its file can't be retrieved, it is not possible to edit the content.
  96       *
  97       * @param int $id Id of the H5P content from the h5p table.
  98       *
  99       * @return void
 100       */
 101      public function set_content(int $id): void {
 102          $this->id = $id;
 103  
 104          // Load the present content.
 105          $this->oldcontent = $this->core->loadContent($id);
 106          if ($this->oldcontent === null) {
 107              throw new \moodle_exception('invalidelementid');
 108          }
 109  
 110          // Identify the content type library.
 111          $this->library = H5PCore::libraryToString($this->oldcontent['library']);
 112  
 113          // Get current file and its file area.
 114          $pathnamehash = $this->oldcontent['pathnamehash'];
 115          $fs = get_file_storage();
 116          $oldfile = $fs->get_file_by_hash($pathnamehash);
 117          if (!$oldfile) {
 118              throw new \moodle_exception('invalidelementid');
 119          }
 120          $this->set_filearea(
 121              $oldfile->get_contextid(),
 122              $oldfile->get_component(),
 123              $oldfile->get_filearea(),
 124              $oldfile->get_itemid(),
 125              $oldfile->get_filepath(),
 126              $oldfile->get_filename(),
 127              $oldfile->get_userid()
 128          );
 129          $this->oldfile = $oldfile;
 130      }
 131  
 132      /**
 133       * Sets the content type library and the file area to create a new H5P content.
 134       *
 135       * Note: this method must be used to create new content, to edit an existing
 136       * H5P content use only set_content with the ID from the H5P table.
 137       *
 138       * @param string $library Library of the H5P content type to create.
 139       * @param int $contextid Context where the file of the H5P content will be stored.
 140       * @param string $component Component where the file of the H5P content will be stored.
 141       * @param string $filearea File area where the file of the H5P content will be stored.
 142       * @param int $itemid Item id file of the H5P content.
 143       * @param string $filepath File path where the file of the H5P content will be stored.
 144       * @param null|string $filename H5P content file name.
 145       * @param null|int $userid H5P content file owner userid (default will use $USER->id).
 146       *
 147       * @return void
 148       */
 149      public function set_library(string $library, int $contextid, string $component, string $filearea,
 150              ?int $itemid = 0, string $filepath = '/', ?string $filename = null, ?int $userid = null): void {
 151  
 152          $this->library = $library;
 153          $this->set_filearea($contextid, $component, $filearea, $itemid, $filepath, $filename, $userid);
 154      }
 155  
 156      /**
 157       * Sets the Moodle file area where the file of a new H5P content will be stored.
 158       *
 159       * @param int $contextid Context where the file of the H5P content will be stored.
 160       * @param string $component Component where the file of the H5P content will be stored.
 161       * @param string $filearea File area where the file of the H5P content will be stored.
 162       * @param int $itemid Item id file of the H5P content.
 163       * @param string $filepath File path where the file of the H5P content will be stored.
 164       * @param null|string $filename H5P content file name.
 165       * @param null|int $userid H5P content file owner userid (default will use $USER->id).
 166       *
 167       * @return void
 168       */
 169      private function set_filearea(int $contextid, string $component, string $filearea,
 170              int $itemid, string $filepath = '/', ?string $filename = null, ?int $userid = null): void {
 171          global $USER;
 172  
 173          $this->filearea = [
 174              'contextid' => $contextid,
 175              'component' => $component,
 176              'filearea' => $filearea,
 177              'itemid' => $itemid,
 178              'filepath' => $filepath,
 179              'filename' => $filename,
 180              'userid' => $userid ?? $USER->id,
 181          ];
 182      }
 183  
 184      /**
 185       * Adds an H5P editor to a form.
 186       *
 187       * @param MoodleQuickForm $mform Moodle Quick Form
 188       *
 189       * @return void
 190       */
 191      public function add_editor_to_form(MoodleQuickForm $mform): void {
 192          global $PAGE;
 193  
 194          $this->add_assets_to_page();
 195  
 196          $data = $this->data_preprocessing();
 197  
 198          // Hidden fields used bu H5P editor.
 199          $mform->addElement('hidden', 'h5plibrary', $data->h5plibrary);
 200          $mform->setType('h5plibrary', PARAM_RAW);
 201  
 202          $mform->addElement('hidden', 'h5pparams', $data->h5pparams);
 203          $mform->setType('h5pparams', PARAM_RAW);
 204  
 205          $mform->addElement('hidden', 'h5paction');
 206          $mform->setType('h5paction', PARAM_ALPHANUMEXT);
 207  
 208          // Render H5P editor.
 209          $ui = new editor_renderer($data);
 210          $editorhtml = $PAGE->get_renderer('core_h5p')->render($ui);
 211          $mform->addElement('html', $editorhtml);
 212      }
 213  
 214      /**
 215       * Creates or updates an H5P content.
 216       *
 217       * @param stdClass $content Object containing all the necessary data.
 218       *
 219       * @return int Content id
 220       */
 221      public function save_content(stdClass $content): int {
 222  
 223          if (empty($content->h5pparams)) {
 224              throw new coding_exception('Missing H5P params.');
 225          }
 226  
 227          if (!isset($content->h5plibrary)) {
 228              throw new coding_exception('Missing H5P library.');
 229          }
 230  
 231          $content->params = $content->h5pparams;
 232  
 233          if (!empty($this->oldcontent)) {
 234              $content->id = $this->oldcontent['id'];
 235              // Get old parameters for comparison.
 236              $oldparams = json_decode($this->oldcontent['params']) ?? null;
 237              // Keep the existing display options.
 238              $content->disable = $this->oldcontent['disable'];
 239              $oldlib = $this->oldcontent['library'];
 240          } else {
 241              $oldparams = null;
 242              $oldlib = null;
 243          }
 244  
 245          // Prepare library data to be save.
 246          $content->library = H5PCore::libraryFromString($content->h5plibrary);
 247          $content->library['libraryId'] = $this->core->h5pF->getLibraryId($content->library['machineName'],
 248              $content->library['majorVersion'],
 249              $content->library['minorVersion']);
 250  
 251          // Prepare current parameters.
 252          $params = json_decode($content->params);
 253  
 254          $modified = false;
 255          if (empty($params->metadata)) {
 256              $params->metadata = new stdClass();
 257              $modified = true;
 258          }
 259          if (empty($params->metadata->title)) {
 260              // Use a default string if not available.
 261              $params->metadata->title = 'Untitled';
 262              $modified = true;
 263          }
 264          if (!isset($content->title)) {
 265              $content->title = $params->metadata->title;
 266          }
 267          if ($modified) {
 268              $content->params = json_encode($params);
 269          }
 270  
 271          // Save content.
 272          $content->id = $this->core->saveContent((array)$content);
 273  
 274          // Move any uploaded images or files. Determine content dependencies.
 275          $this->h5peditor->processParameters($content, $content->library, $params->params, $oldlib, $oldparams);
 276  
 277          $this->update_h5p_file($content);
 278  
 279          return $content->id;
 280      }
 281  
 282      /**
 283       * Creates or updates the H5P file and the related database data.
 284       *
 285       * @param stdClass $content Object containing all the necessary data.
 286       *
 287       * @return void
 288       */
 289      private function update_h5p_file(stdClass $content): void {
 290          global $USER;
 291  
 292          // Keep title before filtering params.
 293          $title = $content->title;
 294          $contentarray = $this->core->loadContent($content->id);
 295          $contentarray['title'] = $title;
 296  
 297          // Generates filtered params and export file.
 298          $this->core->filterParameters($contentarray);
 299  
 300          $slug = isset($contentarray['slug']) ? $contentarray['slug'] . '-' : '';
 301          $filename = $contentarray['id'] ?? $contentarray['title'];
 302          $filename = $slug . $filename . '.h5p';
 303          $file = $this->core->fs->get_export_file($filename);
 304          $fs = get_file_storage();
 305  
 306          if ($file) {
 307              $fields['contenthash'] = $file->get_contenthash();
 308  
 309              // Create or update H5P file.
 310              if (empty($this->filearea['filename'])) {
 311                  $this->filearea['filename'] = $contentarray['slug'] . '.h5p';
 312              }
 313              if (!empty($this->oldfile)) {
 314                  $this->oldfile->replace_file_with($file);
 315                  $newfile = $this->oldfile;
 316              } else {
 317                  $newfile = $fs->create_file_from_storedfile($this->filearea, $file);
 318              }
 319              if (empty($this->oldcontent)) {
 320                  $pathnamehash = $newfile->get_pathnamehash();
 321              } else {
 322                  $pathnamehash = $this->oldcontent['pathnamehash'];
 323              }
 324  
 325              // Update hash fields in the h5p table.
 326              $fields['pathnamehash'] = $pathnamehash;
 327              $this->core->h5pF->updateContentFields($contentarray['id'], $fields);
 328          }
 329      }
 330  
 331      /**
 332       * Add required assets for displaying the editor.
 333       *
 334       * @return void
 335       * @throws coding_exception If page header is already printed.
 336       */
 337      private function add_assets_to_page(): void {
 338          global $PAGE, $CFG;
 339  
 340          if ($PAGE->headerprinted) {
 341              throw new coding_exception('H5P assets cannot be added when header is already printed.');
 342          }
 343  
 344          $context = \context_system::instance();
 345  
 346          $settings = helper::get_core_assets();
 347  
 348          // Use jQuery and styles from core.
 349          $assets = [
 350              'css' => $settings['core']['styles'],
 351              'js' => $settings['core']['scripts']
 352          ];
 353  
 354          // Use relative URL to support both http and https.
 355          $url = autoloader::get_h5p_editor_library_url()->out();
 356          $url = '/' . preg_replace('/^[^:]+:\/\/[^\/]+\//', '', $url);
 357  
 358          // Make sure files are reloaded for each plugin update.
 359          $cachebuster = helper::get_cache_buster();
 360  
 361          // Add editor styles.
 362          foreach (H5peditor::$styles as $style) {
 363              $assets['css'][] = $url . $style . $cachebuster;
 364          }
 365  
 366          // Add editor JavaScript.
 367          foreach (H5peditor::$scripts as $script) {
 368              // We do not want the creator of the iframe inside the iframe.
 369              if ($script !== 'scripts/h5peditor-editor.js') {
 370                  $assets['js'][] = $url . $script . $cachebuster;
 371              }
 372          }
 373  
 374          // Add JavaScript with library framework integration (editor part).
 375          $PAGE->requires->js(autoloader::get_h5p_editor_library_url('scripts/h5peditor-editor.js' . $cachebuster), true);
 376          $PAGE->requires->js(autoloader::get_h5p_editor_library_url('scripts/h5peditor-init.js' . $cachebuster), true);
 377  
 378          // Load editor translations.
 379          $language = framework::get_language();
 380          $editorstrings = $this->get_editor_translations($language);
 381          $PAGE->requires->data_for_js('H5PEditor.language.core', $editorstrings, false);
 382  
 383          // Add JavaScript settings.
 384          $root = $CFG->wwwroot;
 385          $filespathbase = \moodle_url::make_draftfile_url(0, '', '');
 386  
 387          $factory = new factory();
 388          $contentvalidator = $factory->get_content_validator();
 389  
 390          $editorajaxtoken = core::createToken(editor_ajax::EDITOR_AJAX_TOKEN);
 391          $sesskey = sesskey();
 392          $settings['editor'] = [
 393              'filesPath' => $filespathbase->out(),
 394              'fileIcon' => [
 395                  'path' => $url . 'images/binary-file.png',
 396                  'width' => 50,
 397                  'height' => 50,
 398              ],
 399              'ajaxPath' => $CFG->wwwroot . "/h5p/ajax.php?sesskey={$sesskey}&token={$editorajaxtoken}&action=",
 400              'libraryUrl' => $url,
 401              'copyrightSemantics' => $contentvalidator->getCopyrightSemantics(),
 402              'metadataSemantics' => $contentvalidator->getMetadataSemantics(),
 403              'assets' => $assets,
 404              'apiVersion' => H5PCore::$coreApi,
 405              'language' => $language,
 406          ];
 407  
 408          if (!empty($this->id)) {
 409              $settings['editor']['nodeVersionId'] = $this->id;
 410  
 411              // Override content URL.
 412              $contenturl = "{$root}/pluginfile.php/{$context->id}/core_h5p/content/{$this->id}";
 413              $settings['contents']['cid-' . $this->id]['contentUrl'] = $contenturl;
 414          }
 415  
 416          $PAGE->requires->data_for_js('H5PIntegration', $settings, true);
 417      }
 418  
 419      /**
 420       * Get editor translations for the defined language.
 421       * Check if the editor strings have been translated in Moodle.
 422       * If the strings exist, they will override the existing ones in the JS file.
 423       *
 424       * @param string $language The language for the translations to be returned.
 425       * @return array The editor string translations.
 426       */
 427      private function get_editor_translations(string $language): array {
 428          global $CFG;
 429  
 430          // Add translations.
 431          $languagescript = "language/{$language}.js";
 432  
 433          if (!file_exists("{$CFG->dirroot}" . autoloader::get_h5p_editor_library_base($languagescript))) {
 434              $languagescript = 'language/en.js';
 435          }
 436  
 437          // Check if the editor strings have been translated in Moodle.
 438          // If the strings exist, they will override the existing ones in the JS file.
 439  
 440          // Get existing strings from current JS language file.
 441          $langcontent = file_get_contents("{$CFG->dirroot}" . autoloader::get_h5p_editor_library_base($languagescript));
 442  
 443          // Get only the content between { } (for instance, ; at the end of the file has to be removed).
 444          $langcontent = substr($langcontent, 0, strpos($langcontent, '}', -0) + 1);
 445          $langcontent = substr($langcontent, strpos($langcontent, '{'));
 446  
 447          // Parse the JS language content and get a PHP array.
 448          $editorstrings = helper::parse_js_array($langcontent);
 449          foreach ($editorstrings as $key => $value) {
 450              $stringkey = 'editor:'.strtolower(trim($key));
 451              $value = autoloader::get_h5p_string($stringkey, $language);
 452              if (!empty($value)) {
 453                  $editorstrings[$key] = $value;
 454              }
 455          }
 456  
 457          return $editorstrings;
 458      }
 459  
 460      /**
 461       * Preprocess the data sent through the form to the H5P JS Editor Library.
 462       *
 463       * @return stdClass
 464       */
 465      private function data_preprocessing(): stdClass {
 466  
 467          $defaultvalues = [
 468              'id' => $this->id,
 469              'h5plibrary' => $this->library,
 470          ];
 471  
 472          // In case both contentid and library have values, content(edition) takes precedence over library(creation).
 473          if (empty($this->oldcontent)) {
 474              $maincontentdata = ['params' => (object)[]];
 475          } else {
 476              $params = $this->core->filterParameters($this->oldcontent);
 477              $maincontentdata = ['params' => json_decode($params)];
 478              if (isset($this->oldcontent['metadata'])) {
 479                  $maincontentdata['metadata'] = $this->oldcontent['metadata'];
 480              }
 481          }
 482  
 483          $defaultvalues['h5pparams'] = json_encode($maincontentdata, true);
 484  
 485          return (object) $defaultvalues;
 486      }
 487  }