weekly release 4.5dev
[moodle.git] / h5p / classes / editor.php
blobfeb3fe895e7acd160acaa3b4a7c53a64d8d792b0
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.
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/>.
17 /**
18 * H5P editor class.
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
25 namespace core_h5p;
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;
35 defined('MOODLE_INTERNAL') || die();
37 /**
38 * H5P editor class, for editing local H5P content.
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
44 class editor {
46 /**
47 * @var core The H5PCore object.
49 private $core;
51 /**
52 * @var H5peditor $h5peditor The H5P Editor object.
54 private $h5peditor;
56 /**
57 * @var int Id of the H5P content from the h5p table.
59 private $id = null;
61 /**
62 * @var array Existing H5P content instance before edition.
64 private $oldcontent = null;
66 /**
67 * @var stored_file File of ane existing H5P content before edition.
69 private $oldfile = null;
71 /**
72 * @var array File area to save the file of a new H5P content.
74 private $filearea = null;
76 /**
77 * @var string H5P Library name
79 private $library = null;
81 /**
82 * Inits the H5P editor.
84 public function __construct() {
85 autoloader::register();
87 $factory = new factory();
88 $this->h5peditor = $factory->get_editor();
89 $this->core = $factory->get_core();
92 /**
93 * Loads an existing content for edition.
95 * If the H5P content or its file can't be retrieved, it is not possible to edit the content.
97 * @param int $id Id of the H5P content from the h5p table.
99 * @return void
101 public function set_content(int $id): void {
102 $this->id = $id;
104 // Load the present content.
105 $this->oldcontent = $this->core->loadContent($id);
106 if ($this->oldcontent === null) {
107 throw new \moodle_exception('invalidelementid');
110 // Identify the content type library.
111 $this->library = H5PCore::libraryToString($this->oldcontent['library']);
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');
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()
129 $this->oldfile = $oldfile;
133 * Sets the content type library and the file area to create a new H5P content.
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.
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).
147 * @return void
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 {
152 $this->library = $library;
153 $this->set_filearea($contextid, $component, $filearea, $itemid, $filepath, $filename, $userid);
157 * Sets the Moodle file area where the file of a new H5P content will be stored.
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).
167 * @return void
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;
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,
185 * Adds an H5P editor to a form.
187 * @param MoodleQuickForm $mform Moodle Quick Form
189 * @return void
191 public function add_editor_to_form(MoodleQuickForm $mform): void {
192 global $PAGE;
194 $this->add_assets_to_page();
196 $data = $this->data_preprocessing();
198 // Hidden fields used bu H5P editor.
199 $mform->addElement('hidden', 'h5plibrary', $data->h5plibrary);
200 $mform->setType('h5plibrary', PARAM_RAW);
202 $mform->addElement('hidden', 'h5pparams', $data->h5pparams);
203 $mform->setType('h5pparams', PARAM_RAW);
205 $mform->addElement('hidden', 'h5paction');
206 $mform->setType('h5paction', PARAM_ALPHANUMEXT);
208 // Render H5P editor.
209 $ui = new editor_renderer($data);
210 $editorhtml = $PAGE->get_renderer('core_h5p')->render($ui);
211 $mform->addElement('html', $editorhtml);
215 * Creates or updates an H5P content.
217 * @param stdClass $content Object containing all the necessary data.
219 * @return int Content id
221 public function save_content(stdClass $content): int {
223 if (empty($content->h5pparams)) {
224 throw new coding_exception('Missing H5P params.');
227 if (!isset($content->h5plibrary)) {
228 throw new coding_exception('Missing H5P library.');
231 $content->params = $content->h5pparams;
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;
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']);
251 // Prepare current parameters.
252 $params = json_decode($content->params);
254 $modified = false;
255 if (empty($params->metadata)) {
256 $params->metadata = new stdClass();
257 $modified = true;
259 if (empty($params->metadata->title)) {
260 // Use a default string if not available.
261 $params->metadata->title = 'Untitled';
262 $modified = true;
264 if (!isset($content->title)) {
265 $content->title = $params->metadata->title;
267 if ($modified) {
268 $content->params = json_encode($params);
271 // Save content.
272 $content->id = $this->core->saveContent((array)$content);
274 // Move any uploaded images or files. Determine content dependencies.
275 $this->h5peditor->processParameters($content, $content->library, $params->params, $oldlib, $oldparams);
277 $this->update_h5p_file($content);
279 return $content->id;
283 * Creates or updates the H5P file and the related database data.
285 * @param stdClass $content Object containing all the necessary data.
287 * @return void
289 private function update_h5p_file(stdClass $content): void {
290 global $USER;
292 // Keep title before filtering params.
293 $title = $content->title;
294 $contentarray = $this->core->loadContent($content->id);
295 $contentarray['title'] = $title;
297 // Generates filtered params and export file.
298 $this->core->filterParameters($contentarray);
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();
306 if ($file) {
307 $fields['contenthash'] = $file->get_contenthash();
309 // Create or update H5P file.
310 if (empty($this->filearea['filename'])) {
311 $this->filearea['filename'] = $contentarray['slug'] . '.h5p';
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);
319 if (empty($this->oldcontent)) {
320 $pathnamehash = $newfile->get_pathnamehash();
321 } else {
322 $pathnamehash = $this->oldcontent['pathnamehash'];
325 // Update hash fields in the h5p table.
326 $fields['pathnamehash'] = $pathnamehash;
327 $this->core->h5pF->updateContentFields($contentarray['id'], $fields);
332 * Add required assets for displaying the editor.
334 * @return void
335 * @throws coding_exception If page header is already printed.
337 private function add_assets_to_page(): void {
338 global $PAGE, $CFG;
340 if ($PAGE->headerprinted) {
341 throw new coding_exception('H5P assets cannot be added when header is already printed.');
344 $context = \context_system::instance();
346 $settings = helper::get_core_assets();
348 // Use jQuery and styles from core.
349 $assets = [
350 'css' => $settings['core']['styles'],
351 'js' => $settings['core']['scripts']
354 // Use relative URL to support both http and https.
355 $url = autoloader::get_h5p_editor_library_url()->out();
356 $url = '/' . preg_replace('/^[^:]+:\/\/[^\/]+\//', '', $url);
358 // Make sure files are reloaded for each plugin update.
359 $cachebuster = helper::get_cache_buster();
361 // Add editor styles.
362 foreach (H5peditor::$styles as $style) {
363 $assets['css'][] = $url . $style . $cachebuster;
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;
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);
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);
383 // Add JavaScript settings.
384 $root = $CFG->wwwroot;
385 $filespathbase = \moodle_url::make_draftfile_url(0, '', '');
387 $factory = new factory();
388 $contentvalidator = $factory->get_content_validator();
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,
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,
408 if (!empty($this->id)) {
409 $settings['editor']['nodeVersionId'] = $this->id;
411 // Override content URL.
412 $contenturl = "{$root}/pluginfile.php/{$context->id}/core_h5p/content/{$this->id}";
413 $settings['contents']['cid-' . $this->id]['contentUrl'] = $contenturl;
416 $PAGE->requires->data_for_js('H5PIntegration', $settings, true);
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.
424 * @param string $language The language for the translations to be returned.
425 * @return array The editor string translations.
427 private function get_editor_translations(string $language): array {
428 global $CFG;
430 // Add translations.
431 $languagescript = "language/{$language}.js";
433 if (!file_exists("{$CFG->dirroot}" . autoloader::get_h5p_editor_library_base($languagescript))) {
434 $languagescript = 'language/en.js';
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.
440 // Get existing strings from current JS language file.
441 $langcontent = file_get_contents("{$CFG->dirroot}" . autoloader::get_h5p_editor_library_base($languagescript));
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, '{'));
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;
457 return $editorstrings;
461 * Preprocess the data sent through the form to the H5P JS Editor Library.
463 * @return stdClass
465 private function data_preprocessing(): stdClass {
467 $defaultvalues = [
468 'id' => $this->id,
469 'h5plibrary' => $this->library,
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'];
483 $defaultvalues['h5pparams'] = json_encode($maincontentdata, true);
485 return (object) $defaultvalues;