Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
 
25
namespace core_h5p;
26
 
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;
34
 
35
defined('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
 */
44
class 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
            throw new \moodle_exception('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
            throw new \moodle_exception('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
        $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
            // Create or update H5P file.
310
            if (empty($this->filearea['filename'])) {
311
                $this->filearea['filename'] = $contentarray['slug'] . '.h5p';
312
            }
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
            }
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
 
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);
382
 
383
        // Add JavaScript settings.
384
        $root = $CFG->wwwroot;
385
        $filespathbase = \moodle_url::make_draftfile_url(0, '', '');
386
 
387
        $factory = new factory();
388
        $contentvalidator = $factory->get_content_validator();
389
 
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,
398
            ],
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,
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
 
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
 
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
}