Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]
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 * 162 * @return string The embedable code to display a H5P file. 163 */ 164 public static function display(string $url, \stdClass $config, bool $preventredirect = true, 165 string $component = '', bool $displayedit = false): string { 166 global $OUTPUT, $CFG; 167 168 $params = [ 169 'url' => $url, 170 'preventredirect' => $preventredirect, 171 'component' => $component, 172 ]; 173 174 $optparams = ['frame', 'export', 'embed', 'copyright']; 175 foreach ($optparams as $optparam) { 176 if (!empty($config->$optparam)) { 177 $params[$optparam] = $config->$optparam; 178 } 179 } 180 $fileurl = new \moodle_url('/h5p/embed.php', $params); 181 182 $template = new \stdClass(); 183 $template->embedurl = $fileurl->out(false); 184 185 if ($displayedit) { 186 list($originalfile, $h5p) = api::get_original_content_from_pluginfile_url($url, $preventredirect, true); 187 if ($originalfile) { 188 // Check if the user can edit this content. 189 if (api::can_edit_content($originalfile)) { 190 $template->editurl = $CFG->wwwroot . '/h5p/edit.php?url=' . $url; 191 } 192 } 193 } 194 195 $result = $OUTPUT->render_from_template('core_h5p/h5pembed', $template); 196 $result .= self::get_resize_code(); 197 return $result; 198 } 199 200 /** 201 * Get the error messages stored in our H5P framework. 202 * 203 * @return stdClass with framework error messages. 204 */ 205 public function get_messages(): \stdClass { 206 return helper::get_messages($this->messages, $this->factory); 207 } 208 209 /** 210 * Create the H5PIntegration variable that will be included in the page. This variable is used as the 211 * main H5P config variable. 212 */ 213 public function add_assets_to_page() { 214 global $PAGE, $USER; 215 216 $cid = $this->get_cid(); 217 $systemcontext = \context_system::instance(); 218 219 $disable = array_key_exists('disable', $this->content) ? $this->content['disable'] : core::DISABLE_NONE; 220 $displayoptions = $this->core->getDisplayOptionsForView($disable, $this->h5pid); 221 222 $contenturl = \moodle_url::make_pluginfile_url($systemcontext->id, \core_h5p\file_storage::COMPONENT, 223 \core_h5p\file_storage::CONTENT_FILEAREA, $this->h5pid, null, null); 224 $exporturl = $this->get_export_settings($displayoptions[ core::DISPLAY_OPTION_DOWNLOAD ]); 225 $xapiobject = item_activity::create_from_id($this->context->id); 226 227 $contentsettings = [ 228 'library' => core::libraryToString($this->content['library']), 229 'fullScreen' => $this->content['library']['fullscreen'], 230 'exportUrl' => ($exporturl instanceof \moodle_url) ? $exporturl->out(false) : '', 231 'embedCode' => $this->get_embed_code($this->url->out(), 232 $displayoptions[ core::DISPLAY_OPTION_EMBED ]), 233 'resizeCode' => self::get_resize_code(), 234 'title' => $this->content['slug'], 235 'displayOptions' => $displayoptions, 236 'url' => $xapiobject->get_data()->id, 237 'contentUrl' => $contenturl->out(), 238 'metadata' => $this->content['metadata'], 239 'contentUserData' => [0 => ['state' => $this->get_state_data($xapiobject)]], 240 ]; 241 // Get the core H5P assets, needed by the H5P classes to render the H5P content. 242 $settings = $this->get_assets(); 243 $settings['contents'][$cid] = array_merge($settings['contents'][$cid], $contentsettings); 244 245 // Print JavaScript settings to page. 246 $PAGE->requires->data_for_js('H5PIntegration', $settings, true); 247 } 248 249 /** 250 * Get the stored xAPI state to use as user data. 251 * 252 * @param item_activity $xapiobject 253 * @return string The state data to pass to the player frontend 254 */ 255 private function get_state_data(item_activity $xapiobject): string { 256 global $USER; 257 258 // Initialize the H5P content with the saved state (if it's enabled and the user has some stored state). 259 $emptystatedata = '{}'; 260 $savestate = (bool) get_config($this->component, 'enablesavestate'); 261 if (!$savestate) { 262 return $emptystatedata; 263 } 264 265 $xapihandler = handler::create($this->component); 266 if (!$xapihandler) { 267 return $emptystatedata; 268 } 269 270 // The component implements the xAPI handler, so the state can be loaded. 271 $state = new state( 272 item_agent::create_from_user($USER), 273 $xapiobject, 274 'state', 275 null, 276 null 277 ); 278 try { 279 $state = $xapihandler->load_state($state); 280 if (!$state) { 281 // Check if the state has been restored from a backup for the current user. 282 $state = new state( 283 item_agent::create_from_user($USER), 284 $xapiobject, 285 'restored', 286 null, 287 null 288 ); 289 $state = $xapihandler->load_state($state); 290 if ($state && !is_null($state->get_state_data())) { 291 // A restored state has been found. It will be replaced with one with the proper stateid and statedata. 292 $xapihandler->delete_state($state); 293 $state = new state( 294 item_agent::create_from_user($USER), 295 $xapiobject, 296 'state', 297 $state->jsonSerialize(), 298 null 299 ); 300 $xapihandler->save_state($state); 301 } 302 } 303 304 if (!$state) { 305 return $emptystatedata; 306 } 307 308 if (is_null($state->get_state_data())) { 309 // The state content should be reset because, for instance, the content has changed. 310 return 'RESET'; 311 } 312 313 $statedata = $state->jsonSerialize(); 314 if (is_null($statedata)) { 315 return $emptystatedata; 316 } 317 318 if (property_exists($statedata, 'h5p')) { 319 // As the H5P state doesn't always use JSON, we have added this h5p object to jsonize it. 320 return $statedata->h5p; 321 } 322 } catch (xapi_exception $exception) { 323 return $emptystatedata; 324 } 325 326 return $emptystatedata; 327 } 328 329 /** 330 * Outputs H5P wrapper HTML. 331 * 332 * @return string The HTML code to display this H5P content. 333 */ 334 public function output(): string { 335 global $OUTPUT, $USER; 336 337 $template = new \stdClass(); 338 $template->h5pid = $this->h5pid; 339 if ($this->embedtype === 'div') { 340 $h5phtml = $OUTPUT->render_from_template('core_h5p/h5pdiv', $template); 341 } else { 342 $h5phtml = $OUTPUT->render_from_template('core_h5p/h5piframe', $template); 343 } 344 345 // Trigger capability_assigned event. 346 \core_h5p\event\h5p_viewed::create([ 347 'objectid' => $this->h5pid, 348 'userid' => $USER->id, 349 'context' => $this->get_context(), 350 'other' => [ 351 'url' => $this->url->out(), 352 'time' => time() 353 ] 354 ])->trigger(); 355 356 return $h5phtml; 357 } 358 359 /** 360 * Get the title of the H5P content to display. 361 * 362 * @return string the title 363 */ 364 public function get_title(): string { 365 return $this->content['title']; 366 } 367 368 /** 369 * Get the context where the .h5p file belongs. 370 * 371 * @return context The context. 372 */ 373 public function get_context(): \context { 374 return $this->context; 375 } 376 377 /** 378 * Delete an H5P package. 379 * 380 * @param stdClass $content The H5P package to delete. 381 */ 382 private function delete_h5p(\stdClass $content) { 383 $h5pstorage = $this->factory->get_storage(); 384 // Add an empty slug to the content if it's not defined, because the H5P library requires this field exists. 385 // It's not used when deleting a package, so the real slug value is not required at this point. 386 $content->slug = $content->slug ?? ''; 387 $h5pstorage->deletePackage( (array) $content); 388 } 389 390 /** 391 * Export path for settings 392 * 393 * @param bool $downloadenabled Whether the option to export the H5P content is enabled. 394 * 395 * @return \moodle_url|null The URL of the exported file. 396 */ 397 private function get_export_settings(bool $downloadenabled): ?\moodle_url { 398 399 if (!$downloadenabled) { 400 return null; 401 } 402 403 $systemcontext = \context_system::instance(); 404 $slug = $this->content['slug'] ? $this->content['slug'] . '-' : ''; 405 $filename = "{$slug}{$this->content['id']}.h5p"; 406 // We have to build the right URL. 407 // Depending the request was made through webservice/pluginfile.php or pluginfile.php. 408 if (strpos($this->url, '/webservice/pluginfile.php')) { 409 $url = \moodle_url::make_webservice_pluginfile_url( 410 $systemcontext->id, 411 \core_h5p\file_storage::COMPONENT, 412 \core_h5p\file_storage::EXPORT_FILEAREA, 413 '', 414 '', 415 $filename 416 ); 417 } else { 418 // If the request is made by tokenpluginfile.php we need to indicates to generate a token for current user. 419 $includetoken = false; 420 if (strpos($this->url, '/tokenpluginfile.php')) { 421 $includetoken = true; 422 } 423 $url = \moodle_url::make_pluginfile_url( 424 $systemcontext->id, 425 \core_h5p\file_storage::COMPONENT, 426 \core_h5p\file_storage::EXPORT_FILEAREA, 427 '', 428 '', 429 $filename, 430 false, 431 $includetoken 432 ); 433 } 434 435 // Get the required info from the export file to be able to get the export file by third apps. 436 $file = helper::get_export_info($filename, $url); 437 if ($file) { 438 $url->param('modified', $file['timemodified']); 439 } 440 return $url; 441 } 442 443 /** 444 * Get the identifier for the H5P content, to be used in the arrays as index. 445 * 446 * @return string The identifier. 447 */ 448 private function get_cid(): string { 449 return 'cid-' . $this->h5pid; 450 } 451 452 /** 453 * Get the core H5P assets, including all core H5P JavaScript and CSS. 454 * 455 * @return Array core H5P assets. 456 */ 457 private function get_assets(): array { 458 // Get core assets. 459 $settings = helper::get_core_assets($this->component); 460 // Added here because in the helper we don't have the h5p content id. 461 $settings['moodleLibraryPaths'] = $this->core->get_dependency_roots($this->h5pid); 462 // Add also the Moodle component where the results will be tracked. 463 $settings['moodleComponent'] = $this->component; 464 if (!empty($settings['moodleComponent'])) { 465 $settings['reportingIsEnabled'] = true; 466 } 467 468 $cid = $this->get_cid(); 469 // The filterParameters function should be called before getting the dependencyfiles because it rebuild content 470 // dependency cache and export file. 471 $settings['contents'][$cid]['jsonContent'] = $this->get_filtered_parameters(); 472 473 $files = $this->get_dependency_files(); 474 if ($this->embedtype === 'div') { 475 $systemcontext = \context_system::instance(); 476 $h5ppath = "/pluginfile.php/{$systemcontext->id}/core_h5p"; 477 478 // Schedule JavaScripts for loading through Moodle. 479 foreach ($files['scripts'] as $script) { 480 $url = $script->path . $script->version; 481 482 // Add URL prefix if not external. 483 $isexternal = strpos($script->path, '://'); 484 if ($isexternal === false) { 485 $url = $h5ppath . $url; 486 } 487 $settings['loadedJs'][] = $url; 488 $this->jsrequires[] = new \moodle_url($isexternal ? $url : $CFG->wwwroot . $url); 489 } 490 491 // Schedule stylesheets for loading through Moodle. 492 foreach ($files['styles'] as $style) { 493 $url = $style->path . $style->version; 494 495 // Add URL prefix if not external. 496 $isexternal = strpos($style->path, '://'); 497 if ($isexternal === false) { 498 $url = $h5ppath . $url; 499 } 500 $settings['loadedCss'][] = $url; 501 $this->cssrequires[] = new \moodle_url($isexternal ? $url : $CFG->wwwroot . $url); 502 } 503 504 } else { 505 // JavaScripts and stylesheets will be loaded through h5p.js. 506 $settings['contents'][$cid]['scripts'] = $this->core->getAssetsUrls($files['scripts']); 507 $settings['contents'][$cid]['styles'] = $this->core->getAssetsUrls($files['styles']); 508 } 509 return $settings; 510 } 511 512 /** 513 * Get filtered parameters, modifying them by the renderer if the theme implements the h5p_alter_filtered_parameters function. 514 * 515 * @return string Filtered parameters. 516 */ 517 private function get_filtered_parameters(): string { 518 global $PAGE; 519 520 $safeparams = $this->core->filterParameters($this->content); 521 $decodedparams = json_decode($safeparams); 522 $h5poutput = $PAGE->get_renderer('core_h5p'); 523 $h5poutput->h5p_alter_filtered_parameters( 524 $decodedparams, 525 $this->content['library']['name'], 526 $this->content['library']['majorVersion'], 527 $this->content['library']['minorVersion'] 528 ); 529 $safeparams = json_encode($decodedparams); 530 531 return $safeparams; 532 } 533 534 /** 535 * Finds library dependencies of view 536 * 537 * @return array Files that the view has dependencies to 538 */ 539 private function get_dependency_files(): array { 540 global $PAGE; 541 542 $preloadeddeps = $this->core->loadContentDependencies($this->h5pid, 'preloaded'); 543 $files = $this->core->getDependenciesFiles($preloadeddeps); 544 545 // Add additional asset files if required. 546 $h5poutput = $PAGE->get_renderer('core_h5p'); 547 $h5poutput->h5p_alter_scripts($files['scripts'], $preloadeddeps, $this->embedtype); 548 $h5poutput->h5p_alter_styles($files['styles'], $preloadeddeps, $this->embedtype); 549 550 return $files; 551 } 552 553 /** 554 * Resizing script for settings 555 * 556 * @return string The HTML code with the resize script. 557 */ 558 private static function get_resize_code(): string { 559 global $OUTPUT; 560 561 $template = new \stdClass(); 562 $template->resizeurl = autoloader::get_h5p_core_library_url('js/h5p-resizer.js'); 563 564 return $OUTPUT->render_from_template('core_h5p/h5presize', $template); 565 } 566 567 /** 568 * Embed code for settings 569 * 570 * @param string $url The URL of the .h5p file. 571 * @param bool $embedenabled Whether the option to embed the H5P content is enabled. 572 * 573 * @return string The HTML code to reuse this H5P content in a different place. 574 */ 575 private function get_embed_code(string $url, bool $embedenabled): string { 576 global $OUTPUT; 577 578 if ( ! $embedenabled) { 579 return ''; 580 } 581 582 $template = new \stdClass(); 583 $template->embedurl = self::get_embed_url($url, $this->component)->out(false); 584 585 return $OUTPUT->render_from_template('core_h5p/h5pembed', $template); 586 } 587 588 /** 589 * Get the encoded URL for embeding this H5P content. 590 * @param string $url The URL of the .h5p file. 591 * @param string $component optional Moodle component to send xAPI tracking 592 * 593 * @return \moodle_url The embed URL. 594 */ 595 public static function get_embed_url(string $url, string $component = ''): \moodle_url { 596 $params = ['url' => $url]; 597 if (!empty($component)) { 598 // If component is not empty, it will be passed too, in order to allow tracking too. 599 $params['component'] = $component; 600 } 601 602 return new \moodle_url('/h5p/embed.php', $params); 603 } 604 605 /** 606 * Return the info export file for Mobile App. 607 * 608 * @return array or null 609 */ 610 public function get_export_file(): ?array { 611 // Get the export url. 612 $exporturl = $this->get_export_settings(true); 613 // Get the filename of the export url. 614 $path = $exporturl->out_as_local_url(); 615 // Check if the URL has parameters. 616 $parts = explode('?', $path); 617 $path = array_shift($parts); 618 $parts = explode('/', $path); 619 $filename = array_pop($parts); 620 // Get the required info from the export file to be able to get the export file by third apps. 621 $file = helper::get_export_info($filename, $exporturl); 622 623 return $file; 624 } 625 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body