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 player class.
19
 *
20
 * @package    core_h5p
21
 * @copyright  2019 Sara Arjona <sara@moodle.com>
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace core_h5p;
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
use core_h5p\local\library\autoloader;
30
use core_xapi\handler;
31
use core_xapi\local\state;
32
use core_xapi\local\statement\item_activity;
33
use core_xapi\local\statement\item_agent;
34
use core_xapi\xapi_exception;
35
 
36
/**
37
 * H5P player class, for displaying any local H5P content.
38
 *
39
 * @package    core_h5p
40
 * @copyright  2019 Sara Arjona <sara@moodle.com>
41
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42
 */
43
class player {
44
 
45
    /**
46
     * @var string The local H5P URL containing the .h5p file to display.
47
     */
48
    private $url;
49
 
50
    /**
51
     * @var core The H5PCore object.
52
     */
53
    private $core;
54
 
55
    /**
56
     * @var int H5P DB id.
57
     */
58
    private $h5pid;
59
 
60
    /**
61
     * @var array JavaScript requirements for this H5P.
62
     */
63
    private $jsrequires = [];
64
 
65
    /**
66
     * @var array CSS requirements for this H5P.
67
     */
68
    private $cssrequires = [];
69
 
70
    /**
71
     * @var array H5P content to display.
72
     */
73
    private $content;
74
 
75
    /**
76
     * @var string optional component name to send xAPI statements.
77
     */
78
    private $component;
79
 
80
    /**
81
     * @var string Type of embed object, div or iframe.
82
     */
83
    private $embedtype;
84
 
85
    /**
86
     * @var context The context object where the .h5p belongs.
87
     */
88
    private $context;
89
 
90
    /**
91
     * @var factory The \core_h5p\factory object.
92
     */
93
    private $factory;
94
 
95
    /**
96
     * @var stdClass The error, exception and info messages, raised while preparing and running the player.
97
     */
98
    private $messages;
99
 
100
    /**
101
     * @var bool Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions.
102
     */
103
    private $preventredirect;
104
 
105
    /**
106
     * Inits the H5P player for rendering the content.
107
     *
108
     * @param string $url Local URL of the H5P file to display.
109
     * @param \stdClass $config Configuration for H5P buttons.
110
     * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions
111
     * @param string $component optional moodle component to sent xAPI tracking
112
     * @param bool $skipcapcheck Whether capabilities should be checked or not to get the pluginfile URL because sometimes they
113
     *     might be controlled before calling this method.
114
     */
115
    public function __construct(string $url, \stdClass $config, bool $preventredirect = true, string $component = '',
116
            bool $skipcapcheck = false) {
117
        if (empty($url)) {
118
            throw new \moodle_exception('h5pinvalidurl', 'core_h5p');
119
        }
120
        $this->url = new \moodle_url($url);
121
        $this->preventredirect = $preventredirect;
122
 
123
        $this->factory = new \core_h5p\factory();
124
 
125
        $this->messages = new \stdClass();
126
 
127
        $this->component = $component;
128
 
129
        // Create \core_h5p\core instance.
130
        $this->core = $this->factory->get_core();
131
 
132
        // Get the H5P identifier linked to this URL.
133
        list($file, $this->h5pid) = api::create_content_from_pluginfile_url(
134
            $url,
135
            $config,
136
            $this->factory,
137
            $this->messages,
138
            $this->preventredirect,
139
            $skipcapcheck
140
        );
141
        if ($file) {
142
            $this->context = \context::instance_by_id($file->get_contextid());
143
            if ($this->h5pid) {
144
                // Load the content of the H5P content associated to this $url.
145
                $this->content = $this->core->loadContent($this->h5pid);
146
 
147
                // Get the embedtype to use for displaying the H5P content.
148
                $this->embedtype = core::determineEmbedType($this->content['embedType'], $this->content['library']['embedTypes']);
149
            }
150
        }
151
    }
152
 
153
    /**
154
     * Get the encoded URL for embeding this H5P content.
155
     *
156
     * @param string $url Local URL of the H5P file to display.
157
     * @param \stdClass $config Configuration for H5P buttons.
158
     * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions
159
     * @param string $component optional moodle component to sent xAPI tracking
160
     * @param bool $displayedit Whether the edit button should be displayed below the H5P content.
161
     * @param \action_link[] $extraactions Extra actions to display above the H5P content.
162
     *
163
     * @return string The embedable code to display a H5P file.
164
     */
165
    public static function display(
166
        string $url,
167
        \stdClass $config,
168
        bool $preventredirect = true,
169
        string $component = '',
170
        bool $displayedit = false,
171
        array $extraactions = [],
172
    ): string {
173
        global $OUTPUT, $CFG;
174
 
175
        $params = [
176
                'url' => $url,
177
                'preventredirect' => $preventredirect,
178
                'component' => $component,
179
            ];
180
 
181
        $optparams = ['frame', 'export', 'embed', 'copyright'];
182
        foreach ($optparams as $optparam) {
183
            if (!empty($config->$optparam)) {
184
                $params[$optparam] = $config->$optparam;
185
            }
186
        }
187
        $fileurl = new \moodle_url('/h5p/embed.php', $params);
188
 
189
        $template = new \stdClass();
190
        $template->embedurl = $fileurl->out(false);
191
 
192
        if ($displayedit) {
193
            list($originalfile, $h5p) = api::get_original_content_from_pluginfile_url($url, $preventredirect, true);
194
            if ($originalfile) {
195
                // Check if the user can edit this content.
196
                if (api::can_edit_content($originalfile)) {
197
                    $template->editurl = (new \moodle_url('/h5p/edit.php', ['url' => $url]))->out(false);
198
                }
199
            }
200
        }
201
 
202
        $template->extraactions = [];
203
        foreach ($extraactions as $action) {
204
            $template->extraactions[] = $action->export_for_template($OUTPUT);
205
        }
206
        $result = $OUTPUT->render_from_template('core_h5p/h5pembed', $template);
207
        $result .= self::get_resize_code();
208
        return $result;
209
    }
210
 
211
    /**
212
     * Get the error messages stored in our H5P framework.
213
     *
214
     * @return stdClass with framework error messages.
215
     */
216
    public function get_messages(): \stdClass {
217
        return helper::get_messages($this->messages, $this->factory);
218
    }
219
 
220
    /**
221
     * Create the H5PIntegration variable that will be included in the page. This variable is used as the
222
     * main H5P config variable.
223
     */
224
    public function add_assets_to_page() {
225
        global $PAGE, $USER;
226
 
227
        $cid = $this->get_cid();
228
        $systemcontext = \context_system::instance();
229
 
230
        $disable = array_key_exists('disable', $this->content) ? $this->content['disable'] : core::DISABLE_NONE;
231
        $displayoptions = $this->core->getDisplayOptionsForView($disable, $this->h5pid);
232
 
233
        $contenturl = \moodle_url::make_pluginfile_url($systemcontext->id, \core_h5p\file_storage::COMPONENT,
234
            \core_h5p\file_storage::CONTENT_FILEAREA, $this->h5pid, null, null);
235
        $exporturl = $this->get_export_settings($displayoptions[ core::DISPLAY_OPTION_DOWNLOAD ]);
236
        $xapiobject = item_activity::create_from_id($this->context->id);
237
 
238
        $contentsettings = [
239
            'library'         => core::libraryToString($this->content['library']),
240
            'fullScreen'      => $this->content['library']['fullscreen'],
241
            'exportUrl'       => ($exporturl instanceof \moodle_url) ? $exporturl->out(false) : '',
242
            'embedCode'       => $this->get_embed_code($this->url->out(),
243
                $displayoptions[ core::DISPLAY_OPTION_EMBED ]),
244
            'resizeCode'      => self::get_resize_code(),
245
            'title'           => $this->content['slug'],
246
            'displayOptions'  => $displayoptions,
247
            'url'             => $xapiobject->get_data()->id,
248
            'contentUrl'      => $contenturl->out(),
249
            'metadata'        => $this->content['metadata'],
250
            'contentUserData' => [0 => ['state' => $this->get_state_data($xapiobject)]],
251
        ];
252
        // Get the core H5P assets, needed by the H5P classes to render the H5P content.
253
        $settings = $this->get_assets();
254
        $settings['contents'][$cid] = array_merge($settings['contents'][$cid], $contentsettings);
255
 
256
        // Print JavaScript settings to page.
257
        $PAGE->requires->data_for_js('H5PIntegration', $settings, true);
258
    }
259
 
260
    /**
261
     * Get the stored xAPI state to use as user data.
262
     *
263
     * @param item_activity $xapiobject
264
     * @return string The state data to pass to the player frontend
265
     */
266
    private function get_state_data(item_activity $xapiobject): string {
267
        global $USER;
268
 
269
        // Initialize the H5P content with the saved state (if it's enabled and the user has some stored state).
270
        $emptystatedata = '{}';
271
        $savestate = (bool) get_config($this->component, 'enablesavestate');
272
        if (!$savestate) {
273
            return $emptystatedata;
274
        }
275
 
276
        $xapihandler = handler::create($this->component);
277
        if (!$xapihandler) {
278
            return $emptystatedata;
279
        }
280
 
281
        // The component implements the xAPI handler, so the state can be loaded.
282
        $state = new state(
283
            item_agent::create_from_user($USER),
284
            $xapiobject,
285
            'state',
286
            null,
287
            null
288
        );
289
        try {
290
            $state = $xapihandler->load_state($state);
291
            if (!$state) {
292
                // Check if the state has been restored from a backup for the current user.
293
                $state = new state(
294
                    item_agent::create_from_user($USER),
295
                    $xapiobject,
296
                    'restored',
297
                    null,
298
                    null
299
                );
300
                $state = $xapihandler->load_state($state);
301
                if ($state && !is_null($state->get_state_data())) {
302
                    // A restored state has been found. It will be replaced with one with the proper stateid and statedata.
303
                    $xapihandler->delete_state($state);
304
                    $state = new state(
305
                        item_agent::create_from_user($USER),
306
                        $xapiobject,
307
                        'state',
308
                        $state->jsonSerialize(),
309
                        null
310
                    );
311
                    $xapihandler->save_state($state);
312
                }
313
            }
314
 
315
            if (!$state) {
316
                return $emptystatedata;
317
            }
318
 
319
            if (is_null($state->get_state_data())) {
320
                // The state content should be reset because, for instance, the content has changed.
321
                return 'RESET';
322
            }
323
 
324
            $statedata = $state->jsonSerialize();
325
            if (is_null($statedata)) {
326
                return $emptystatedata;
327
            }
328
 
329
            if (property_exists($statedata, 'h5p')) {
330
                // As the H5P state doesn't always use JSON, we have added this h5p object to jsonize it.
331
                return $statedata->h5p;
332
            }
333
        } catch (xapi_exception $exception) {
334
            return $emptystatedata;
335
        }
336
 
337
        return $emptystatedata;
338
    }
339
 
340
    /**
341
     * Outputs H5P wrapper HTML.
342
     *
343
     * @return string The HTML code to display this H5P content.
344
     */
345
    public function output(): string {
346
        global $OUTPUT, $USER;
347
 
348
        $template = new \stdClass();
349
        $template->h5pid = $this->h5pid;
350
        if ($this->embedtype === 'div') {
351
            $h5phtml = $OUTPUT->render_from_template('core_h5p/h5pdiv', $template);
352
        } else {
353
            $h5phtml = $OUTPUT->render_from_template('core_h5p/h5piframe', $template);
354
        }
355
 
356
        // Trigger capability_assigned event.
357
        \core_h5p\event\h5p_viewed::create([
358
            'objectid' => $this->h5pid,
359
            'userid' => $USER->id,
360
            'context' => $this->get_context(),
361
            'other' => [
362
                'url' => $this->url->out(),
363
                'time' => time()
364
            ]
365
        ])->trigger();
366
 
367
        return $h5phtml;
368
    }
369
 
370
    /**
371
     * Get the title of the H5P content to display.
372
     *
373
     * @return string the title
374
     */
375
    public function get_title(): string {
376
        return $this->content['title'];
377
    }
378
 
379
    /**
380
     * Get the context where the .h5p file belongs.
381
     *
382
     * @return context The context.
383
     */
384
    public function get_context(): \context {
385
        return $this->context;
386
    }
387
 
388
    /**
389
     * Delete an H5P package.
390
     *
391
     * @param stdClass $content The H5P package to delete.
392
     */
393
    private function delete_h5p(\stdClass $content) {
394
        $h5pstorage = $this->factory->get_storage();
395
        // Add an empty slug to the content if it's not defined, because the H5P library requires this field exists.
396
        // It's not used when deleting a package, so the real slug value is not required at this point.
397
        $content->slug = $content->slug ?? '';
398
        $h5pstorage->deletePackage( (array) $content);
399
    }
400
 
401
    /**
402
     * Export path for settings
403
     *
404
     * @param bool $downloadenabled Whether the option to export the H5P content is enabled.
405
     *
406
     * @return \moodle_url|null The URL of the exported file.
407
     */
408
    private function get_export_settings(bool $downloadenabled): ?\moodle_url {
409
 
410
        if (!$downloadenabled) {
411
            return null;
412
        }
413
 
414
        $systemcontext = \context_system::instance();
415
        $slug = $this->content['slug'] ? $this->content['slug'] . '-' : '';
416
        $filename = "{$slug}{$this->content['id']}.h5p";
417
        // We have to build the right URL.
418
        // Depending the request was made through webservice/pluginfile.php or pluginfile.php.
419
        if (strpos($this->url, '/webservice/pluginfile.php')) {
420
            $url  = \moodle_url::make_webservice_pluginfile_url(
421
                $systemcontext->id,
422
                \core_h5p\file_storage::COMPONENT,
423
                \core_h5p\file_storage::EXPORT_FILEAREA,
424
                '',
425
                '',
426
                $filename
427
            );
428
        } else {
429
            // If the request is made by tokenpluginfile.php we need to indicates to generate a token for current user.
430
            $includetoken = false;
431
            if (strpos($this->url, '/tokenpluginfile.php')) {
432
                $includetoken = true;
433
            }
434
            $url  = \moodle_url::make_pluginfile_url(
435
                $systemcontext->id,
436
                \core_h5p\file_storage::COMPONENT,
437
                \core_h5p\file_storage::EXPORT_FILEAREA,
438
                '',
439
                '',
440
                $filename,
441
                false,
442
                $includetoken
443
            );
444
        }
445
 
446
        // Get the required info from the export file to be able to get the export file by third apps.
447
        $file = helper::get_export_info($filename, $url);
448
        if ($file) {
449
            $url->param('modified', $file['timemodified']);
450
        }
451
        return $url;
452
    }
453
 
454
    /**
455
     * Get the identifier for the H5P content, to be used in the arrays as index.
456
     *
457
     * @return string The identifier.
458
     */
459
    private function get_cid(): string {
460
        return 'cid-' . $this->h5pid;
461
    }
462
 
463
    /**
464
     * Get the core H5P assets, including all core H5P JavaScript and CSS.
465
     *
466
     * @return Array core H5P assets.
467
     */
468
    private function get_assets(): array {
469
        // Get core assets.
470
        $settings = helper::get_core_assets($this->component);
471
        // Added here because in the helper we don't have the h5p content id.
472
        $settings['moodleLibraryPaths'] = $this->core->get_dependency_roots($this->h5pid);
473
        // Add also the Moodle component where the results will be tracked.
474
        $settings['moodleComponent'] = $this->component;
475
        if (!empty($settings['moodleComponent'])) {
476
            $settings['reportingIsEnabled'] = true;
477
        }
478
 
479
        $cid = $this->get_cid();
480
        // The filterParameters function should be called before getting the dependencyfiles because it rebuild content
481
        // dependency cache and export file.
482
        $settings['contents'][$cid]['jsonContent'] = $this->get_filtered_parameters();
483
 
484
        $files = $this->get_dependency_files();
485
        if ($this->embedtype === 'div') {
486
            $systemcontext = \context_system::instance();
487
            $h5ppath = "/pluginfile.php/{$systemcontext->id}/core_h5p";
488
 
489
            // Schedule JavaScripts for loading through Moodle.
490
            foreach ($files['scripts'] as $script) {
491
                $url = $script->path . $script->version;
492
 
493
                // Add URL prefix if not external.
494
                $isexternal = strpos($script->path, '://');
495
                if ($isexternal === false) {
496
                    $url = $h5ppath . $url;
497
                }
498
                $settings['loadedJs'][] = $url;
499
                $this->jsrequires[] = new \moodle_url($isexternal ? $url : $CFG->wwwroot . $url);
500
            }
501
 
502
            // Schedule stylesheets for loading through Moodle.
503
            foreach ($files['styles'] as $style) {
504
                $url = $style->path . $style->version;
505
 
506
                // Add URL prefix if not external.
507
                $isexternal = strpos($style->path, '://');
508
                if ($isexternal === false) {
509
                    $url = $h5ppath . $url;
510
                }
511
                $settings['loadedCss'][] = $url;
512
                $this->cssrequires[] = new \moodle_url($isexternal ? $url : $CFG->wwwroot . $url);
513
            }
514
 
515
        } else {
516
            // JavaScripts and stylesheets will be loaded through h5p.js.
517
            $settings['contents'][$cid]['scripts'] = $this->core->getAssetsUrls($files['scripts']);
518
            $settings['contents'][$cid]['styles']  = $this->core->getAssetsUrls($files['styles']);
519
        }
520
        return $settings;
521
    }
522
 
523
    /**
524
     * Get filtered parameters, modifying them by the renderer if the theme implements the h5p_alter_filtered_parameters function.
525
     *
526
     * @return string Filtered parameters.
527
     */
528
    private function get_filtered_parameters(): string {
529
        global $PAGE;
530
 
531
        $safeparams = $this->core->filterParameters($this->content);
532
        $decodedparams = json_decode($safeparams);
533
        $h5poutput = $PAGE->get_renderer('core_h5p');
534
        $h5poutput->h5p_alter_filtered_parameters(
535
            $decodedparams,
536
            $this->content['library']['name'],
537
            $this->content['library']['majorVersion'],
538
            $this->content['library']['minorVersion']
539
        );
540
        $safeparams = json_encode($decodedparams);
541
 
542
        return $safeparams;
543
    }
544
 
545
    /**
546
     * Finds library dependencies of view
547
     *
548
     * @return array Files that the view has dependencies to
549
     */
550
    private function get_dependency_files(): array {
551
        global $PAGE;
552
 
553
        $preloadeddeps = $this->core->loadContentDependencies($this->h5pid, 'preloaded');
554
        $files = $this->core->getDependenciesFiles($preloadeddeps);
555
 
556
        // Add additional asset files if required.
557
        $h5poutput = $PAGE->get_renderer('core_h5p');
558
        $h5poutput->h5p_alter_scripts($files['scripts'], $preloadeddeps, $this->embedtype);
559
        $h5poutput->h5p_alter_styles($files['styles'], $preloadeddeps, $this->embedtype);
560
 
561
        return $files;
562
    }
563
 
564
    /**
565
     * Resizing script for settings
566
     *
567
     * @return string The HTML code with the resize script.
568
     */
569
    private static function get_resize_code(): string {
570
        global $OUTPUT;
571
 
572
        $template = new \stdClass();
573
        $template->resizeurl = autoloader::get_h5p_core_library_url('js/h5p-resizer.js');
574
 
575
        return $OUTPUT->render_from_template('core_h5p/h5presize', $template);
576
    }
577
 
578
    /**
579
     * Embed code for settings
580
     *
581
     * @param string $url The URL of the .h5p file.
582
     * @param bool $embedenabled Whether the option to embed the H5P content is enabled.
583
     *
584
     * @return string The HTML code to reuse this H5P content in a different place.
585
     */
586
    private function get_embed_code(string $url, bool $embedenabled): string {
587
        global $OUTPUT;
588
 
589
        if ( ! $embedenabled) {
590
            return '';
591
        }
592
 
593
        $template = new \stdClass();
594
        $template->embedurl = self::get_embed_url($url, $this->component)->out(false);
595
 
596
        return $OUTPUT->render_from_template('core_h5p/h5pembed', $template);
597
    }
598
 
599
    /**
600
     * Get the encoded URL for embeding this H5P content.
601
     * @param  string $url The URL of the .h5p file.
602
     * @param string $component optional Moodle component to send xAPI tracking
603
     *
604
     * @return \moodle_url The embed URL.
605
     */
606
    public static function get_embed_url(string $url, string $component = ''): \moodle_url {
607
        $params = ['url' => $url];
608
        if (!empty($component)) {
609
            // If component is not empty, it will be passed too, in order to allow tracking too.
610
            $params['component'] = $component;
611
        }
612
 
613
        return new \moodle_url('/h5p/embed.php', $params);
614
    }
615
 
616
    /**
617
     * Return the info export file for Mobile App.
618
     *
619
     * @return array or null
620
     */
621
    public function get_export_file(): ?array {
622
        // Get the export url.
623
        $exporturl = $this->get_export_settings(true);
624
        // Get the filename of the export url.
625
        $path = $exporturl->out_as_local_url();
626
        // Check if the URL has parameters.
627
        $parts = explode('?', $path);
628
        $path = array_shift($parts);
629
        $parts = explode('/', $path);
630
        $filename = array_pop($parts);
631
        // Get the required info from the export file to be able to get the export file by third apps.
632
        $file = helper::get_export_info($filename, $exporturl);
633
 
634
        return $file;
635
    }
636
}