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