2 // This file is part of Moodle - http://moodle.org/
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.
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/>.
21 * @copyright 2019 Sara Arjona <sara@moodle.com>
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
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
;
37 * H5P player class, for displaying any local H5P content.
40 * @copyright 2019 Sara Arjona <sara@moodle.com>
41 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
46 * @var string The local H5P URL containing the .h5p file to display.
51 * @var core The H5PCore object.
61 * @var array JavaScript requirements for this H5P.
63 private $jsrequires = [];
66 * @var array CSS requirements for this H5P.
68 private $cssrequires = [];
71 * @var array H5P content to display.
76 * @var string optional component name to send xAPI statements.
81 * @var string Type of embed object, div or iframe.
86 * @var context The context object where the .h5p belongs.
91 * @var factory The \core_h5p\factory object.
96 * @var stdClass The error, exception and info messages, raised while preparing and running the player.
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) {
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(
138 $this->preventredirect
,
142 $this->context
= \context
::instance_by_id($file->get_contextid());
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(
168 bool $preventredirect = true,
169 string $component = '',
170 bool $displayedit = false,
171 array $extraactions = [],
173 global $OUTPUT, $CFG;
177 'preventredirect' => $preventredirect,
178 'component' => $component,
181 $optparams = ['frame', 'export', 'embed', 'copyright'];
182 foreach ($optparams as $optparam) {
183 if (!empty($config->$optparam)) {
184 $params[$optparam] = $config->$optparam;
187 $fileurl = new \
moodle_url('/h5p/embed.php', $params);
189 $template = new \
stdClass();
190 $template->embedurl
= $fileurl->out(false);
193 list($originalfile, $h5p) = api
::get_original_content_from_pluginfile_url($url, $preventredirect, true);
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);
202 $template->extraactions
= [];
203 foreach ($extraactions as $action) {
204 $template->extraactions
[] = $action->export_for_template($OUTPUT);
206 $result = $OUTPUT->render_from_template('core_h5p/h5pembed', $template);
207 $result .= self
::get_resize_code();
212 * Get the error messages stored in our H5P framework.
214 * @return stdClass with framework error messages.
216 public function get_messages(): \stdClass
{
217 return helper
::get_messages($this->messages
, $this->factory
);
221 * Create the H5PIntegration variable that will be included in the page. This variable is used as the
222 * main H5P config variable.
224 public function add_assets_to_page() {
227 $cid = $this->get_cid();
228 $systemcontext = \context_system
::instance();
230 $disable = array_key_exists('disable', $this->content
) ?
$this->content
['disable'] : core
::DISABLE_NONE
;
231 $displayoptions = $this->core
->getDisplayOptionsForView($disable, $this->h5pid
);
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
);
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)]],
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);
256 // Print JavaScript settings to page.
257 $PAGE->requires
->data_for_js('H5PIntegration', $settings, true);
261 * Get the stored xAPI state to use as user data.
263 * @param item_activity $xapiobject
264 * @return string The state data to pass to the player frontend
266 private function get_state_data(item_activity
$xapiobject): string {
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');
273 return $emptystatedata;
276 $xapihandler = handler
::create($this->component
);
278 return $emptystatedata;
281 // The component implements the xAPI handler, so the state can be loaded.
283 item_agent
::create_from_user($USER),
290 $state = $xapihandler->load_state($state);
292 // Check if the state has been restored from a backup for the current user.
294 item_agent
::create_from_user($USER),
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);
305 item_agent
::create_from_user($USER),
308 $state->jsonSerialize(),
311 $xapihandler->save_state($state);
316 return $emptystatedata;
319 if (is_null($state->get_state_data())) {
320 // The state content should be reset because, for instance, the content has changed.
324 $statedata = $state->jsonSerialize();
325 if (is_null($statedata)) {
326 return $emptystatedata;
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
;
333 } catch (xapi_exception
$exception) {
334 return $emptystatedata;
337 return $emptystatedata;
341 * Outputs H5P wrapper HTML.
343 * @return string The HTML code to display this H5P content.
345 public function output(): string {
346 global $OUTPUT, $USER;
348 $template = new \
stdClass();
349 $template->h5pid
= $this->h5pid
;
350 if ($this->embedtype
=== 'div') {
351 $h5phtml = $OUTPUT->render_from_template('core_h5p/h5pdiv', $template);
353 $h5phtml = $OUTPUT->render_from_template('core_h5p/h5piframe', $template);
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(),
362 'url' => $this->url
->out(),
371 * Get the title of the H5P content to display.
373 * @return string the title
375 public function get_title(): string {
376 return $this->content
['title'];
380 * Get the context where the .h5p file belongs.
382 * @return context The context.
384 public function get_context(): \context
{
385 return $this->context
;
389 * Delete an H5P package.
391 * @param stdClass $content The H5P package to delete.
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);
402 * Export path for settings
404 * @param bool $downloadenabled Whether the option to export the H5P content is enabled.
406 * @return \moodle_url|null The URL of the exported file.
408 private function get_export_settings(bool $downloadenabled): ?\moodle_url
{
410 if (!$downloadenabled) {
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(
422 \core_h5p\file_storage
::COMPONENT
,
423 \core_h5p\file_storage
::EXPORT_FILEAREA
,
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;
434 $url = \moodle_url
::make_pluginfile_url(
436 \core_h5p\file_storage
::COMPONENT
,
437 \core_h5p\file_storage
::EXPORT_FILEAREA
,
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);
449 $url->param('modified', $file['timemodified']);
455 * Get the identifier for the H5P content, to be used in the arrays as index.
457 * @return string The identifier.
459 private function get_cid(): string {
460 return 'cid-' . $this->h5pid
;
464 * Get the core H5P assets, including all core H5P JavaScript and CSS.
466 * @return Array core H5P assets.
468 private function get_assets(): array {
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;
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();
484 $files = $this->get_dependency_files();
485 if ($this->embedtype
=== 'div') {
486 $systemcontext = \context_system
::instance();
487 $h5ppath = "/pluginfile.php/{$systemcontext->id}/core_h5p";
489 // Schedule JavaScripts for loading through Moodle.
490 foreach ($files['scripts'] as $script) {
491 $url = $script->path
. $script->version
;
493 // Add URL prefix if not external.
494 $isexternal = strpos($script->path
, '://');
495 if ($isexternal === false) {
496 $url = $h5ppath . $url;
498 $settings['loadedJs'][] = $url;
499 $this->jsrequires
[] = new \
moodle_url($isexternal ?
$url : $CFG->wwwroot
. $url);
502 // Schedule stylesheets for loading through Moodle.
503 foreach ($files['styles'] as $style) {
504 $url = $style->path
. $style->version
;
506 // Add URL prefix if not external.
507 $isexternal = strpos($style->path
, '://');
508 if ($isexternal === false) {
509 $url = $h5ppath . $url;
511 $settings['loadedCss'][] = $url;
512 $this->cssrequires
[] = new \
moodle_url($isexternal ?
$url : $CFG->wwwroot
. $url);
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']);
524 * Get filtered parameters, modifying them by the renderer if the theme implements the h5p_alter_filtered_parameters function.
526 * @return string Filtered parameters.
528 private function get_filtered_parameters(): string {
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(
536 $this->content
['library']['name'],
537 $this->content
['library']['majorVersion'],
538 $this->content
['library']['minorVersion']
540 $safeparams = json_encode($decodedparams);
546 * Finds library dependencies of view
548 * @return array Files that the view has dependencies to
550 private function get_dependency_files(): array {
553 $preloadeddeps = $this->core
->loadContentDependencies($this->h5pid
, 'preloaded');
554 $files = $this->core
->getDependenciesFiles($preloadeddeps);
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
);
565 * Resizing script for settings
567 * @return string The HTML code with the resize script.
569 private static function get_resize_code(): string {
572 $template = new \
stdClass();
573 $template->resizeurl
= autoloader
::get_h5p_core_library_url('js/h5p-resizer.js');
575 return $OUTPUT->render_from_template('core_h5p/h5presize', $template);
579 * Embed code for settings
581 * @param string $url The URL of the .h5p file.
582 * @param bool $embedenabled Whether the option to embed the H5P content is enabled.
584 * @return string The HTML code to reuse this H5P content in a different place.
586 private function get_embed_code(string $url, bool $embedenabled): string {
589 if ( ! $embedenabled) {
593 $template = new \
stdClass();
594 $template->embedurl
= self
::get_embed_url($url, $this->component
)->out(false);
596 return $OUTPUT->render_from_template('core_h5p/h5pembed', $template);
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
604 * @return \moodle_url The embed URL.
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;
613 return new \
moodle_url('/h5p/embed.php', $params);
617 * Return the info export file for Mobile App.
619 * @return array or null
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);