MDL-67814 core_h5p: added renderer and editor classes
[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
231 if ($content->h5plibrary != $this->library) {
232 throw new coding_exception("Wrong H5P library.");
233 }
234
235 $content->params = $content->h5pparams;
236
237 if (!empty($this->oldcontent)) {
238 $content->id = $this->oldcontent['id'];
239 // Get old parameters for comparison.
240 $oldparams = json_decode($this->oldcontent['params']) ?? null;
241 // Keep the existing display options.
242 $content->disable = $this->oldcontent['disable'];
243 $oldlib = $this->oldcontent['library'];
244 } else {
245 $oldparams = null;
246 $oldlib = null;
247 }
248
249 // Prepare library data to be save.
250 $content->library = H5PCore::libraryFromString($content->h5plibrary);
251 $content->library['libraryId'] = $this->core->h5pF->getLibraryId($content->library['machineName'],
252 $content->library['majorVersion'],
253 $content->library['minorVersion']);
254
255 // Prepare current parameters.
256 $params = json_decode($content->params);
257
258 $modified = false;
259 if (empty($params->metadata)) {
260 $params->metadata = new stdClass();
261 $modified = true;
262 }
263 if (empty($params->metadata->title)) {
264 // Use a default string if not available.
265 $params->metadata->title = 'Untitled';
266 $modified = true;
267 }
268 if (!isset($content->title)) {
269 $content->title = $params->metadata->title;
270 }
271 if ($modified) {
272 $content->params = json_encode($params);
273 }
274
275 // Save content.
276 $content->id = $this->core->saveContent((array)$content);
277
278 // Move any uploaded images or files. Determine content dependencies.
279 $this->h5peditor->processParameters($content, $content->library, $params->params, $oldlib, $oldparams);
280
281 $this->update_h5p_file($content);
282
283 return $content->id;
284 }
285
286 /**
287 * Creates or updates the H5P file and the related database data.
288 *
289 * @param stdClass $content Object containing all the necessary data.
290 *
291 * @return void
292 */
293 private function update_h5p_file(stdClass $content): void {
294 global $USER;
295
296 // Keep title before filtering params.
297 $title = $content->title;
298 $contentarray = $this->core->loadContent($content->id);
299 $contentarray['title'] = $title;
300
301 // Generates filtered params and export file.
302 $this->core->filterParameters($contentarray);
303
304 $slug = isset($contentarray['slug']) ? $contentarray['slug'] . '-' : '';
305 $filename = $contentarray['id'] ?? $contentarray['title'];
306 $filename = $slug . $filename . '.h5p';
307 $file = $this->core->fs->get_export_file($filename);
308 $fs = get_file_storage();
309
310 if ($file) {
311 $fields['contenthash'] = $file->get_contenthash();
312
313 // Delete old file if any.
314 if (!empty($this->oldfile)) {
315 $this->oldfile->delete();
316 }
317 // Create new file.
318 if (empty($this->filearea['filename'])) {
319 $this->filearea['filename'] = $contentarray['slug'] . '.h5p';
320 }
321 $newfile = $fs->create_file_from_storedfile($this->filearea, $file);
322 if (empty($this->oldcontent)) {
323 $pathnamehash = $newfile->get_pathnamehash();
324 } else {
325 $pathnamehash = $this->oldcontent['pathnamehash'];
326 }
327
328 // Update hash fields in the h5p table.
329 $fields['pathnamehash'] = $pathnamehash;
330 $this->core->h5pF->updateContentFields($contentarray['id'], $fields);
331 }
332 }
333
334 /**
335 * Add required assets for displaying the editor.
336 *
337 * @return void
338 * @throws coding_exception If page header is already printed.
339 */
340 private function add_assets_to_page(): void {
341 global $PAGE, $CFG;
342
343 if ($PAGE->headerprinted) {
344 throw new coding_exception('H5P assets cannot be added when header is already printed.');
345 }
346
347 $context = \context_system::instance();
348
349 $settings = helper::get_core_assets();
350
351 // Use jQuery and styles from core.
352 $assets = [
353 'css' => $settings['core']['styles'],
354 'js' => $settings['core']['scripts']
355 ];
356
357 // Use relative URL to support both http and https.
358 $url = autoloader::get_h5p_editor_library_url()->out();
359 $url = '/' . preg_replace('/^[^:]+:\/\/[^\/]+\//', '', $url);
360
361 // Make sure files are reloaded for each plugin update.
362 $cachebuster = helper::get_cache_buster();
363
364 // Add editor styles.
365 foreach (H5peditor::$styles as $style) {
366 $assets['css'][] = $url . $style . $cachebuster;
367 }
368
369 // Add editor JavaScript.
370 foreach (H5peditor::$scripts as $script) {
371 // We do not want the creator of the iframe inside the iframe.
372 if ($script !== 'scripts/h5peditor-editor.js') {
373 $assets['js'][] = $url . $script . $cachebuster;
374 }
375 }
376
377 // Add JavaScript with library framework integration (editor part).
378 $PAGE->requires->js(autoloader::get_h5p_editor_library_url('scripts/h5peditor-editor.js' . $cachebuster), true);
379 $PAGE->requires->js(autoloader::get_h5p_editor_library_url('scripts/h5peditor-init.js' . $cachebuster), true);
380
381 // Add translations.
382 $language = framework::get_language();
383 $languagescript = "language/{$language}.js";
384
385 if (!file_exists("{$CFG->dirroot}" . autoloader::get_h5p_editor_library_base($languagescript))) {
386 $languagescript = 'language/en.js';
387 }
388 $PAGE->requires->js(autoloader::get_h5p_editor_library_url($languagescript . $cachebuster),
389 true);
390
391 // Add JavaScript settings.
392 $root = $CFG->wwwroot;
393 $filespathbase = "{$root}/pluginfile.php/{$context->id}/core_h5p/";
394
395 $factory = new factory();
396 $contentvalidator = $factory->get_content_validator();
397
398 $editorajaxtoken = core::createToken(editor_ajax::EDITOR_AJAX_TOKEN);
399 $settings['editor'] = [
400 'filesPath' => $filespathbase . 'editor',
401 'fileIcon' => [
402 'path' => $url . 'images/binary-file.png',
403 'width' => 50,
404 'height' => 50,
405 ],
406 'ajaxPath' => $CFG->wwwroot . '/h5p/' . "ajax.php?contextId={$context->id}&token={$editorajaxtoken}&action=",
407 'libraryUrl' => $url,
408 'copyrightSemantics' => $contentvalidator->getCopyrightSemantics(),
409 'metadataSemantics' => $contentvalidator->getMetadataSemantics(),
410 'assets' => $assets,
411 'apiVersion' => H5PCore::$coreApi,
412 'language' => $language,
413 ];
414
415 if (!empty($this->id)) {
416 $settings['editor']['nodeVersionId'] = $this->id;
417
418 // Override content URL.
419 $contenturl = "{$root}/pluginfile.php/{$context->id}/core_h5p/content/{$this->id}";
420 $settings['contents']['cid-' . $this->id]['contentUrl'] = $contenturl;
421 }
422
423 $PAGE->requires->data_for_js('H5PIntegration', $settings, true);
424 }
425
426 /**
427 * Preprocess the data sent through the form to the H5P JS Editor Library.
428 *
429 * @return stdClass
430 */
431 private function data_preprocessing(): stdClass {
432
433 $defaultvalues = [
434 'id' => $this->id,
435 'h5plibrary' => $this->library,
436 ];
437
438 // In case both contentid and library have values, content(edition) takes precedence over library(creation).
439 if (empty($this->oldcontent)) {
440 $maincontentdata = ['params' => (object)[]];
441 } else {
442 $params = $this->core->filterParameters($this->oldcontent);
443 $maincontentdata = ['params' => json_decode($params)];
444 if (isset($this->oldcontent['metadata'])) {
445 $maincontentdata['metadata'] = $this->oldcontent['metadata'];
446 }
447 }
448
449 $defaultvalues['h5pparams'] = json_encode($maincontentdata, true);
450
451 return (object) $defaultvalues;
452 }
453}