Merge branch 'MDL-81073' of https://github.com/paulholden/moodle
[moodle.git] / h5p / classes / player.php
bloba2f8564cea3f2a43658d1eeaf1005bc91ec8bbe7
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.
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.
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
25 namespace core_h5p;
27 defined('MOODLE_INTERNAL') || die();
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;
36 /**
37 * H5P player class, for displaying any local H5P content.
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
43 class player {
45 /**
46 * @var string The local H5P URL containing the .h5p file to display.
48 private $url;
50 /**
51 * @var core The H5PCore object.
53 private $core;
55 /**
56 * @var int H5P DB id.
58 private $h5pid;
60 /**
61 * @var array JavaScript requirements for this H5P.
63 private $jsrequires = [];
65 /**
66 * @var array CSS requirements for this H5P.
68 private $cssrequires = [];
70 /**
71 * @var array H5P content to display.
73 private $content;
75 /**
76 * @var string optional component name to send xAPI statements.
78 private $component;
80 /**
81 * @var string Type of embed object, div or iframe.
83 private $embedtype;
85 /**
86 * @var context The context object where the .h5p belongs.
88 private $context;
90 /**
91 * @var factory The \core_h5p\factory object.
93 private $factory;
95 /**
96 * @var stdClass The error, exception and info messages, raised while preparing and running the player.
98 private $messages;
101 * @var bool Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions.
103 private $preventredirect;
106 * Inits the H5P player for rendering the content.
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.
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');
120 $this->url = new \moodle_url($url);
121 $this->preventredirect = $preventredirect;
123 $this->factory = new \core_h5p\factory();
125 $this->messages = new \stdClass();
127 $this->component = $component;
129 // Create \core_h5p\core instance.
130 $this->core = $this->factory->get_core();
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
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);
147 // Get the embedtype to use for displaying the H5P content.
148 $this->embedtype = core::determineEmbedType($this->content['embedType'], $this->content['library']['embedTypes']);
154 * Get the encoded URL for embeding this H5P content.
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.
163 * @return string The embedable code to display a H5P file.
165 public static function display(
166 string $url, \stdClass $config,
167 bool $preventredirect = true,
168 string $component = '',
169 bool $displayedit = false,
170 array $extraactions = [],
171 ): string {
172 global $OUTPUT, $CFG;
174 $params = [
175 'url' => $url,
176 'preventredirect' => $preventredirect,
177 'component' => $component,
180 $optparams = ['frame', 'export', 'embed', 'copyright'];
181 foreach ($optparams as $optparam) {
182 if (!empty($config->$optparam)) {
183 $params[$optparam] = $config->$optparam;
186 $fileurl = new \moodle_url('/h5p/embed.php', $params);
188 $template = new \stdClass();
189 $template->embedurl = $fileurl->out(false);
191 if ($displayedit) {
192 list($originalfile, $h5p) = api::get_original_content_from_pluginfile_url($url, $preventredirect, true);
193 if ($originalfile) {
194 // Check if the user can edit this content.
195 if (api::can_edit_content($originalfile)) {
196 $template->editurl = $CFG->wwwroot . '/h5p/edit.php?url=' . $url;
201 $template->extraactions = [];
202 foreach ($extraactions as $action) {
203 $template->extraactions[] = $action->export_for_template($OUTPUT);
205 $result = $OUTPUT->render_from_template('core_h5p/h5pembed', $template);
206 $result .= self::get_resize_code();
207 return $result;
211 * Get the error messages stored in our H5P framework.
213 * @return stdClass with framework error messages.
215 public function get_messages(): \stdClass {
216 return helper::get_messages($this->messages, $this->factory);
220 * Create the H5PIntegration variable that will be included in the page. This variable is used as the
221 * main H5P config variable.
223 public function add_assets_to_page() {
224 global $PAGE, $USER;
226 $cid = $this->get_cid();
227 $systemcontext = \context_system::instance();
229 $disable = array_key_exists('disable', $this->content) ? $this->content['disable'] : core::DISABLE_NONE;
230 $displayoptions = $this->core->getDisplayOptionsForView($disable, $this->h5pid);
232 $contenturl = \moodle_url::make_pluginfile_url($systemcontext->id, \core_h5p\file_storage::COMPONENT,
233 \core_h5p\file_storage::CONTENT_FILEAREA, $this->h5pid, null, null);
234 $exporturl = $this->get_export_settings($displayoptions[ core::DISPLAY_OPTION_DOWNLOAD ]);
235 $xapiobject = item_activity::create_from_id($this->context->id);
237 $contentsettings = [
238 'library' => core::libraryToString($this->content['library']),
239 'fullScreen' => $this->content['library']['fullscreen'],
240 'exportUrl' => ($exporturl instanceof \moodle_url) ? $exporturl->out(false) : '',
241 'embedCode' => $this->get_embed_code($this->url->out(),
242 $displayoptions[ core::DISPLAY_OPTION_EMBED ]),
243 'resizeCode' => self::get_resize_code(),
244 'title' => $this->content['slug'],
245 'displayOptions' => $displayoptions,
246 'url' => $xapiobject->get_data()->id,
247 'contentUrl' => $contenturl->out(),
248 'metadata' => $this->content['metadata'],
249 'contentUserData' => [0 => ['state' => $this->get_state_data($xapiobject)]],
251 // Get the core H5P assets, needed by the H5P classes to render the H5P content.
252 $settings = $this->get_assets();
253 $settings['contents'][$cid] = array_merge($settings['contents'][$cid], $contentsettings);
255 // Print JavaScript settings to page.
256 $PAGE->requires->data_for_js('H5PIntegration', $settings, true);
260 * Get the stored xAPI state to use as user data.
262 * @param item_activity $xapiobject
263 * @return string The state data to pass to the player frontend
265 private function get_state_data(item_activity $xapiobject): string {
266 global $USER;
268 // Initialize the H5P content with the saved state (if it's enabled and the user has some stored state).
269 $emptystatedata = '{}';
270 $savestate = (bool) get_config($this->component, 'enablesavestate');
271 if (!$savestate) {
272 return $emptystatedata;
275 $xapihandler = handler::create($this->component);
276 if (!$xapihandler) {
277 return $emptystatedata;
280 // The component implements the xAPI handler, so the state can be loaded.
281 $state = new state(
282 item_agent::create_from_user($USER),
283 $xapiobject,
284 'state',
285 null,
286 null
288 try {
289 $state = $xapihandler->load_state($state);
290 if (!$state) {
291 // Check if the state has been restored from a backup for the current user.
292 $state = new state(
293 item_agent::create_from_user($USER),
294 $xapiobject,
295 'restored',
296 null,
297 null
299 $state = $xapihandler->load_state($state);
300 if ($state && !is_null($state->get_state_data())) {
301 // A restored state has been found. It will be replaced with one with the proper stateid and statedata.
302 $xapihandler->delete_state($state);
303 $state = new state(
304 item_agent::create_from_user($USER),
305 $xapiobject,
306 'state',
307 $state->jsonSerialize(),
308 null
310 $xapihandler->save_state($state);
314 if (!$state) {
315 return $emptystatedata;
318 if (is_null($state->get_state_data())) {
319 // The state content should be reset because, for instance, the content has changed.
320 return 'RESET';
323 $statedata = $state->jsonSerialize();
324 if (is_null($statedata)) {
325 return $emptystatedata;
328 if (property_exists($statedata, 'h5p')) {
329 // As the H5P state doesn't always use JSON, we have added this h5p object to jsonize it.
330 return $statedata->h5p;
332 } catch (xapi_exception $exception) {
333 return $emptystatedata;
336 return $emptystatedata;
340 * Outputs H5P wrapper HTML.
342 * @return string The HTML code to display this H5P content.
344 public function output(): string {
345 global $OUTPUT, $USER;
347 $template = new \stdClass();
348 $template->h5pid = $this->h5pid;
349 if ($this->embedtype === 'div') {
350 $h5phtml = $OUTPUT->render_from_template('core_h5p/h5pdiv', $template);
351 } else {
352 $h5phtml = $OUTPUT->render_from_template('core_h5p/h5piframe', $template);
355 // Trigger capability_assigned event.
356 \core_h5p\event\h5p_viewed::create([
357 'objectid' => $this->h5pid,
358 'userid' => $USER->id,
359 'context' => $this->get_context(),
360 'other' => [
361 'url' => $this->url->out(),
362 'time' => time()
364 ])->trigger();
366 return $h5phtml;
370 * Get the title of the H5P content to display.
372 * @return string the title
374 public function get_title(): string {
375 return $this->content['title'];
379 * Get the context where the .h5p file belongs.
381 * @return context The context.
383 public function get_context(): \context {
384 return $this->context;
388 * Delete an H5P package.
390 * @param stdClass $content The H5P package to delete.
392 private function delete_h5p(\stdClass $content) {
393 $h5pstorage = $this->factory->get_storage();
394 // Add an empty slug to the content if it's not defined, because the H5P library requires this field exists.
395 // It's not used when deleting a package, so the real slug value is not required at this point.
396 $content->slug = $content->slug ?? '';
397 $h5pstorage->deletePackage( (array) $content);
401 * Export path for settings
403 * @param bool $downloadenabled Whether the option to export the H5P content is enabled.
405 * @return \moodle_url|null The URL of the exported file.
407 private function get_export_settings(bool $downloadenabled): ?\moodle_url {
409 if (!$downloadenabled) {
410 return null;
413 $systemcontext = \context_system::instance();
414 $slug = $this->content['slug'] ? $this->content['slug'] . '-' : '';
415 $filename = "{$slug}{$this->content['id']}.h5p";
416 // We have to build the right URL.
417 // Depending the request was made through webservice/pluginfile.php or pluginfile.php.
418 if (strpos($this->url, '/webservice/pluginfile.php')) {
419 $url = \moodle_url::make_webservice_pluginfile_url(
420 $systemcontext->id,
421 \core_h5p\file_storage::COMPONENT,
422 \core_h5p\file_storage::EXPORT_FILEAREA,
425 $filename
427 } else {
428 // If the request is made by tokenpluginfile.php we need to indicates to generate a token for current user.
429 $includetoken = false;
430 if (strpos($this->url, '/tokenpluginfile.php')) {
431 $includetoken = true;
433 $url = \moodle_url::make_pluginfile_url(
434 $systemcontext->id,
435 \core_h5p\file_storage::COMPONENT,
436 \core_h5p\file_storage::EXPORT_FILEAREA,
439 $filename,
440 false,
441 $includetoken
445 // Get the required info from the export file to be able to get the export file by third apps.
446 $file = helper::get_export_info($filename, $url);
447 if ($file) {
448 $url->param('modified', $file['timemodified']);
450 return $url;
454 * Get the identifier for the H5P content, to be used in the arrays as index.
456 * @return string The identifier.
458 private function get_cid(): string {
459 return 'cid-' . $this->h5pid;
463 * Get the core H5P assets, including all core H5P JavaScript and CSS.
465 * @return Array core H5P assets.
467 private function get_assets(): array {
468 // Get core assets.
469 $settings = helper::get_core_assets($this->component);
470 // Added here because in the helper we don't have the h5p content id.
471 $settings['moodleLibraryPaths'] = $this->core->get_dependency_roots($this->h5pid);
472 // Add also the Moodle component where the results will be tracked.
473 $settings['moodleComponent'] = $this->component;
474 if (!empty($settings['moodleComponent'])) {
475 $settings['reportingIsEnabled'] = true;
478 $cid = $this->get_cid();
479 // The filterParameters function should be called before getting the dependencyfiles because it rebuild content
480 // dependency cache and export file.
481 $settings['contents'][$cid]['jsonContent'] = $this->get_filtered_parameters();
483 $files = $this->get_dependency_files();
484 if ($this->embedtype === 'div') {
485 $systemcontext = \context_system::instance();
486 $h5ppath = "/pluginfile.php/{$systemcontext->id}/core_h5p";
488 // Schedule JavaScripts for loading through Moodle.
489 foreach ($files['scripts'] as $script) {
490 $url = $script->path . $script->version;
492 // Add URL prefix if not external.
493 $isexternal = strpos($script->path, '://');
494 if ($isexternal === false) {
495 $url = $h5ppath . $url;
497 $settings['loadedJs'][] = $url;
498 $this->jsrequires[] = new \moodle_url($isexternal ? $url : $CFG->wwwroot . $url);
501 // Schedule stylesheets for loading through Moodle.
502 foreach ($files['styles'] as $style) {
503 $url = $style->path . $style->version;
505 // Add URL prefix if not external.
506 $isexternal = strpos($style->path, '://');
507 if ($isexternal === false) {
508 $url = $h5ppath . $url;
510 $settings['loadedCss'][] = $url;
511 $this->cssrequires[] = new \moodle_url($isexternal ? $url : $CFG->wwwroot . $url);
514 } else {
515 // JavaScripts and stylesheets will be loaded through h5p.js.
516 $settings['contents'][$cid]['scripts'] = $this->core->getAssetsUrls($files['scripts']);
517 $settings['contents'][$cid]['styles'] = $this->core->getAssetsUrls($files['styles']);
519 return $settings;
523 * Get filtered parameters, modifying them by the renderer if the theme implements the h5p_alter_filtered_parameters function.
525 * @return string Filtered parameters.
527 private function get_filtered_parameters(): string {
528 global $PAGE;
530 $safeparams = $this->core->filterParameters($this->content);
531 $decodedparams = json_decode($safeparams);
532 $h5poutput = $PAGE->get_renderer('core_h5p');
533 $h5poutput->h5p_alter_filtered_parameters(
534 $decodedparams,
535 $this->content['library']['name'],
536 $this->content['library']['majorVersion'],
537 $this->content['library']['minorVersion']
539 $safeparams = json_encode($decodedparams);
541 return $safeparams;
545 * Finds library dependencies of view
547 * @return array Files that the view has dependencies to
549 private function get_dependency_files(): array {
550 global $PAGE;
552 $preloadeddeps = $this->core->loadContentDependencies($this->h5pid, 'preloaded');
553 $files = $this->core->getDependenciesFiles($preloadeddeps);
555 // Add additional asset files if required.
556 $h5poutput = $PAGE->get_renderer('core_h5p');
557 $h5poutput->h5p_alter_scripts($files['scripts'], $preloadeddeps, $this->embedtype);
558 $h5poutput->h5p_alter_styles($files['styles'], $preloadeddeps, $this->embedtype);
560 return $files;
564 * Resizing script for settings
566 * @return string The HTML code with the resize script.
568 private static function get_resize_code(): string {
569 global $OUTPUT;
571 $template = new \stdClass();
572 $template->resizeurl = autoloader::get_h5p_core_library_url('js/h5p-resizer.js');
574 return $OUTPUT->render_from_template('core_h5p/h5presize', $template);
578 * Embed code for settings
580 * @param string $url The URL of the .h5p file.
581 * @param bool $embedenabled Whether the option to embed the H5P content is enabled.
583 * @return string The HTML code to reuse this H5P content in a different place.
585 private function get_embed_code(string $url, bool $embedenabled): string {
586 global $OUTPUT;
588 if ( ! $embedenabled) {
589 return '';
592 $template = new \stdClass();
593 $template->embedurl = self::get_embed_url($url, $this->component)->out(false);
595 return $OUTPUT->render_from_template('core_h5p/h5pembed', $template);
599 * Get the encoded URL for embeding this H5P content.
600 * @param string $url The URL of the .h5p file.
601 * @param string $component optional Moodle component to send xAPI tracking
603 * @return \moodle_url The embed URL.
605 public static function get_embed_url(string $url, string $component = ''): \moodle_url {
606 $params = ['url' => $url];
607 if (!empty($component)) {
608 // If component is not empty, it will be passed too, in order to allow tracking too.
609 $params['component'] = $component;
612 return new \moodle_url('/h5p/embed.php', $params);
616 * Return the info export file for Mobile App.
618 * @return array or null
620 public function get_export_file(): ?array {
621 // Get the export url.
622 $exporturl = $this->get_export_settings(true);
623 // Get the filename of the export url.
624 $path = $exporturl->out_as_local_url();
625 // Check if the URL has parameters.
626 $parts = explode('?', $path);
627 $path = array_shift($parts);
628 $parts = explode('/', $path);
629 $filename = array_pop($parts);
630 // Get the required info from the export file to be able to get the export file by third apps.
631 $file = helper::get_export_info($filename, $exporturl);
633 return $file;