MDL-67795 h5p: move methods from player to helper

This commit is contained in:
Sara Arjona 2020-02-21 09:16:00 +01:00
parent 172d3ee7ab
commit 153c45625d
5 changed files with 568 additions and 292 deletions

View file

@ -26,6 +26,8 @@ namespace core_h5p;
defined('MOODLE_INTERNAL') || die(); defined('MOODLE_INTERNAL') || die();
use core\lock\lock_config;
/** /**
* Contains API class for the H5P area. * Contains API class for the H5P area.
* *
@ -176,4 +178,311 @@ class api {
return $libraries; 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;
}
} }

View file

@ -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. * Get the representation of display options as int.
* *

View file

@ -28,7 +28,6 @@ defined('MOODLE_INTERNAL') || die();
use core_h5p\local\library\autoloader; use core_h5p\local\library\autoloader;
use core_xapi\local\statement\item_activity; use core_xapi\local\statement\item_activity;
use core\lock\lock_config;
/** /**
* H5P player class, for displaying any local H5P content. * H5P player class, for displaying any local H5P content.
@ -124,12 +123,21 @@ class player {
$this->core = $this->factory->get_core(); $this->core = $this->factory->get_core();
// Get the H5P identifier linked to this URL. // Get the H5P identifier linked to this URL.
if ($this->h5pid = $this->get_h5p_id($url, $config)) { list($file, $this->h5pid) = api::create_content_from_pluginfile_url(
// Load the content of the H5P content associated to this $url. $url,
$this->content = $this->core->loadContent($this->h5pid); $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. // Get the embedtype to use for displaying the H5P content.
$this->embedtype = core::determineEmbedType($this->content['embedType'], $this->content['library']['embedTypes']); $this->embedtype = core::determineEmbedType($this->content['embedType'], $this->content['library']['embedTypes']);
}
} }
} }
@ -174,20 +182,7 @@ class player {
* @return stdClass with framework error messages. * @return stdClass with framework error messages.
*/ */
public function get_messages(): \stdClass { public function get_messages(): \stdClass {
// Check if there are some errors and store them in $messages. return helper::get_messages($this->messages, $this->factory);
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;
} }
/** /**
@ -249,7 +244,7 @@ class player {
\core_h5p\event\h5p_viewed::create([ \core_h5p\event\h5p_viewed::create([
'objectid' => $this->h5pid, 'objectid' => $this->h5pid,
'userid' => $USER->id, 'userid' => $USER->id,
'context' => $this->context, 'context' => $this->get_context(),
'other' => [ 'other' => [
'url' => $this->url->out(), 'url' => $this->url->out(),
'time' => time() 'time' => time()
@ -277,277 +272,6 @@ class player {
return $this->context; 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. * Delete an H5P package.
* *

View file

@ -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'));
}
} }

View file

@ -290,4 +290,42 @@ class helper_testcase extends \advanced_testcase {
$candeploy = helper::can_update_library($file); $candeploy = helper::can_update_library($file);
$this->assertTrue($candeploy); $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);
}
} }