Merge branch 'MDL-68899-master' of git://github.com/mihailges/moodle
[moodle.git] / h5p / classes / editor.php
CommitLineData
eeb90e7e
VDF
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
25namespace core_h5p;
26
27use core_h5p\local\library\autoloader;
28use core_h5p\output\h5peditor as editor_renderer;
29use H5PCore;
30use H5peditor;
31use stdClass;
32use coding_exception;
33use MoodleQuickForm;
34
35defined('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 */
44class 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 print_error('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 print_error('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
eeb90e7e
VDF
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 // Delete old file if any.
310 if (!empty($this->oldfile)) {
311 $this->oldfile->delete();
312 }
313 // Create new file.
314 if (empty($this->filearea['filename'])) {
315 $this->filearea['filename'] = $contentarray['slug'] . '.h5p';
316 }
317 $newfile = $fs->create_file_from_storedfile($this->filearea, $file);
318 if (empty($this->oldcontent)) {
319 $pathnamehash = $newfile->get_pathnamehash();
320 } else {
321 $pathnamehash = $this->oldcontent['pathnamehash'];
322 }
323
324 // Update hash fields in the h5p table.
325 $fields['pathnamehash'] = $pathnamehash;
326 $this->core->h5pF->updateContentFields($contentarray['id'], $fields);
327 }
328 }
329
330 /**
331 * Add required assets for displaying the editor.
332 *
333 * @return void
334 * @throws coding_exception If page header is already printed.
335 */
336 private function add_assets_to_page(): void {
337 global $PAGE, $CFG;
338
339 if ($PAGE->headerprinted) {
340 throw new coding_exception('H5P assets cannot be added when header is already printed.');
341 }
342
343 $context = \context_system::instance();
344
345 $settings = helper::get_core_assets();
346
347 // Use jQuery and styles from core.
348 $assets = [
349 'css' => $settings['core']['styles'],
350 'js' => $settings['core']['scripts']
351 ];
352
353 // Use relative URL to support both http and https.
354 $url = autoloader::get_h5p_editor_library_url()->out();
355 $url = '/' . preg_replace('/^[^:]+:\/\/[^\/]+\//', '', $url);
356
357 // Make sure files are reloaded for each plugin update.
358 $cachebuster = helper::get_cache_buster();
359
360 // Add editor styles.
361 foreach (H5peditor::$styles as $style) {
362 $assets['css'][] = $url . $style . $cachebuster;
363 }
364
365 // Add editor JavaScript.
366 foreach (H5peditor::$scripts as $script) {
367 // We do not want the creator of the iframe inside the iframe.
368 if ($script !== 'scripts/h5peditor-editor.js') {
369 $assets['js'][] = $url . $script . $cachebuster;
370 }
371 }
372
373 // Add JavaScript with library framework integration (editor part).
374 $PAGE->requires->js(autoloader::get_h5p_editor_library_url('scripts/h5peditor-editor.js' . $cachebuster), true);
375 $PAGE->requires->js(autoloader::get_h5p_editor_library_url('scripts/h5peditor-init.js' . $cachebuster), true);
376
a42a9ad9 377 // Load editor translations.
eeb90e7e 378 $language = framework::get_language();
a42a9ad9
SA
379 $editorstrings = $this->get_editor_translations($language);
380 $PAGE->requires->data_for_js('H5PEditor.language.core', $editorstrings, false);
eeb90e7e
VDF
381
382 // Add JavaScript settings.
383 $root = $CFG->wwwroot;
384 $filespathbase = "{$root}/pluginfile.php/{$context->id}/core_h5p/";
385
386 $factory = new factory();
387 $contentvalidator = $factory->get_content_validator();
388
389 $editorajaxtoken = core::createToken(editor_ajax::EDITOR_AJAX_TOKEN);
6157f593 390 $sesskey = sesskey();
eeb90e7e
VDF
391 $settings['editor'] = [
392 'filesPath' => $filespathbase . 'editor',
393 'fileIcon' => [
394 'path' => $url . 'images/binary-file.png',
395 'width' => 50,
396 'height' => 50,
397 ],
6157f593 398 'ajaxPath' => $CFG->wwwroot . "/h5p/ajax.php?sesskey={$sesskey}&token={$editorajaxtoken}&action=",
eeb90e7e
VDF
399 'libraryUrl' => $url,
400 'copyrightSemantics' => $contentvalidator->getCopyrightSemantics(),
401 'metadataSemantics' => $contentvalidator->getMetadataSemantics(),
402 'assets' => $assets,
403 'apiVersion' => H5PCore::$coreApi,
404 'language' => $language,
405 ];
406
407 if (!empty($this->id)) {
408 $settings['editor']['nodeVersionId'] = $this->id;
409
410 // Override content URL.
411 $contenturl = "{$root}/pluginfile.php/{$context->id}/core_h5p/content/{$this->id}";
412 $settings['contents']['cid-' . $this->id]['contentUrl'] = $contenturl;
413 }
414
415 $PAGE->requires->data_for_js('H5PIntegration', $settings, true);
416 }
417
a42a9ad9
SA
418 /**
419 * Get editor translations for the defined language.
420 * Check if the editor strings have been translated in Moodle.
421 * If the strings exist, they will override the existing ones in the JS file.
422 *
423 * @param string $language The language for the translations to be returned.
424 * @return array The editor string translations.
425 */
426 private function get_editor_translations(string $language): array {
427 global $CFG;
428
429 // Add translations.
430 $languagescript = "language/{$language}.js";
431
432 if (!file_exists("{$CFG->dirroot}" . autoloader::get_h5p_editor_library_base($languagescript))) {
433 $languagescript = 'language/en.js';
434 }
435
436 // Check if the editor strings have been translated in Moodle.
437 // If the strings exist, they will override the existing ones in the JS file.
438
439 // Get existing strings from current JS language file.
440 $langcontent = file_get_contents("{$CFG->dirroot}" . autoloader::get_h5p_editor_library_base($languagescript));
441
442 // Get only the content between { } (for instance, ; at the end of the file has to be removed).
443 $langcontent = substr($langcontent, 0, strpos($langcontent, '}', -0) + 1);
444 $langcontent = substr($langcontent, strpos($langcontent, '{'));
445
446 // Parse the JS language content and get a PHP array.
447 $editorstrings = helper::parse_js_array($langcontent);
448 foreach ($editorstrings as $key => $value) {
449 $stringkey = 'editor:'.strtolower(trim($key));
450 $value = autoloader::get_h5p_string($stringkey, $language);
451 if (!empty($value)) {
452 $editorstrings[$key] = $value;
453 }
454 }
455
456 return $editorstrings;
457 }
458
eeb90e7e
VDF
459 /**
460 * Preprocess the data sent through the form to the H5P JS Editor Library.
461 *
462 * @return stdClass
463 */
464 private function data_preprocessing(): stdClass {
465
466 $defaultvalues = [
467 'id' => $this->id,
468 'h5plibrary' => $this->library,
469 ];
470
471 // In case both contentid and library have values, content(edition) takes precedence over library(creation).
472 if (empty($this->oldcontent)) {
473 $maincontentdata = ['params' => (object)[]];
474 } else {
475 $params = $this->core->filterParameters($this->oldcontent);
476 $maincontentdata = ['params' => json_decode($params)];
477 if (isset($this->oldcontent['metadata'])) {
478 $maincontentdata['metadata'] = $this->oldcontent['metadata'];
479 }
480 }
481
482 $defaultvalues['h5pparams'] = json_encode($maincontentdata, true);
483
484 return (object) $defaultvalues;
485 }
486}