2 // This file is part of Moodle - http://moodle.org/
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.
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/>.
21 * @copyright 2020 Victor Deniz <victor@moodle.com>
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 use core_h5p\local\library\autoloader
;
28 use core_h5p\output\h5peditor
as editor_renderer
;
35 defined('MOODLE_INTERNAL') ||
die();
38 * H5P editor class, for editing local H5P content.
41 * @copyright 2020 Victor Deniz <victor@moodle.com>
42 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
47 * @var core The H5PCore object.
52 * @var H5peditor $h5peditor The H5P Editor object.
57 * @var int Id of the H5P content from the h5p table.
62 * @var array Existing H5P content instance before edition.
64 private $oldcontent = null;
67 * @var stored_file File of ane existing H5P content before edition.
69 private $oldfile = null;
72 * @var array File area to save the file of a new H5P content.
74 private $filearea = null;
77 * @var string H5P Library name
79 private $library = null;
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();
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.
101 public function set_content(int $id): void
{
104 // Load the present content.
105 $this->oldcontent
= $this->core
->loadContent($id);
106 if ($this->oldcontent
=== null) {
107 print_error('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);
118 print_error('invalidelementid');
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).
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).
169 private function set_filearea(int $contextid, string $component, string $filearea,
170 int $itemid, string $filepath = '/', ?
string $filename = null, ?
int $userid = null): void
{
174 'contextid' => $contextid,
175 'component' => $component,
176 'filearea' => $filearea,
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
191 public function add_editor_to_form(MoodleQuickForm
$mform): void
{
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'];
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
);
255 if (empty($params->metadata
)) {
256 $params->metadata
= new stdClass();
259 if (empty($params->metadata
->title
)) {
260 // Use a default string if not available.
261 $params->metadata
->title
= 'Untitled';
264 if (!isset($content->title
)) {
265 $content->title
= $params->metadata
->title
;
268 $content->params
= json_encode($params);
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);
283 * Creates or updates the H5P file and the related database data.
285 * @param stdClass $content Object containing all the necessary data.
289 private function update_h5p_file(stdClass
$content): void
{
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();
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
;
317 $newfile = $fs->create_file_from_storedfile($this->filearea
, $file);
319 if (empty($this->oldcontent
)) {
320 $pathnamehash = $newfile->get_pathnamehash();
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.
335 * @throws coding_exception If page header is already printed.
337 private function add_assets_to_page(): void
{
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.
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(),
395 'path' => $url . 'images/binary-file.png',
399 'ajaxPath' => $CFG->wwwroot
. "/h5p/ajax.php?sesskey={$sesskey}&token={$editorajaxtoken}&action=",
400 'libraryUrl' => $url,
401 'copyrightSemantics' => $contentvalidator->getCopyrightSemantics(),
402 'metadataSemantics' => $contentvalidator->getMetadataSemantics(),
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 {
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.
465 private function data_preprocessing(): stdClass
{
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)[]];
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;