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