From 153c45625d49445c7533784dced2ee911229102e Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Fri, 21 Feb 2020 09:16:00 +0100 Subject: [PATCH] MDL-67795 h5p: move methods from player to helper --- h5p/classes/api.php | 309 +++++++++++++++++++++++++++++++++++++ h5p/classes/helper.php | 27 ++++ h5p/classes/player.php | 310 +++----------------------------------- h5p/tests/api_test.php | 178 ++++++++++++++++++++++ h5p/tests/helper_test.php | 38 +++++ 5 files changed, 569 insertions(+), 293 deletions(-) diff --git a/h5p/classes/api.php b/h5p/classes/api.php index 983619d2a83..691731cff33 100644 --- a/h5p/classes/api.php +++ b/h5p/classes/api.php @@ -26,6 +26,8 @@ namespace core_h5p; defined('MOODLE_INTERNAL') || die(); +use core\lock\lock_config; + /** * Contains API class for the H5P area. * @@ -176,4 +178,311 @@ class api { return $libraries; } + + /** + * Get the H5P DB instance id for a H5P pluginfile URL. If it doesn't exist, it's not created. + * + * @param string $url H5P pluginfile URL. + * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions + * + * @return array of [file, stdClass|false]: + * - file local file for this $url. + * - stdClass is an H5P object or false if there isn't any H5P with this URL. + */ + public static function get_content_from_pluginfile_url(string $url, bool $preventredirect = true): array { + global $DB; + + // Deconstruct the URL and get the pathname associated. + $pathnamehash = self::get_pluginfile_hash($url, $preventredirect); + if (!$pathnamehash) { + return [false, false]; + } + + // Get the file. + $fs = get_file_storage(); + $file = $fs->get_file_by_hash($pathnamehash); + if (!$file) { + return [false, false]; + } + + $h5p = $DB->get_record('h5p', ['pathnamehash' => $pathnamehash]); + return [$file, $h5p]; + } + + /** + * Create, if it doesn't exist, the H5P DB instance id for a H5P pluginfile URL. If it exists: + * - If the content is not the same, remove the existing content and re-deploy the H5P content again. + * - If the content is the same, returns the H5P identifier. + * + * @param string $url H5P pluginfile URL. + * @param stdClass $config Configuration for H5P buttons. + * @param factory $factory The \core_h5p\factory object + * @param stdClass $messages The error, exception and info messages, raised while preparing and running an H5P content. + * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions + * + * @return array of [file, h5pid]: + * - file local file for this $url. + * - h5pid is the H5P identifier or false if there isn't any H5P with this URL. + */ + public static function create_content_from_pluginfile_url(string $url, \stdClass $config, factory $factory, + \stdClass &$messages, bool $preventredirect = true): array { + global $USER; + + $core = $factory->get_core(); + list($file, $h5p) = self::get_content_from_pluginfile_url($url, $preventredirect); + + if (!$file) { + $core->h5pF->setErrorMessage(get_string('h5pfilenotfound', 'core_h5p')); + return [false, false]; + } + + $contenthash = $file->get_contenthash(); + if ($h5p && $h5p->contenthash != $contenthash) { + // The content exists and it is different from the one deployed previously. The existing one should be removed before + // deploying the new version. + self::delete_content($h5p, $factory); + $h5p = false; + } + + $context = \context::instance_by_id($file->get_contextid()); + if ($h5p) { + // The H5P content has been deployed previously. + $displayoptions = helper::get_display_options($core, $config); + // Check if the user can set the displayoptions. + if ($displayoptions != $h5p->displayoptions && has_capability('moodle/h5p:setdisplayoptions', $context)) { + // If the displayoptions has changed and the user has permission to modify it, update this information in the DB. + $core->h5pF->updateContentFields($h5p->id, ['displayoptions' => $displayoptions]); + } + return [$file, $h5p->id]; + } else { + // The H5P content hasn't been deployed previously. + + // Check if the user uploading the H5P content is "trustable". If the file hasn't been uploaded by a user with this + // capability, the content won't be deployed and an error message will be displayed. + if (!helper::can_deploy_package($file)) { + $core->h5pF->setErrorMessage(get_string('nopermissiontodeploy', 'core_h5p')); + return [$file, false]; + } + + // The H5P content can be only deployed if the author of the .h5p file can update libraries or if all the + // content-type libraries exist, to avoid users without the h5p:updatelibraries capability upload malicious content. + $onlyupdatelibs = !helper::can_update_library($file); + + // Start lock to prevent synchronous access to save the same H5P. + $lockfactory = lock_config::get_lock_factory('core_h5p'); + $lockkey = 'core_h5p_' . $file->get_pathnamehash(); + if ($lock = $lockfactory->get_lock($lockkey, 10)) { + try { + // Validate and store the H5P content before displaying it. + $h5pid = helper::save_h5p($factory, $file, $config, $onlyupdatelibs, false); + } finally { + $lock->release(); + } + } else { + $core->h5pF->setErrorMessage(get_string('lockh5pdeploy', 'core_h5p')); + return [$file, false]; + }; + + if (!$h5pid && $file->get_userid() != $USER->id && has_capability('moodle/h5p:updatelibraries', $context)) { + // The user has permission to update libraries but the package has been uploaded by a different + // user without this permission. Check if there is some missing required library error. + $missingliberror = false; + $messages = helper::get_messages($messages, $factory); + if (!empty($messages->error)) { + foreach ($messages->error as $error) { + if ($error->code == 'missing-required-library') { + $missingliberror = true; + break; + } + } + } + if ($missingliberror) { + // The message about the permissions to upload libraries should be removed. + $infomsg = "Note that the libraries may exist in the file you uploaded, but you're not allowed to upload " . + "new libraries. Contact the site administrator about this."; + if (($key = array_search($infomsg, $messages->info)) !== false) { + unset($messages->info[$key]); + } + + // No library will be installed and an error will be displayed, because this content is not trustable. + $core->h5pF->setInfoMessage(get_string('notrustablefile', 'core_h5p')); + } + return [$file, false]; + + } + return [$file, $h5pid]; + } + } + + /** + * Delete an H5P package. + * + * @param stdClass $content The H5P package to delete with, at least content['id]. + * @param factory $factory The \core_h5p\factory object + */ + public static function delete_content(\stdClass $content, factory $factory): void { + $h5pstorage = $factory->get_storage(); + + // Add an empty slug to the content if it's not defined, because the H5P library requires this field exists. + // It's not used when deleting a package, so the real slug value is not required at this point. + $content->slug = $content->slug ?? ''; + $h5pstorage->deletePackage( (array) $content); + } + + /** + * Delete an H5P package deployed from the defined $url. + * + * @param string $url pluginfile URL of the H5P package to delete. + * @param factory $factory The \core_h5p\factory object + */ + public static function delete_content_from_pluginfile_url(string $url, factory $factory): void { + // Get the H5P to delete. + list($file, $h5p) = self::get_content_from_pluginfile_url($url); + if ($h5p) { + self::delete_content($h5p, $factory); + } + } + + /** + * Get the pathnamehash from an H5P internal URL. + * + * @param string $url H5P pluginfile URL poiting to an H5P file. + * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions + * + * @return string|false pathnamehash for the file in the internal URL. + */ + protected static function get_pluginfile_hash(string $url, bool $preventredirect = true) { + global $USER, $CFG; + + // Decode the URL before start processing it. + $url = new \moodle_url(urldecode($url)); + + // Remove params from the URL (such as the 'forcedownload=1'), to avoid errors. + $url->remove_params(array_keys($url->params())); + $path = $url->out_as_local_url(); + + // We only need the slasharguments. + $path = substr($path, strpos($path, '.php/') + 5); + $parts = explode('/', $path); + $filename = array_pop($parts); + + // If the request is made by tokenpluginfile.php we need to avoid userprivateaccesskey. + if (strpos($url, '/tokenpluginfile.php')) { + array_shift($parts); + } + + // Get the contextid, component and filearea. + $contextid = array_shift($parts); + $component = array_shift($parts); + $filearea = array_shift($parts); + + // Ignore draft files, because they are considered temporary files, so shouldn't be displayed. + if ($filearea == 'draft') { + return false; + } + + // Get the context. + try { + list($context, $course, $cm) = get_context_info_array($contextid); + } catch (\moodle_exception $e) { + throw new \moodle_exception('invalidcontextid', 'core_h5p'); + } + + // For CONTEXT_USER, such as the private files, raise an exception if the owner of the file is not the current user. + if ($context->contextlevel == CONTEXT_USER && $USER->id !== $context->instanceid) { + throw new \moodle_exception('h5pprivatefile', 'core_h5p'); + } + + // For CONTEXT_COURSECAT No login necessary - unless login forced everywhere. + if ($context->contextlevel == CONTEXT_COURSECAT) { + if ($CFG->forcelogin) { + require_login(null, true, null, false, true); + } + } + + // For CONTEXT_BLOCK. + if ($context->contextlevel == CONTEXT_BLOCK) { + if ($context->get_course_context(false)) { + // If block is in course context, then check if user has capability to access course. + require_course_login($course, true, null, false, true); + } else if ($CFG->forcelogin) { + // No login necessary - unless login forced everywhere. + require_login(null, true, null, false, true); + } else { + // Get parent context and see if user have proper permission. + $parentcontext = $context->get_parent_context(); + if ($parentcontext->contextlevel === CONTEXT_COURSECAT) { + // Check if category is visible and user can view this category. + if (!core_course_category::get($parentcontext->instanceid, IGNORE_MISSING)) { + send_file_not_found(); + } + } else if ($parentcontext->contextlevel === CONTEXT_USER && $parentcontext->instanceid != $USER->id) { + // The block is in the context of a user, it is only visible to the user who it belongs to. + send_file_not_found(); + } + if ($filearea !== 'content') { + send_file_not_found(); + } + } + } + + // For CONTEXT_MODULE and CONTEXT_COURSE check if the user is enrolled in the course. + // And for CONTEXT_MODULE has permissions view this .h5p file. + if ($context->contextlevel == CONTEXT_MODULE || + $context->contextlevel == CONTEXT_COURSE) { + // Require login to the course first (without login to the module). + require_course_login($course, true, null, !$preventredirect, $preventredirect); + + // Now check if module is available OR it is restricted but the intro is shown on the course page. + if ($context->contextlevel == CONTEXT_MODULE) { + $cminfo = \cm_info::create($cm); + if (!$cminfo->uservisible) { + if (!$cm->showdescription || !$cminfo->is_visible_on_course_page()) { + // Module intro is not visible on the course page and module is not available, show access error. + require_course_login($course, true, $cminfo, !$preventredirect, $preventredirect); + } + } + } + } + + // Some components, such as mod_page or mod_resource, add the revision to the URL to prevent caching problems. + // So the URL contains this revision number as itemid but a 0 is always stored in the files table. + // In order to get the proper hash, a callback should be done (looking for those exceptions). + $pathdata = null; + if ($context->contextlevel == CONTEXT_MODULE || $context->contextlevel == CONTEXT_BLOCK) { + $pathdata = component_callback($component, 'get_path_from_pluginfile', [$filearea, $parts], null); + } + if (null === $pathdata) { + // Look for the components and fileareas which have empty itemid defined in xxx_pluginfile. + $hasnullitemid = false; + $hasnullitemid = $hasnullitemid || ($component === 'user' && ($filearea === 'private' || $filearea === 'profile')); + $hasnullitemid = $hasnullitemid || (substr($component, 0, 4) === 'mod_' && $filearea === 'intro'); + $hasnullitemid = $hasnullitemid || ($component === 'course' && + ($filearea === 'summary' || $filearea === 'overviewfiles')); + $hasnullitemid = $hasnullitemid || ($component === 'coursecat' && $filearea === 'description'); + $hasnullitemid = $hasnullitemid || ($component === 'backup' && + ($filearea === 'course' || $filearea === 'activity' || $filearea === 'automated')); + if ($hasnullitemid) { + $itemid = 0; + } else { + $itemid = array_shift($parts); + } + + if (empty($parts)) { + $filepath = '/'; + } else { + $filepath = '/' . implode('/', $parts) . '/'; + } + } else { + // The itemid and filepath have been returned by the component callback. + [ + 'itemid' => $itemid, + 'filepath' => $filepath, + ] = $pathdata; + } + + $fs = get_file_storage(); + $pathnamehash = $fs->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename); + return $pathnamehash; + } } diff --git a/h5p/classes/helper.php b/h5p/classes/helper.php index b561b5e2ca3..f34236ea5c4 100644 --- a/h5p/classes/helper.php +++ b/h5p/classes/helper.php @@ -83,6 +83,33 @@ class helper { } + /** + * Get the error messages stored in our H5P framework. + * + * @param stdClass $messages The error, exception and info messages, raised while preparing and running an H5P content. + * @param factory $factory The \core_h5p\factory object + * + * @return stdClass with framework error messages. + */ + public static function get_messages(\stdClass $messages, factory $factory): \stdClass { + $core = $factory->get_core(); + + // Check if there are some errors and store them in $messages. + if (empty($messages->error)) { + $messages->error = $core->h5pF->getMessages('error') ?: false; + } else { + $messages->error = array_merge($messages->error, $core->h5pF->getMessages('error')); + } + + if (empty($messages->info)) { + $messages->info = $core->h5pF->getMessages('info') ?: false; + } else { + $messages->info = array_merge($messages->info, $core->h5pF->getMessages('info')); + } + + return $messages; + } + /** * Get the representation of display options as int. * diff --git a/h5p/classes/player.php b/h5p/classes/player.php index e8c68a6cbdb..23c542010a6 100644 --- a/h5p/classes/player.php +++ b/h5p/classes/player.php @@ -28,7 +28,6 @@ defined('MOODLE_INTERNAL') || die(); use core_h5p\local\library\autoloader; use core_xapi\local\statement\item_activity; -use core\lock\lock_config; /** * H5P player class, for displaying any local H5P content. @@ -124,12 +123,21 @@ class player { $this->core = $this->factory->get_core(); // Get the H5P identifier linked to this URL. - if ($this->h5pid = $this->get_h5p_id($url, $config)) { - // Load the content of the H5P content associated to this $url. - $this->content = $this->core->loadContent($this->h5pid); - - // Get the embedtype to use for displaying the H5P content. - $this->embedtype = core::determineEmbedType($this->content['embedType'], $this->content['library']['embedTypes']); + list($file, $this->h5pid) = api::create_content_from_pluginfile_url( + $url, + $config, + $this->factory, + $this->messages + ); + if ($file) { + $this->context = \context::instance_by_id($file->get_contextid()); + if ($this->h5pid) { + // Load the content of the H5P content associated to this $url. + $this->content = $this->core->loadContent($this->h5pid); + + // Get the embedtype to use for displaying the H5P content. + $this->embedtype = core::determineEmbedType($this->content['embedType'], $this->content['library']['embedTypes']); + } } } @@ -174,20 +182,7 @@ class player { * @return stdClass with framework error messages. */ public function get_messages(): \stdClass { - // Check if there are some errors and store them in $messages. - if (empty($this->messages->error)) { - $this->messages->error = $this->core->h5pF->getMessages('error') ?: false; - } else { - $this->messages->error = array_merge($this->messages->error, $this->core->h5pF->getMessages('error')); - } - - if (empty($this->messages->info)) { - $this->messages->info = $this->core->h5pF->getMessages('info') ?: false; - } else { - $this->messages->info = array_merge($this->messages->info, $this->core->h5pF->getMessages('info')); - } - - return $this->messages; + return helper::get_messages($this->messages, $this->factory); } /** @@ -249,7 +244,7 @@ class player { \core_h5p\event\h5p_viewed::create([ 'objectid' => $this->h5pid, 'userid' => $USER->id, - 'context' => $this->context, + 'context' => $this->get_context(), 'other' => [ 'url' => $this->url->out(), 'time' => time() @@ -277,277 +272,6 @@ class player { return $this->context; } - /** - * Get the H5P DB instance id for a H5P pluginfile URL. The H5P file will be saved if it doesn't exist previously or - * if its content has changed. Besides, the displayoptions in the $config will be also updated when they have changed and - * the user has the right permissions. - * - * @param string $url H5P pluginfile URL. - * @param stdClass $config Configuration for H5P buttons. - * - * @return int|false H5P DB identifier. - */ - private function get_h5p_id(string $url, \stdClass $config) { - global $DB, $USER; - - $fs = get_file_storage(); - - // Deconstruct the URL and get the pathname associated. - $pathnamehash = $this->get_pluginfile_hash($url); - if (!$pathnamehash) { - $this->core->h5pF->setErrorMessage(get_string('h5pfilenotfound', 'core_h5p')); - return false; - } - - // Get the file. - $file = $fs->get_file_by_hash($pathnamehash); - if (!$file) { - $this->core->h5pF->setErrorMessage(get_string('h5pfilenotfound', 'core_h5p')); - return false; - } - - $h5p = $DB->get_record('h5p', ['pathnamehash' => $pathnamehash]); - $contenthash = $file->get_contenthash(); - if ($h5p && $h5p->contenthash != $contenthash) { - // The content exists and it is different from the one deployed previously. The existing one should be removed before - // deploying the new version. - $this->delete_h5p($h5p); - $h5p = false; - } - - if ($h5p) { - // The H5P content has been deployed previously. - $displayoptions = $this->get_display_options($config); - // Check if the user can set the displayoptions. - if ($displayoptions != $h5p->displayoptions && has_capability('moodle/h5p:setdisplayoptions', $this->context)) { - // If the displayoptions has changed and the user has permission to modify it, update this information in the DB. - $this->core->h5pF->updateContentFields($h5p->id, ['displayoptions' => $displayoptions]); - } - return $h5p->id; - } else { - // The H5P content hasn't been deployed previously. - - // Check if the user uploading the H5P content is "trustable". If the file hasn't been uploaded by a user with this - // capability, the content won't be deployed and an error message will be displayed. - if (!helper::can_deploy_package($file)) { - $this->core->h5pF->setErrorMessage(get_string('nopermissiontodeploy', 'core_h5p')); - return false; - } - - // The H5P content can be only deployed if the author of the .h5p file can update libraries or if all the - // content-type libraries exist, to avoid users without the h5p:updatelibraries capability upload malicious content. - $onlyupdatelibs = !helper::can_update_library($file); - - // Start lock to prevent synchronous access to save the same h5p. - $lockfactory = lock_config::get_lock_factory('core_h5p'); - $lockkey = 'core_h5p_' . $pathnamehash; - if ($lock = $lockfactory->get_lock($lockkey, 10)) { - try { - // Validate and store the H5P content before displaying it. - $h5pid = helper::save_h5p($this->factory, $file, $config, $onlyupdatelibs, false); - } finally { - $lock->release(); - } - } else { - $this->core->h5pF->setErrorMessage(get_string('lockh5pdeploy', 'core_h5p')); - return false; - }; - if (!$h5pid && $file->get_userid() != $USER->id && has_capability('moodle/h5p:updatelibraries', $this->context)) { - // The user has permission to update libraries but the package has been uploaded by a different - // user without this permission. Check if there is some missing required library error. - $missingliberror = false; - $messages = $this->get_messages(); - if (!empty($messages->error)) { - foreach ($messages->error as $error) { - if ($error->code == 'missing-required-library') { - $missingliberror = true; - break; - } - } - } - if ($missingliberror) { - // The message about the permissions to upload libraries should be removed. - $infomsg = "Note that the libraries may exist in the file you uploaded, but you're not allowed to upload " . - "new libraries. Contact the site administrator about this."; - if (($key = array_search($infomsg, $messages->info)) !== false) { - unset($messages->info[$key]); - } - - // No library will be installed and an error will be displayed, because this content is not trustable. - $this->core->h5pF->setInfoMessage(get_string('notrustablefile', 'core_h5p')); - } - return false; - - } - return $h5pid; - } - } - - /** - * Get the pathnamehash from an H5P internal URL. - * - * @param string $url H5P pluginfile URL poiting to an H5P file. - * - * @return string|false pathnamehash for the file in the internal URL. - */ - private function get_pluginfile_hash(string $url) { - global $USER, $CFG; - - // Decode the URL before start processing it. - $url = new \moodle_url(urldecode($url)); - - // Remove params from the URL (such as the 'forcedownload=1'), to avoid errors. - $url->remove_params(array_keys($url->params())); - $path = $url->out_as_local_url(); - - // We only need the slasharguments. - $path = substr($path, strpos($path, '.php/') + 5); - $parts = explode('/', $path); - $filename = array_pop($parts); - - // If the request is made by tokenpluginfile.php we need to avoid userprivateaccesskey. - if (strpos($this->url, '/tokenpluginfile.php')) { - array_shift($parts); - } - // Get the contextid, component and filearea. - $contextid = array_shift($parts); - $component = array_shift($parts); - $filearea = array_shift($parts); - - // Ignore draft files, because they are considered temporary files, so shouldn't be displayed. - if ($filearea == 'draft') { - return false; - } - - // Get the context. - try { - list($this->context, $course, $cm) = get_context_info_array($contextid); - } catch (\moodle_exception $e) { - throw new \moodle_exception('invalidcontextid', 'core_h5p'); - } - - // For CONTEXT_USER, such as the private files, raise an exception if the owner of the file is not the current user. - if ($this->context->contextlevel == CONTEXT_USER && $USER->id !== $this->context->instanceid) { - throw new \moodle_exception('h5pprivatefile', 'core_h5p'); - } - - // For CONTEXT_COURSECAT No login necessary - unless login forced everywhere. - if ($this->context->contextlevel == CONTEXT_COURSECAT) { - if ($CFG->forcelogin) { - require_login(null, true, null, false, true); - } - } - - // For CONTEXT_BLOCK. - if ($this->context->contextlevel == CONTEXT_BLOCK) { - if ($this->context->get_course_context(false)) { - // If block is in course context, then check if user has capability to access course. - require_course_login($course, true, null, false, true); - } else if ($CFG->forcelogin) { - // No login necessary - unless login forced everywhere. - require_login(null, true, null, false, true); - } else { - // Get parent context and see if user have proper permission. - $parentcontext = $this->context->get_parent_context(); - if ($parentcontext->contextlevel === CONTEXT_COURSECAT) { - // Check if category is visible and user can view this category. - if (!core_course_category::get($parentcontext->instanceid, IGNORE_MISSING)) { - send_file_not_found(); - } - } else if ($parentcontext->contextlevel === CONTEXT_USER && $parentcontext->instanceid != $USER->id) { - // The block is in the context of a user, it is only visible to the user who it belongs to. - send_file_not_found(); - } - if ($filearea !== 'content') { - send_file_not_found(); - } - } - } - - // For CONTEXT_MODULE and CONTEXT_COURSE check if the user is enrolled in the course. - // And for CONTEXT_MODULE has permissions view this .h5p file. - if ($this->context->contextlevel == CONTEXT_MODULE || - $this->context->contextlevel == CONTEXT_COURSE) { - // Require login to the course first (without login to the module). - require_course_login($course, true, null, !$this->preventredirect, $this->preventredirect); - - // Now check if module is available OR it is restricted but the intro is shown on the course page. - if ($this->context->contextlevel == CONTEXT_MODULE) { - $cminfo = \cm_info::create($cm); - if (!$cminfo->uservisible) { - if (!$cm->showdescription || !$cminfo->is_visible_on_course_page()) { - // Module intro is not visible on the course page and module is not available, show access error. - require_course_login($course, true, $cminfo, !$this->preventredirect, $this->preventredirect); - } - } - } - } - - // Some components, such as mod_page or mod_resource, add the revision to the URL to prevent caching problems. - // So the URL contains this revision number as itemid but a 0 is always stored in the files table. - // In order to get the proper hash, a callback should be done (looking for those exceptions). - $pathdata = null; - if ($this->context->contextlevel == CONTEXT_MODULE || $this->context->contextlevel == CONTEXT_BLOCK) { - $pathdata = component_callback($component, 'get_path_from_pluginfile', [$filearea, $parts], null); - } - if (null === $pathdata) { - // Look for the components and fileareas which have empty itemid defined in xxx_pluginfile. - $hasnullitemid = false; - $hasnullitemid = $hasnullitemid || ($component === 'user' && ($filearea === 'private' || $filearea === 'profile')); - $hasnullitemid = $hasnullitemid || (substr($component, 0, 4) === 'mod_' && $filearea === 'intro'); - $hasnullitemid = $hasnullitemid || ($component === 'course' && - ($filearea === 'summary' || $filearea === 'overviewfiles')); - $hasnullitemid = $hasnullitemid || ($component === 'coursecat' && $filearea === 'description'); - $hasnullitemid = $hasnullitemid || ($component === 'backup' && - ($filearea === 'course' || $filearea === 'activity' || $filearea === 'automated')); - if ($hasnullitemid) { - $itemid = 0; - } else { - $itemid = array_shift($parts); - } - - if (empty($parts)) { - $filepath = '/'; - } else { - $filepath = '/' . implode('/', $parts) . '/'; - } - } else { - // The itemid and filepath have been returned by the component callback. - [ - 'itemid' => $itemid, - 'filepath' => $filepath, - ] = $pathdata; - } - - $fs = get_file_storage(); - return $fs->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename); - } - - /** - * Get the representation of display options as int. - * @param stdClass $config Button options config. - * - * @return int The representation of display options as int. - */ - private function get_display_options(\stdClass $config): int { - $export = isset($config->export) ? $config->export : 0; - $embed = isset($config->embed) ? $config->embed : 0; - $copyright = isset($config->copyright) ? $config->copyright : 0; - $frame = ($export || $embed || $copyright); - if (!$frame) { - $frame = isset($config->frame) ? $config->frame : 0; - } - - $disableoptions = [ - core::DISPLAY_OPTION_FRAME => $frame, - core::DISPLAY_OPTION_DOWNLOAD => $export, - core::DISPLAY_OPTION_EMBED => $embed, - core::DISPLAY_OPTION_COPYRIGHT => $copyright, - ]; - - return $this->core->getStorableDisplayOptions($disableoptions, 0); - } - /** * Delete an H5P package. * diff --git a/h5p/tests/api_test.php b/h5p/tests/api_test.php index d64e1eb3775..f7f01b88a03 100644 --- a/h5p/tests/api_test.php +++ b/h5p/tests/api_test.php @@ -273,4 +273,182 @@ class api_testcase extends \advanced_testcase { ], ]; } + + /** + * Test the behaviour of get_content_from_pluginfile_url(). + */ + public function test_get_content_from_pluginfile_url(): void { + $this->setRunTestInSeparateProcess(true); + $this->resetAfterTest(); + $factory = new factory(); + + // Create the H5P data. + $filename = 'find-the-words.h5p'; + $path = __DIR__ . '/fixtures/' . $filename; + $fakefile = helper::create_fake_stored_file_from_path($path); + $config = (object)[ + 'frame' => 1, + 'export' => 1, + 'embed' => 0, + 'copyright' => 0, + ]; + + // Get URL for this H5P content file. + $syscontext = \context_system::instance(); + $url = \moodle_url::make_pluginfile_url( + $syscontext->id, + \core_h5p\file_storage::COMPONENT, + 'unittest', + $fakefile->get_itemid(), + '/', + $filename + ); + + // Scenario 1: Get the H5P for this URL and check there isn't any existing H5P (because it hasn't been saved). + list($newfile, $h5p) = api::get_content_from_pluginfile_url($url->out()); + $this->assertEquals($fakefile->get_pathnamehash(), $newfile->get_pathnamehash()); + $this->assertEquals($fakefile->get_contenthash(), $newfile->get_contenthash()); + $this->assertFalse($h5p); + + // Scenario 2: Save the H5P and check now the H5P is exactly the same as the original one. + $h5pid = helper::save_h5p($factory, $fakefile, $config); + list($newfile, $h5p) = api::get_content_from_pluginfile_url($url->out()); + + $this->assertEquals($h5pid, $h5p->id); + $this->assertEquals($fakefile->get_pathnamehash(), $h5p->pathnamehash); + $this->assertEquals($fakefile->get_contenthash(), $h5p->contenthash); + + // Scenario 3: Get the H5P for an unexisting H5P file. + $url = \moodle_url::make_pluginfile_url( + $syscontext->id, + \core_h5p\file_storage::COMPONENT, + 'unittest', + $fakefile->get_itemid(), + '/', + 'unexisting.h5p' + ); + list($newfile, $h5p) = api::get_content_from_pluginfile_url($url->out()); + $this->assertFalse($newfile); + $this->assertFalse($h5p); + } + + /** + * Test the behaviour of create_content_from_pluginfile_url(). + */ + public function test_create_content_from_pluginfile_url(): void { + global $DB; + + $this->setRunTestInSeparateProcess(true); + $this->resetAfterTest(); + $factory = new factory(); + + // Create the H5P data. + $filename = 'find-the-words.h5p'; + $path = __DIR__ . '/fixtures/' . $filename; + $fakefile = helper::create_fake_stored_file_from_path($path); + $config = (object)[ + 'frame' => 1, + 'export' => 1, + 'embed' => 0, + 'copyright' => 0, + ]; + + // Get URL for this H5P content file. + $syscontext = \context_system::instance(); + $url = \moodle_url::make_pluginfile_url( + $syscontext->id, + \core_h5p\file_storage::COMPONENT, + 'unittest', + $fakefile->get_itemid(), + '/', + $filename + ); + + // Scenario 1: Create the H5P from this URL and check the content is exactly the same as the fake file. + $messages = new \stdClass(); + list($newfile, $h5pid) = api::create_content_from_pluginfile_url($url->out(), $config, $factory, $messages); + $this->assertNotFalse($h5pid); + $h5p = $DB->get_record('h5p', ['id' => $h5pid]); + $this->assertEquals($fakefile->get_pathnamehash(), $h5p->pathnamehash); + $this->assertEquals($fakefile->get_contenthash(), $h5p->contenthash); + $this->assertTrue(empty($messages->error)); + $this->assertTrue(empty($messages->info)); + + // Scenario 2: Create the H5P for an unexisting H5P file. + $url = \moodle_url::make_pluginfile_url( + $syscontext->id, + \core_h5p\file_storage::COMPONENT, + 'unittest', + $fakefile->get_itemid(), + '/', + 'unexisting.h5p' + ); + list($newfile, $h5p) = api::create_content_from_pluginfile_url($url->out(), $config, $factory, $messages); + $this->assertFalse($newfile); + $this->assertFalse($h5p); + $this->assertTrue(empty($messages->error)); + $this->assertTrue(empty($messages->info)); + } + + /** + * Test the behaviour of delete_content_from_pluginfile_url(). + */ + public function test_delete_content_from_pluginfile_url(): void { + global $DB; + + $this->setRunTestInSeparateProcess(true); + $this->resetAfterTest(); + $factory = new factory(); + + // Create the H5P data. + $filename = 'find-the-words.h5p'; + $path = __DIR__ . '/fixtures/' . $filename; + $fakefile = helper::create_fake_stored_file_from_path($path); + $config = (object)[ + 'frame' => 1, + 'export' => 1, + 'embed' => 0, + 'copyright' => 0, + ]; + + // Get URL for this H5P content file. + $syscontext = \context_system::instance(); + $url = \moodle_url::make_pluginfile_url( + $syscontext->id, + \core_h5p\file_storage::COMPONENT, + 'unittest', + $fakefile->get_itemid(), + '/', + $filename + ); + + // Scenario 1: Try to remove the H5P content for an undeployed file. + list($newfile, $h5p) = api::get_content_from_pluginfile_url($url->out()); + $this->assertEquals(0, $DB->count_records('h5p')); + api::delete_content_from_pluginfile_url($url->out(), $factory); + $this->assertEquals(0, $DB->count_records('h5p')); + + // Scenario 2: Deploy an H5P from this URL, check it's created, remove it and check it has been removed as expected. + $this->assertEquals(0, $DB->count_records('h5p')); + + $messages = new \stdClass(); + list($newfile, $h5pid) = api::create_content_from_pluginfile_url($url->out(), $config, $factory, $messages); + $this->assertEquals(1, $DB->count_records('h5p')); + + api::delete_content_from_pluginfile_url($url->out(), $factory); + $this->assertEquals(0, $DB->count_records('h5p')); + + // Scenario 3: Try to remove the H5P for an unexisting H5P URL. + $url = \moodle_url::make_pluginfile_url( + $syscontext->id, + \core_h5p\file_storage::COMPONENT, + 'unittest', + $fakefile->get_itemid(), + '/', + 'unexisting.h5p' + ); + $this->assertEquals(0, $DB->count_records('h5p')); + api::delete_content_from_pluginfile_url($url->out(), $factory); + $this->assertEquals(0, $DB->count_records('h5p')); + } } diff --git a/h5p/tests/helper_test.php b/h5p/tests/helper_test.php index c216a2ce126..734bd08e431 100644 --- a/h5p/tests/helper_test.php +++ b/h5p/tests/helper_test.php @@ -290,4 +290,42 @@ class helper_testcase extends \advanced_testcase { $candeploy = helper::can_update_library($file); $this->assertTrue($candeploy); } + + /** + * Test the behaviour of get_messages(). + */ + public function test_get_messages(): void { + $this->resetAfterTest(); + + $factory = new \core_h5p\factory(); + $messages = new \stdClass(); + + helper::get_messages($messages, $factory); + $this->assertTrue(empty($messages->error)); + $this->assertTrue(empty($messages->info)); + + // Add an some messages manually and check they are still there. + $messages->error['error1'] = 'Testing ERROR message'; + $messages->info['info1'] = 'Testing INFO message'; + $messages->info['info2'] = 'Testing INFO message'; + helper::get_messages($messages, $factory); + $this->assertCount(1, $messages->error); + $this->assertCount(2, $messages->info); + + // When saving an invalid .h5p file, 6 errors should be raised. + $path = __DIR__ . '/fixtures/h5ptest.zip'; + $file = helper::create_fake_stored_file_from_path($path); + $factory->get_framework()->set_file($file); + $config = (object)[ + 'frame' => 1, + 'export' => 1, + 'embed' => 0, + 'copyright' => 0, + ]; + $h5pid = helper::save_h5p($factory, $file, $config); + $this->assertFalse($h5pid); + helper::get_messages($messages, $factory); + $this->assertCount(7, $messages->error); + $this->assertCount(2, $messages->info); + } } -- 2.43.0