mirror of
https://github.com/moodle/moodle.git
synced 2025-08-04 16:36:37 +02:00
MDL-67795 h5p: move methods from player to helper
This commit is contained in:
parent
172d3ee7ab
commit
153c45625d
5 changed files with 568 additions and 292 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
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']);
|
||||
// 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.
|
||||
*
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue