See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body