From a11b55d22f8d49e4ef65dbec7b37511be6a82534 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Fri, 13 Sep 2019 14:23:55 +0200 Subject: [PATCH] MDL-66402 core_h5p: adapted code for deploying H5P The code added by Bas in the manager.php file has been moved to the player file (we can decide later whether that's the correct location or we should move to any other place like api.php/manager.php or similar). There is still a lot of code to review (all the required for displaying the H5P content). Besides, some TODOs have been added in order to take into account some tasks/checks. --- h5p/classes/player.php | 386 ++++++++++++++++++++++++++++++++++++++++- h5p/embed.php | 19 +- 2 files changed, 391 insertions(+), 14 deletions(-) diff --git a/h5p/classes/player.php b/h5p/classes/player.php index f07e7ac21085e..886b787aff3f3 100644 --- a/h5p/classes/player.php +++ b/h5p/classes/player.php @@ -29,23 +29,368 @@ defined('MOODLE_INTERNAL') || die(); class player { - protected $embedtype; - protected $settings; + + /** + * @var string The local H5P URL containing the .h5p file to display. + */ + private $url; + + /** + * @var \H5PCore The H5PCore object. + */ + private $core; + + private $h5pid; + + private $jsrequires; + + private $cssrequires; + + /** + * @var string + */ + private $embedtype; + + /** + * @var array + */ + private $settings; /** * Inits the H5P player for rendering the content. * - * @param string $pluginfile Local URL of the H5P file to display. + * @param string $url Local URL of the H5P file to display. + */ + public function __construct(string $url) { + global $CFG; + + $this->url = $url; + $this->jsrequires = []; + $this->cssrequires = []; + $context = \context_system::instance(); + $this->core = \core_h5p\framework::instance(); + // Get the H5P identifier linked to this URL. + $this->h5pid = $this->get_h5p_id($url); + + $this->content = $this->core->loadContent($this->h5pid); + $this->settings = $this->get_core_assets($context); + $displayoptions = $this->core->getDisplayOptionsForView(0, $this->h5pid); + // TODO: Remove this hack (it has been added to display the export and embed buttons). + $displayoptions['export'] = true; + $displayoptions['embed'] = true; + $displayoptions['copy'] = true; + // END + $this->settings['contents'][ 'cid-' . $this->h5pid ] = [ + 'library' => \H5PCore::libraryToString($this->content['library']), + 'jsonContent' => $this->get_filtered_parameters(), + 'fullScreen' => $this->content['library']['fullscreen'], + 'exportUrl' => $this->get_export_settings($displayoptions[ \H5PCore::DISPLAY_OPTION_DOWNLOAD ]), + 'embedCode' => "No Embed Code", + 'resizeCode' => $this->get_resize_code(), + 'title' => $this->content['slug'], + 'displayOptions' => $displayoptions, + 'url' => "{$CFG->wwwroot}/h5p/embed.php?id={$this->h5pid}", + 'contentUrl' => "{$CFG->wwwroot}/pluginfile.php/{$context->id}/core_h5p/content/{$this->h5pid}", + 'metadata' => '', + 'contentUserData' => array() + ]; + + // TODO: Use determineEmbedType to update the embed type. + $this->embedtype = 'iframe'; + + $this->files = $this->get_dependency_files(); + $this->generate_assets(); + } + + /** + * Get the H5P DB instance id for a H5P pluginfile URL. + * + * @param string $url H5P pluginfile URL. + * @return int H5P DB identifier. + */ + private function get_h5p_id($url) { + global $DB; + + $hash = $this->get_pluginfile_hash($url); + // TODO: Check what happens if there is no hash. + + $h5p = $DB->get_record('h5p', ['pathnamehash' => $hash]); + if (!$h5p) { + // The H5P content hasn't been deployed previously. It has to be validated and stored before displaying it. + $fs = get_file_storage(); + $file = $fs->get_file_by_hash($hash); + if (!$file) { + // TODO: Throw an exception or move the string to the lang. + return "File not found"; + } else { + return $this->save_h5p($file, $hash); + } + } else { + // The H5P content has been deployed previously. + + // TODO: Check if the file has been updated after being deployed and redeploy it if needed. + + return $h5p->id; + } + } + + /** + * Get the pluginfile hash for an H5P internal URL. + * + * @param string $url H5P pluginfile URL + * @return string hash for pluginfile */ - public function __construct(string $pluginfile) { + private function get_pluginfile_hash($url) { global $CFG; + // TODO: Validate this method with all the places where the Atto editor can be used. + // TODO: Take into account $CFG->slasharguments. + + $path = str_replace($CFG->wwwroot, '', $url); + $parts = array_reverse(explode('/', $path)); + + $i = 0; + $filename = $parts[$i++]; + $filepath = '/'; + if (is_numeric($parts[$i])) { + $itemid = $parts[$i++]; + } else { + $itemid = 0; + } + $filearea = $parts[$i++]; + $component = $parts[$i++]; + $contextid = $parts[$i++]; + + // TODO: Review how to avoid the following dirty hack for getting the correct itemid. + // Dirty hack for the 'mod_page' because, although the itemid = 0 in DB, there is a /1/ in the URL. + if ($component == 'mod_page') { + $itemid = 0; + } + + $fs = get_file_storage(); + return $fs->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename); + } + + /** + * Store a H5P file + * + * @param Object $file Moodle file instance + * @param string $hash + * + * @return int|false The H5P identifier or false if it's not a valid H5P package. + */ + private function save_h5p($file, $hash) { + global $CFG; + + $path = $this->core->fs->getTmpPath(); + $this->core->h5pF->getUploadedH5pFolderPath($path); + // Add manually the extension to the file to avoid the validation fails. + $path .= '.h5p'; + $this->core->h5pF->getUploadedH5pPath($path); + + // Copy the .h5p file to the temporary folder. + $file->copy_content_to($path); + + // Check if the h5p file is valid before saving it. + $h5pvalidator = \core_h5p\framework::instance('validator'); + if ($h5pvalidator->isValidPackage(false, false)) { + $h5pstorage = \core_h5p\framework::instance('storage'); + $h5pstorage->savePackage(null, $hash, false); + return $h5pstorage->contentId; + } else { + $messages = $this->core->h5pF->getMessages('error'); + $errors = array_map(function($error) { + return $error->message; + }, $messages); + throw new \Exception(implode(',', $errors)); + } + + return false; + } + + /** + * Export path for settings + * + * @param $downloadenabled + * + * @return string + */ + private function get_export_settings($downloadenabled) { + global $CFG; + + if ( ! $downloadenabled) { + return ''; + } + + $context = \context_system::instance(); + //TODO: Get the expected context (not the system one). + //$modulecontext = \context_module::instance($this->cm->id); + $slug = $this->content['slug'] ? $this->content['slug'] . '-' : ''; + $url = \moodle_url::make_pluginfile_url( + $context->id, + \core_h5p\file_storage::COMPONENT, + \core_h5p\file_storage::EXPORT_FILEAREA, + '', + '', + "{$slug}{$this->content['id']}.h5p" + ); + + return $url->out(); + } + + private function get_cache_buster() { + return '?ver=' . 1; + } + + private function get_core_assets($context) { + global $CFG, $PAGE; + // Get core settings. + $settings = $this->get_core_settings($context); + $settings['core'] = [ + 'styles' => [], + 'scripts' => [] + ]; + $settings['loadedJs'] = []; + $settings['loadedCss'] = []; + + // Make sure files are reloaded for each plugin update. + $cachebuster = $this->get_cache_buster(); + + // Use relative URL to support both http and https. + $liburl = $CFG->wwwroot . '/lib/h5p/'; + $relpath = '/' . preg_replace('/^[^:]+:\/\/[^\/]+\//', '', $liburl); + + // Add core stylesheets. + foreach (\H5PCore::$styles as $style) { + $settings['core']['styles'][] = $relpath . $style . $cachebuster; + //$this->cssrequires[] = new moodle_url($liburl . $style . $cachebuster); + } + // Add core JavaScript. + foreach (\H5PCore::$scripts as $script) { + $settings['core']['scripts'][] = $relpath . $script . $cachebuster; + $this->jsrequires[] = new \moodle_url($liburl . $script . $cachebuster); + } + + return $settings; + } + + private function get_core_settings($context) { + global $USER, $CFG; + + $basepath = $CFG->wwwroot . '/'; + $systemcontext = \context_system::instance(); + // Check permissions and generate ajax paths. + $ajaxpaths = []; + $ajaxpaths['setFinished'] = ''; + $ajaxpaths['xAPIResult'] = ''; + $ajaxpaths['contentUserData'] = ''; + + $settings = array( + 'baseUrl' => $basepath, + 'url' => "{", + 'urlLibraries' => "{$basepath}pluginfile.php/{$systemcontext->id}/core_h5p/libraries", + 'postUserStatistics' => true, + 'ajax' => $ajaxpaths, + 'saveFreq' => false, + 'siteUrl' => $CFG->wwwroot, + 'l10n' => array('H5P' => $this->core->getLocalization()), + 'user' => [], + 'hubIsEnabled' => false, + 'reportingIsEnabled' => true, + 'crossorigin' => null, + 'libraryConfig' => '', + 'pluginCacheBuster' => $this->get_cache_buster(), + 'libraryUrl' => '' + ); + + return $settings; + } + + private function generate_assets() { + global $CFG; + + if ($this->embedtype === 'div') { + $context = \context_system::instance(); + $h5ppath = "/pluginfile.php/{$context->id}/core_h5p"; + + // Schedule JavaScripts for loading through Moodle. + foreach ($this->files['scripts'] as $script) { + $url = $script->path . $script->version; + + // Add URL prefix if not external. + $isexternal = strpos($script->path, '://'); + if ($isexternal === false) { + $url = $h5ppath . $url; + } + $this->settings['loadedJs'][] = $url; + $this->jsrequires[] = new \moodle_url($isexternal ? $url : $CFG->wwwroot . $url); + } + + // Schedule stylesheets for loading through Moodle. + foreach ($this->files['styles'] as $style) { + $url = $style->path . $style->version; + + // Add URL prefix if not external. + $isexternal = strpos($style->path, '://'); + if ($isexternal === false) { + $url = $h5ppath . $url; + } + $this->settings['loadedCss'][] = $url; + $this->cssrequires[] = new \moodle_url($isexternal ? $url : $CFG->wwwroot . $url); + } + + } else { + // JavaScripts and stylesheets will be loaded through h5p.js. + $cid = 'cid-' . $this->h5pid; + $this->settings['contents'][ $cid ]['scripts'] = $this->core->getAssetsUrls($this->files['scripts']); + $this->settings['contents'][ $cid ]['styles'] = $this->core->getAssetsUrls($this->files['styles']); + } + } + + /** + * Finds library dependencies of view + * + * @return array Files that the view has dependencies to + */ + private function get_dependency_files() { + global $PAGE; + + $preloadeddeps = $this->core->loadContentDependencies($this->h5pid, 'preloaded'); + $files = $this->core->getDependenciesFiles($preloadeddeps); + + return $files; + } + + private function get_filtered_parameters() { + global $PAGE; + + $safeparameters = $this->core->filterParameters($this->content); + + $decodedparams = json_decode($safeparameters); + $safeparameters = json_encode($decodedparams); + + return $safeparameters; + } + + /** + * Resizing script for settings + * + * @param $embedenabled + * + * @return string + */ + private function get_resize_code() { + global $CFG; + + $resizeurl = new \moodle_url($CFG->wwwroot . '/lib/h5p/js/h5p-resizer.js'); + + return ""; } /** * Adds js assets to the current page. */ - public function addassetstopage() { + public function add_assets_to_page() { global $PAGE, $CFG; foreach ($this->jsrequires as $script) { @@ -63,14 +408,14 @@ public function addassetstopage() { /** * Outputs an H5P content. */ - public function outputview() { + public function output() { if ($this->embedtype === 'div') { - echo "
idnumber}\">
"; + echo "
h5pid}\">
"; } else { echo "
" . - "