MDL-68909 h5p: move temporary editor files to draft area
[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
346c5169 309 // Create or update H5P file.
eeb90e7e
VDF
310 if (empty($this->filearea['filename'])) {
311 $this->filearea['filename'] = $contentarray['slug'] . '.h5p';
312 }
346c5169
FR
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 }
eeb90e7e
VDF
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
a42a9ad9 378 // Load editor translations.
eeb90e7e 379 $language = framework::get_language();
a42a9ad9
SA
380 $editorstrings = $this->get_editor_translations($language);
381 $PAGE->requires->data_for_js('H5PEditor.language.core', $editorstrings, false);
eeb90e7e
VDF
382
383 // Add JavaScript settings.
384 $root = $CFG->wwwroot;
67a11150 385 $filespathbase = \moodle_url::make_draftfile_url(0, '', '');
eeb90e7e
VDF
386
387 $factory = new factory();
388 $contentvalidator = $factory->get_content_validator();
389
390 $editorajaxtoken = core::createToken(editor_ajax::EDITOR_AJAX_TOKEN);
6157f593 391 $sesskey = sesskey();
eeb90e7e 392 $settings['editor'] = [
67a11150 393 'filesPath' => $filespathbase->out(),
eeb90e7e
VDF
394 'fileIcon' => [
395 'path' => $url . 'images/binary-file.png',
396 'width' => 50,
397 'height' => 50,
398 ],
6157f593 399 'ajaxPath' => $CFG->wwwroot . "/h5p/ajax.php?sesskey={$sesskey}&token={$editorajaxtoken}&action=",
eeb90e7e
VDF
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
a42a9ad9
SA
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
eeb90e7e
VDF
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}