first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+840
View File
@@ -0,0 +1,840 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains API class for the H5P area.
*
* @package core_h5p
* @copyright 2020 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_h5p;
use core\lock\lock_config;
use Moodle\H5PCore;
/**
* Contains API class for the H5P area.
*
* @copyright 2020 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class api {
/**
* Delete a library and also all the libraries depending on it and the H5P contents using it. For the H5P content, only the
* database entries in {h5p} are removed (the .h5p files are not removed in order to let users to deploy them again).
*
* @param factory $factory The H5P factory.
* @param \stdClass $library The library to delete.
*/
public static function delete_library(factory $factory, \stdClass $library): void {
global $DB;
// Get the H5P contents using this library, to remove them from DB. The .h5p files won't be removed
// so they will be displayed by the player next time a user with the proper permissions accesses it.
$sql = 'SELECT DISTINCT hcl.h5pid
FROM {h5p_contents_libraries} hcl
WHERE hcl.libraryid = :libraryid';
$params = ['libraryid' => $library->id];
$h5pcontents = $DB->get_records_sql($sql, $params);
foreach ($h5pcontents as $h5pcontent) {
$factory->get_framework()->deleteContentData($h5pcontent->h5pid);
}
$fs = $factory->get_core()->fs;
$framework = $factory->get_framework();
// Delete the library from the file system.
$fs->delete_library(array('libraryId' => $library->id));
// Delete also the cache assets to rebuild them next time.
$framework->deleteCachedAssets($library->id);
// Remove library data from database.
$DB->delete_records('h5p_library_dependencies', array('libraryid' => $library->id));
$DB->delete_records('h5p_libraries', array('id' => $library->id));
// Remove the library from the cache.
$libscache = \cache::make('core', 'h5p_libraries');
$libarray = [
'machineName' => $library->machinename,
'majorVersion' => $library->majorversion,
'minorVersion' => $library->minorversion,
];
$libstring = H5PCore::libraryToString($libarray);
$librarykey = helper::get_cache_librarykey($libstring);
$libscache->delete($librarykey);
// Remove the libraries using this library.
$requiredlibraries = self::get_dependent_libraries($library->id);
foreach ($requiredlibraries as $requiredlibrary) {
self::delete_library($factory, $requiredlibrary);
}
}
/**
* Get all the libraries using a defined library.
*
* @param int $libraryid The library to get its dependencies.
* @return array List of libraryid with all the libraries required by a defined library.
*/
public static function get_dependent_libraries(int $libraryid): array {
global $DB;
$sql = 'SELECT *
FROM {h5p_libraries}
WHERE id IN (SELECT DISTINCT hl.id
FROM {h5p_library_dependencies} hld
JOIN {h5p_libraries} hl ON hl.id = hld.libraryid
WHERE hld.requiredlibraryid = :libraryid)';
$params = ['libraryid' => $libraryid];
return $DB->get_records_sql($sql, $params);
}
/**
* Get a library from an identifier.
*
* @param int $libraryid The library identifier.
* @return \stdClass The library object having the library identifier defined.
* @throws dml_exception A DML specific exception is thrown if the libraryid doesn't exist.
*/
public static function get_library(int $libraryid): \stdClass {
global $DB;
return $DB->get_record('h5p_libraries', ['id' => $libraryid], '*', MUST_EXIST);
}
/**
* Returns a library as an object with properties that correspond to the fetched row's field names.
*
* @param array $params An associative array with the values of the machinename, majorversion and minorversion fields.
* @param bool $configurable A library that has semantics so it can be configured in the editor.
* @param string $fields Library attributes to retrieve.
*
* @return \stdClass|null An object with one attribute for each field name in $fields param.
*/
public static function get_library_details(array $params, bool $configurable, string $fields = ''): ?\stdClass {
global $DB;
$select = "machinename = :machinename
AND majorversion = :majorversion
AND minorversion = :minorversion";
if ($configurable) {
$select .= " AND semantics IS NOT NULL";
}
$fields = $fields ?: '*';
$record = $DB->get_record_select('h5p_libraries', $select, $params, $fields);
return $record ?: null;
}
/**
* Get all the H5P content type libraries versions.
*
* @param string|null $fields Library fields to return.
*
* @return array An array with an object for each content type library installed.
*/
public static function get_contenttype_libraries(?string $fields = ''): array {
global $DB;
$libraries = [];
$fields = $fields ?: '*';
$select = "runnable = :runnable
AND semantics IS NOT NULL";
$params = ['runnable' => 1];
$sort = 'title, majorversion DESC, minorversion DESC';
$records = $DB->get_records_select('h5p_libraries', $select, $params, $sort, $fields);
$added = [];
foreach ($records as $library) {
// Remove unique index.
unset($library->id);
// Convert snakes to camels.
$library->majorVersion = (int) $library->majorversion;
unset($library->major_version);
$library->minorVersion = (int) $library->minorversion;
unset($library->minorversion);
$library->metadataSettings = json_decode($library->metadatasettings ?? '');
// If we already add this library means that it is an old version,as the previous query was sorted by version.
if (isset($added[$library->name])) {
$library->isOld = true;
} else {
$added[$library->name] = true;
}
// Add new library.
$libraries[] = $library;
}
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
* @param bool $skipcapcheck Whether capabilities should be checked or not to get the pluginfile URL because sometimes they
* might be controlled before calling this method.
*
* @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,
bool $skipcapcheck = false): array {
global $DB;
// Deconstruct the URL and get the pathname associated.
if ($skipcapcheck || self::can_access_pluginfile_hash($url, $preventredirect)) {
$pathnamehash = self::get_pluginfile_hash($url);
}
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];
}
/**
* Get the original file and H5P DB instance for a given H5P pluginfile URL. If it doesn't exist, it's not created.
* If the file has been added as a reference, this method will return the original linked file.
*
* @param string $url H5P pluginfile URL.
* @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions.
* @param bool $skipcapcheck Whether capabilities should be checked or not to get the pluginfile URL because sometimes they
* might be controlled before calling this method.
*
* @return array of [\stored_file|false, \stdClass|false, \stored_file|false]:
* - \stored_file: original local file for the given url (if it has been added as a reference, this method
* will return the linked file) or false if there isn't any H5P file with this URL.
* - \stdClass: an H5P object or false if there isn't any H5P with this URL.
* - \stored_file: file associated to the given url (if it's different from original) or false when both files
* (original and file) are the same.
* @since Moodle 4.0
*/
public static function get_original_content_from_pluginfile_url(string $url, bool $preventredirect = true,
bool $skipcapcheck = false): array {
$file = false;
list($originalfile, $h5p) = self::get_content_from_pluginfile_url($url, $preventredirect, $skipcapcheck);
if ($originalfile) {
if ($reference = $originalfile->get_reference()) {
$file = $originalfile;
// If the file has been added as a reference to any other file, get it.
$fs = new \file_storage();
$referenced = \file_storage::unpack_reference($reference);
$originalfile = $fs->get_file(
$referenced['contextid'],
$referenced['component'],
$referenced['filearea'],
$referenced['itemid'],
$referenced['filepath'],
$referenced['filename']
);
$h5p = self::get_content_from_pathnamehash($originalfile->get_pathnamehash());
if (empty($h5p)) {
$h5p = false;
}
}
}
return [$originalfile, $h5p, $file];
}
/**
* Check if the user can edit an H5P file. It will return true in the following situations:
* - The user is the author of the file.
* - The component is different from user (i.e. private files).
* - If the component is contentbank, the user can edit this file (calling the ContentBank API).
* - If the component is mod_xxx or block_xxx, the user has the addinstance capability.
* - If the component implements the can_edit_content in the h5p\canedit class and the callback to this method returns true.
*
* @param \stored_file $file The H5P file to check.
*
* @return boolean Whether the user can edit or not the given file.
* @since Moodle 4.0
*/
public static function can_edit_content(\stored_file $file): bool {
global $USER;
list($type, $component) = \core_component::normalize_component($file->get_component());
// Private files.
$currentuserisauthor = $file->get_userid() == $USER->id;
$isuserfile = $component === 'user';
if ($currentuserisauthor && $isuserfile) {
// The user can edit the content because it's a private user file and she is the owner.
return true;
}
// Check if the plugin where the file belongs implements the custom can_edit_content method and call it if that's the case.
$classname = '\\' . $file->get_component() . '\\h5p\\canedit';
$methodname = 'can_edit_content';
if (method_exists($classname, $methodname)) {
return $classname::{$methodname}($file);
}
// For mod/block files, check if the user has the addinstance capability of the component where the file belongs.
if ($type === 'mod' || $type === 'block') {
// For any other component, check whether the user can add/edit them.
$context = \context::instance_by_id($file->get_contextid());
$plugins = \core_component::get_plugin_list($type);
$isvalid = array_key_exists($component, $plugins);
if ($isvalid && has_capability("$type/$component:addinstance", $context)) {
// The user can edit the content because she has the capability for creating instances where the file belongs.
return true;
}
}
// For contentbank files, use the API to check if the user has access.
if ($component == 'contentbank') {
$cb = new \core_contentbank\contentbank();
$content = $cb->get_content_from_id($file->get_itemid());
$contenttype = $content->get_content_type_instance();
if ($contenttype instanceof \contenttype_h5p\contenttype) {
// Only H5P contenttypes should be considered here.
if ($contenttype->can_edit($content)) {
// The user has permissions to edit the H5P in the content bank.
return true;
}
}
}
return false;
}
/**
* 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
* @param bool $skipcapcheck Whether capabilities should be checked or not to get the pluginfile URL because sometimes they
* might be controlled before calling this method.
*
* @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, bool $skipcapcheck = false): array {
global $USER;
$core = $factory->get_core();
list($file, $h5p) = self::get_content_from_pluginfile_url($url, $preventredirect, $skipcapcheck);
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.
// If the main library for this H5P content is disabled, the content won't be displayed.
$mainlibrary = (object) ['id' => $h5p->mainlibraryid];
if (!self::is_library_enabled($mainlibrary)) {
$core->h5pF->setErrorMessage(get_string('mainlibrarydisabled', 'core_h5p'));
return [$file, false];
} else {
$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 displayoptions has changed and user has permission to modify it, update this information in 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 {
global $DB;
// Get the H5P to delete.
$pathnamehash = self::get_pluginfile_hash($url);
$h5p = $DB->get_record('h5p', ['pathnamehash' => $pathnamehash]);
if ($h5p) {
self::delete_content($h5p, $factory);
}
}
/**
* If user can access 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 bool if user can access pluginfile hash.
* @throws \moodle_exception
* @throws \coding_exception
* @throws \require_login_exception
*/
protected static function can_access_pluginfile_hash(string $url, bool $preventredirect = true): bool {
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);
// 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);
// 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');
}
if (!is_siteadmin($USER)) {
// 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);
}
}
}
}
}
return true;
}
/**
* 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.
*
* @throws \moodle_exception
*/
protected static function get_pluginfile_hash(string $url) {
// 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');
}
// 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;
}
/**
* Returns the H5P content object corresponding to an H5P content file.
*
* @param string $pathnamehash The pathnamehash of the file associated to an H5P content.
*
* @return null|\stdClass H5P content object or null if not found.
*/
public static function get_content_from_pathnamehash(string $pathnamehash): ?\stdClass {
global $DB;
$h5p = $DB->get_record('h5p', ['pathnamehash' => $pathnamehash]);
return ($h5p) ? $h5p : null;
}
/**
* Return the H5P export information file when the file has been deployed.
* Otherwise, return null if H5P file:
* i) has not been deployed.
* ii) has changed the content.
*
* The information returned will be:
* - filename, filepath, mimetype, filesize, timemodified and fileurl.
*
* @param int $contextid ContextId of the H5P activity.
* @param factory $factory The \core_h5p\factory object.
* @param string $component component
* @param string $filearea file area
* @return array|null Return file info otherwise null.
*/
public static function get_export_info_from_context_id(int $contextid,
factory $factory,
string $component,
string $filearea): ?array {
$core = $factory->get_core();
$fs = get_file_storage();
$files = $fs->get_area_files($contextid, $component, $filearea, 0, 'id', false);
$file = reset($files);
if ($h5p = self::get_content_from_pathnamehash($file->get_pathnamehash())) {
if ($h5p->contenthash == $file->get_contenthash()) {
$content = $core->loadContent($h5p->id);
$slug = $content['slug'] ? $content['slug'] . '-' : '';
$filename = "{$slug}{$content['id']}.h5p";
$deployedfile = helper::get_export_info($filename, null, $factory);
return $deployedfile;
}
}
return null;
}
/**
* Enable or disable a library.
*
* @param int $libraryid The id of the library to enable/disable.
* @param bool $isenabled True if the library should be enabled; false otherwise.
*/
public static function set_library_enabled(int $libraryid, bool $isenabled): void {
global $DB;
$library = $DB->get_record('h5p_libraries', ['id' => $libraryid], '*', MUST_EXIST);
if ($library->runnable) {
// For now, only runnable libraries can be enabled/disabled.
$record = [
'id' => $libraryid,
'enabled' => $isenabled,
];
$DB->update_record('h5p_libraries', $record);
}
}
/**
* Check whether a library is enabled or not. When machinename is passed, it will return false if any of the versions
* for this machinename is disabled.
* If the library doesn't exist, it will return true.
*
* @param \stdClass $librarydata Supported fields for library: 'id' and 'machichename'.
* @return bool
* @throws \moodle_exception
*/
public static function is_library_enabled(\stdClass $librarydata): bool {
global $DB;
$params = [];
if (property_exists($librarydata, 'machinename')) {
$params['machinename'] = $librarydata->machinename;
}
if (property_exists($librarydata, 'id')) {
$params['id'] = $librarydata->id;
}
if (empty($params)) {
throw new \moodle_exception("Missing 'machinename' or 'id' in librarydata parameter");
}
$libraries = $DB->get_records('h5p_libraries', $params);
// If any of the libraries with these values have been disabled, return false.
foreach ($libraries as $id => $library) {
if (!$library->enabled) {
return false;
}
}
return true;
}
/**
* Check whether an H5P package is valid or not.
*
* @param \stored_file $file The file with the H5P content.
* @param bool $onlyupdatelibs Whether new libraries can be installed or only the existing ones can be updated
* @param bool $skipcontent Should the content be skipped (so only the libraries will be saved)?
* @param factory|null $factory The \core_h5p\factory object
* @param bool $deletefiletree Should the temporary files be deleted before returning?
* @return bool True if the H5P file is valid (expected format, valid libraries...); false otherwise.
*/
public static function is_valid_package(\stored_file $file, bool $onlyupdatelibs, bool $skipcontent = false,
?factory $factory = null, bool $deletefiletree = true): bool {
// This may take a long time.
\core_php_time_limit::raise();
$isvalid = false;
if (empty($factory)) {
$factory = new factory();
}
$core = $factory->get_core();
$h5pvalidator = $factory->get_validator();
// Set the H5P file path.
$core->h5pF->set_file($file);
$path = $core->fs->getTmpPath();
$core->h5pF->getUploadedH5pFolderPath($path);
// Add manually the extension to the file to avoid the validation fails.
$path .= '.h5p';
$core->h5pF->getUploadedH5pPath($path);
// Copy the .h5p file to the temporary folder.
$file->copy_content_to($path);
if ($h5pvalidator->isValidPackage($skipcontent, $onlyupdatelibs)) {
if ($skipcontent) {
$isvalid = true;
} else if (!empty($h5pvalidator->h5pC->mainJsonData['mainLibrary'])) {
$mainlibrary = (object) ['machinename' => $h5pvalidator->h5pC->mainJsonData['mainLibrary']];
if (self::is_library_enabled($mainlibrary)) {
$isvalid = true;
} else {
// If the main library of the package is disabled, the H5P content will be considered invalid.
$core->h5pF->setErrorMessage(get_string('mainlibrarydisabled', 'core_h5p'));
}
}
}
if ($deletefiletree) {
// Remove temp content folder.
H5PCore::deleteFileTree($path);
}
return $isvalid;
}
}
+459
View File
@@ -0,0 +1,459 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_h5p;
defined('MOODLE_INTERNAL') || die();
require_once("$CFG->libdir/filelib.php");
use Moodle\H5PCore;
use Moodle\H5PFrameworkInterface;
use Moodle\H5PHubEndpoints;
use stdClass;
use moodle_url;
use core_h5p\local\library\autoloader;
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
// phpcs:disable moodle.NamingConventions.ValidVariableName.VariableNameLowerCase
/**
* H5P core class, containing functions and storage shared by the other H5P classes.
*
* @package core_h5p
* @copyright 2019 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core extends H5PCore {
/** @var array The array containing all the present libraries */
protected $libraries;
/**
* Constructor for core_h5p/core.
*
* @param H5PFrameworkInterface $framework The frameworks implementation of the H5PFrameworkInterface
* @param string|H5PFileStorage $path The H5P file storage directory or class
* @param string $url The URL to the file storage directory
* @param string $language The language code. Defaults to english
* @param boolean $export Whether export is enabled
*/
public function __construct(H5PFrameworkInterface $framework, $path, string $url, string $language = 'en',
bool $export = false) {
parent::__construct($framework, $path, $url, $language, $export);
// Aggregate the assets by default.
$this->aggregateAssets = true;
}
/**
* Get the path to the dependency.
*
* @param array $dependency An array containing the information of the requested dependency library
* @return string The path to the dependency library
*/
protected function getDependencyPath(array $dependency): string {
$library = $this->find_library($dependency);
return "libraries/{$library->id}/" . H5PCore::libraryToFolderName($dependency);
}
/**
* Get the paths to the content dependencies.
*
* @param int $id The H5P content ID
* @return array An array containing the path of each content dependency
*/
public function get_dependency_roots(int $id): array {
$roots = [];
$dependencies = $this->h5pF->loadContentDependencies($id);
$context = \context_system::instance();
foreach ($dependencies as $dependency) {
$library = $this->find_library($dependency);
$roots[self::libraryToFolderName($dependency)] = (moodle_url::make_pluginfile_url(
$context->id,
'core_h5p',
'libraries',
$library->id,
"/" . self::libraryToFolderName($dependency),
''
))->out(false);
}
return $roots;
}
/**
* Get a particular dependency library.
*
* @param array $dependency An array containing information of the dependency library
* @return stdClass|null The library object if the library dependency exists, null otherwise
*/
protected function find_library(array $dependency): ?\stdClass {
global $DB;
if (null === $this->libraries) {
$this->libraries = $DB->get_records('h5p_libraries');
}
$major = $dependency['majorVersion'];
$minor = $dependency['minorVersion'];
$patch = $dependency['patchVersion'];
foreach ($this->libraries as $library) {
if ($library->machinename !== $dependency['machineName']) {
continue;
}
if ($library->majorversion != $major) {
continue;
}
if ($library->minorversion != $minor) {
continue;
}
if ($library->patchversion != $patch) {
continue;
}
return $library;
}
return null;
}
/**
* Get the list of JS scripts to include on the page.
*
* @return array The array containg urls of the core JavaScript files
*/
public static function get_scripts(): array {
global $PAGE;
$jsrev = $PAGE->requires->get_jsrev();
$urls = [];
foreach (self::$scripts as $script) {
$urls[] = autoloader::get_h5p_core_library_url($script, [
'ver' => $jsrev,
]);
}
$urls[] = new moodle_url("/h5p/js/h5p_overrides.js", [
'ver' => $jsrev,
]);
return $urls;
}
/**
* Fetch and install the latest H5P content types libraries from the official H5P repository.
* If the latest version of a content type library is present in the system, nothing is done for that content type.
*
* @return stdClass
*/
public function fetch_latest_content_types(): ?\stdClass {
$contenttypes = $this->get_latest_content_types();
if (!empty($contenttypes->error)) {
return $contenttypes;
}
$typesinstalled = [];
$factory = new factory();
$framework = $factory->get_framework();
foreach ($contenttypes->contentTypes as $type) {
// Don't fetch content types if any of the versions is disabled.
$librarydata = (object) ['machinename' => $type->id];
if (!api::is_library_enabled($librarydata)) {
continue;
}
// Don't fetch content types that require a higher H5P core API version.
if (!$this->is_required_core_api($type->coreApiVersionNeeded)) {
continue;
}
$library = [
'machineName' => $type->id,
'majorVersion' => $type->version->major,
'minorVersion' => $type->version->minor,
'patchVersion' => $type->version->patch,
];
// Add example and tutorial to the library, to store this information too.
if (isset($type->example)) {
$library['example'] = $type->example;
}
if (isset($type->tutorial)) {
$library['tutorial'] = $type->tutorial;
}
$shoulddownload = true;
if ($framework->getLibraryId($type->id, $type->version->major, $type->version->minor)) {
if (!$framework->isPatchedLibrary($library)) {
$shoulddownload = false;
}
}
if ($shoulddownload) {
$installed['id'] = $this->fetch_content_type($library);
if ($installed['id']) {
$installed['name'] = H5PCore::libraryToString($library);
$typesinstalled[] = $installed;
}
}
}
$result = new stdClass();
$result->error = '';
$result->typesinstalled = $typesinstalled;
return $result;
}
/**
* Given an H5P content type machine name, fetch and install the required library from the official H5P repository.
*
* @param array $library Library machineName, majorversion and minorversion.
* @return int|null Returns the id of the content type library installed, null otherwise.
*/
public function fetch_content_type(array $library): ?int {
global $DB;
$factory = new factory();
$fs = get_file_storage();
// Delete any existing file, if it was not deleted during a previous download.
$existing = $fs->get_file(
(\context_system::instance())->id,
'core_h5p',
'library_sources',
0,
'/',
$library['machineName']
);
if ($existing) {
$existing->delete();
}
// Download the latest content type from the H5P official repository.
$file = $fs->create_file_from_url(
(object) [
'component' => 'core_h5p',
'filearea' => 'library_sources',
'itemid' => 0,
'contextid' => (\context_system::instance())->id,
'filepath' => '/',
'filename' => $library['machineName'],
],
$this->get_api_endpoint($library['machineName']),
null,
true
);
if (!$file) {
return null;
}
helper::save_h5p($factory, $file, (object) [], false, true);
$file->delete();
$librarykey = static::libraryToString($library);
if (is_null($factory->get_storage()->h5pC->librariesJsonData)) {
// There was an error fetching the content type.
debugging('Error fetching content type: ' . $librarykey);
return null;
}
$libraryjson = $factory->get_storage()->h5pC->librariesJsonData[$librarykey];
if (is_null($libraryjson) || !array_key_exists('libraryId', $libraryjson)) {
// There was an error fetching the content type.
debugging('Error fetching content type: ' . $librarykey);
return null;
}
$libraryid = $libraryjson['libraryId'];
// Update example and tutorial (if any of them are defined in $library).
$params = ['id' => $libraryid];
if (array_key_exists('example', $library)) {
$params['example'] = $library['example'];
}
if (array_key_exists('tutorial', $library)) {
$params['tutorial'] = $library['tutorial'];
}
if (count($params) > 1) {
$DB->update_record('h5p_libraries', $params);
}
return $libraryid;
}
/**
* Get H5P endpoints.
*
* If $endpoint = 'content' and $library is null, moodle_url is the endpoint of the latest version of the H5P content
* types; however, if $library is the machine name of a content type, moodle_url is the endpoint to download the content type.
* The SITES endpoint ($endpoint = 'site') may be use to get a site UUID or send site data.
*
* @param string|null $library The machineName of the library whose endpoint is requested.
* @param string $endpoint The endpoint required. Valid values: "site", "content".
* @return moodle_url The endpoint moodle_url object.
*/
public function get_api_endpoint(?string $library = null, string $endpoint = 'content'): moodle_url {
if ($endpoint == 'site') {
$h5purl = H5PHubEndpoints::createURL(H5PHubEndpoints::SITES );
} else if ($endpoint == 'content') {
$h5purl = H5PHubEndpoints::createURL(H5PHubEndpoints::CONTENT_TYPES ) . $library;
}
return new moodle_url($h5purl);
}
/**
* Get the latest version of the H5P content types available in the official repository.
*
* @return stdClass An object with 2 properties:
* - string error: error message when there is any problem, empty otherwise
* - array contentTypes: an object for each H5P content type with its information
*/
public function get_latest_content_types(): \stdClass {
global $CFG;
$siteuuid = $this->get_site_uuid() ?? md5($CFG->wwwroot);
$postdata = ['uuid' => $siteuuid];
// Get the latest content-types json.
$endpoint = $this->get_api_endpoint();
$request = download_file_content($endpoint, null, $postdata, true);
if (!empty($request->error) || $request->status != '200' || empty($request->results)) {
if (empty($request->error)) {
$request->error = get_string('fetchtypesfailure', 'core_h5p');
}
return $request;
}
$contenttypes = json_decode($request->results);
$contenttypes->error = '';
return $contenttypes;
}
/**
* Get the site UUID. If site UUID is not defined, try to register the site.
*
* return $string The site UUID, null if it is not set.
*/
public function get_site_uuid(): ?string {
// Check if the site_uuid is already set.
$siteuuid = get_config('core_h5p', 'site_uuid');
if (!$siteuuid) {
$siteuuid = $this->register_site();
}
return $siteuuid;
}
/**
* Get H5P generated site UUID.
*
* return ?string Returns H5P generated site UUID, null if can't get it.
*/
private function register_site(): ?string {
$endpoint = $this->get_api_endpoint(null, 'site');
$siteuuid = download_file_content($endpoint, null, '');
// Successful UUID retrieval from H5P.
if ($siteuuid) {
$json = json_decode($siteuuid);
if (isset($json->uuid)) {
set_config('site_uuid', $json->uuid, 'core_h5p');
return $json->uuid;
}
}
return null;
}
/**
* Checks that the required H5P core API version or higher is installed.
*
* @param stdClass $coreapi Object with properties major and minor for the core API version required.
* @return bool True if the required H5P core API version is installed. False if not.
*/
public function is_required_core_api($coreapi): bool {
if (isset($coreapi) && !empty($coreapi)) {
if (($coreapi->major > H5PCore::$coreApi['majorVersion']) ||
(($coreapi->major == H5PCore::$coreApi['majorVersion']) && ($coreapi->minor > H5PCore::$coreApi['minorVersion']))) {
return false;
}
}
return true;
}
/**
* Get the library string from a DB library record.
*
* @param stdClass $record The DB library record.
* @param bool $foldername If true, use hyphen instead of space in returned string.
* @return string The string name on the form {machineName} {majorVersion}.{minorVersion}.
*/
public static function record_to_string(stdClass $record, bool $foldername = false): string {
if ($foldername) {
return static::libraryToFolderName([
'machineName' => $record->machinename,
'majorVersion' => $record->majorversion,
'minorVersion' => $record->minorversion,
]);
} else {
return static::libraryToString([
'machineName' => $record->machinename,
'majorVersion' => $record->majorversion,
'minorVersion' => $record->minorversion,
]);
}
}
/**
* Small helper for getting the library's ID.
* This method is rewritten to use MUC (instead of an static variable which causes some problems with PHPUnit).
*
* @param array $library
* @param string $libString
* @return int Identifier, or FALSE if non-existent
*/
public function getLibraryId($library, $libString = null) {
if (!$libString) {
$libString = self::libraryToString($library);
}
// Check if this information has been saved previously into the cache.
$libcache = \cache::make('core', 'h5p_libraries');
$librarykey = helper::get_cache_librarykey($libString);
$libraryId = $libcache->get($librarykey);
if ($libraryId === false) {
$libraryId = $this->h5pF->getLibraryId($library['machineName'], $library['majorVersion'], $library['minorVersion']);
$libcache->set($librarykey, $libraryId);
}
return $libraryId;
}
}
+487
View File
@@ -0,0 +1,487 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* H5P editor class.
*
* @package core_h5p
* @copyright 2020 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_h5p;
use core_h5p\local\library\autoloader;
use core_h5p\output\h5peditor as editor_renderer;
use Moodle\H5PCore;
use Moodle\H5peditor;
use stdClass;
use coding_exception;
use MoodleQuickForm;
defined('MOODLE_INTERNAL') || die();
/**
* H5P editor class, for editing local H5P content.
*
* @package core_h5p
* @copyright 2020 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class editor {
/**
* @var core The H5PCore object.
*/
private $core;
/**
* @var H5peditor $h5peditor The H5P Editor object.
*/
private $h5peditor;
/**
* @var int Id of the H5P content from the h5p table.
*/
private $id = null;
/**
* @var array Existing H5P content instance before edition.
*/
private $oldcontent = null;
/**
* @var stored_file File of ane existing H5P content before edition.
*/
private $oldfile = null;
/**
* @var array File area to save the file of a new H5P content.
*/
private $filearea = null;
/**
* @var string H5P Library name
*/
private $library = null;
/**
* Inits the H5P editor.
*/
public function __construct() {
autoloader::register();
$factory = new factory();
$this->h5peditor = $factory->get_editor();
$this->core = $factory->get_core();
}
/**
* Loads an existing content for edition.
*
* If the H5P content or its file can't be retrieved, it is not possible to edit the content.
*
* @param int $id Id of the H5P content from the h5p table.
*
* @return void
*/
public function set_content(int $id): void {
$this->id = $id;
// Load the present content.
$this->oldcontent = $this->core->loadContent($id);
if ($this->oldcontent === null) {
throw new \moodle_exception('invalidelementid');
}
// Identify the content type library.
$this->library = H5PCore::libraryToString($this->oldcontent['library']);
// Get current file and its file area.
$pathnamehash = $this->oldcontent['pathnamehash'];
$fs = get_file_storage();
$oldfile = $fs->get_file_by_hash($pathnamehash);
if (!$oldfile) {
throw new \moodle_exception('invalidelementid');
}
$this->set_filearea(
$oldfile->get_contextid(),
$oldfile->get_component(),
$oldfile->get_filearea(),
$oldfile->get_itemid(),
$oldfile->get_filepath(),
$oldfile->get_filename(),
$oldfile->get_userid()
);
$this->oldfile = $oldfile;
}
/**
* Sets the content type library and the file area to create a new H5P content.
*
* Note: this method must be used to create new content, to edit an existing
* H5P content use only set_content with the ID from the H5P table.
*
* @param string $library Library of the H5P content type to create.
* @param int $contextid Context where the file of the H5P content will be stored.
* @param string $component Component where the file of the H5P content will be stored.
* @param string $filearea File area where the file of the H5P content will be stored.
* @param int $itemid Item id file of the H5P content.
* @param string $filepath File path where the file of the H5P content will be stored.
* @param null|string $filename H5P content file name.
* @param null|int $userid H5P content file owner userid (default will use $USER->id).
*
* @return void
*/
public function set_library(string $library, int $contextid, string $component, string $filearea,
?int $itemid = 0, string $filepath = '/', ?string $filename = null, ?int $userid = null): void {
$this->library = $library;
$this->set_filearea($contextid, $component, $filearea, $itemid, $filepath, $filename, $userid);
}
/**
* Sets the Moodle file area where the file of a new H5P content will be stored.
*
* @param int $contextid Context where the file of the H5P content will be stored.
* @param string $component Component where the file of the H5P content will be stored.
* @param string $filearea File area where the file of the H5P content will be stored.
* @param int $itemid Item id file of the H5P content.
* @param string $filepath File path where the file of the H5P content will be stored.
* @param null|string $filename H5P content file name.
* @param null|int $userid H5P content file owner userid (default will use $USER->id).
*
* @return void
*/
private function set_filearea(int $contextid, string $component, string $filearea,
int $itemid, string $filepath = '/', ?string $filename = null, ?int $userid = null): void {
global $USER;
$this->filearea = [
'contextid' => $contextid,
'component' => $component,
'filearea' => $filearea,
'itemid' => $itemid,
'filepath' => $filepath,
'filename' => $filename,
'userid' => $userid ?? $USER->id,
];
}
/**
* Adds an H5P editor to a form.
*
* @param MoodleQuickForm $mform Moodle Quick Form
*
* @return void
*/
public function add_editor_to_form(MoodleQuickForm $mform): void {
global $PAGE;
$this->add_assets_to_page();
$data = $this->data_preprocessing();
// Hidden fields used bu H5P editor.
$mform->addElement('hidden', 'h5plibrary', $data->h5plibrary);
$mform->setType('h5plibrary', PARAM_RAW);
$mform->addElement('hidden', 'h5pparams', $data->h5pparams);
$mform->setType('h5pparams', PARAM_RAW);
$mform->addElement('hidden', 'h5paction');
$mform->setType('h5paction', PARAM_ALPHANUMEXT);
// Render H5P editor.
$ui = new editor_renderer($data);
$editorhtml = $PAGE->get_renderer('core_h5p')->render($ui);
$mform->addElement('html', $editorhtml);
}
/**
* Creates or updates an H5P content.
*
* @param stdClass $content Object containing all the necessary data.
*
* @return int Content id
*/
public function save_content(stdClass $content): int {
if (empty($content->h5pparams)) {
throw new coding_exception('Missing H5P params.');
}
if (!isset($content->h5plibrary)) {
throw new coding_exception('Missing H5P library.');
}
$content->params = $content->h5pparams;
if (!empty($this->oldcontent)) {
$content->id = $this->oldcontent['id'];
// Get old parameters for comparison.
$oldparams = json_decode($this->oldcontent['params']) ?? null;
// Keep the existing display options.
$content->disable = $this->oldcontent['disable'];
$oldlib = $this->oldcontent['library'];
} else {
$oldparams = null;
$oldlib = null;
}
// Prepare library data to be save.
$content->library = H5PCore::libraryFromString($content->h5plibrary);
$content->library['libraryId'] = $this->core->h5pF->getLibraryId($content->library['machineName'],
$content->library['majorVersion'],
$content->library['minorVersion']);
// Prepare current parameters.
$params = json_decode($content->params);
$modified = false;
if (empty($params->metadata)) {
$params->metadata = new stdClass();
$modified = true;
}
if (empty($params->metadata->title)) {
// Use a default string if not available.
$params->metadata->title = 'Untitled';
$modified = true;
}
if (!isset($content->title)) {
$content->title = $params->metadata->title;
}
if ($modified) {
$content->params = json_encode($params);
}
// Save content.
$content->id = $this->core->saveContent((array)$content);
// Move any uploaded images or files. Determine content dependencies.
$this->h5peditor->processParameters($content, $content->library, $params->params, $oldlib, $oldparams);
$this->update_h5p_file($content);
return $content->id;
}
/**
* Creates or updates the H5P file and the related database data.
*
* @param stdClass $content Object containing all the necessary data.
*
* @return void
*/
private function update_h5p_file(stdClass $content): void {
global $USER;
// Keep title before filtering params.
$title = $content->title;
$contentarray = $this->core->loadContent($content->id);
$contentarray['title'] = $title;
// Generates filtered params and export file.
$this->core->filterParameters($contentarray);
$slug = isset($contentarray['slug']) ? $contentarray['slug'] . '-' : '';
$filename = $contentarray['id'] ?? $contentarray['title'];
$filename = $slug . $filename . '.h5p';
$file = $this->core->fs->get_export_file($filename);
$fs = get_file_storage();
if ($file) {
$fields['contenthash'] = $file->get_contenthash();
// Create or update H5P file.
if (empty($this->filearea['filename'])) {
$this->filearea['filename'] = $contentarray['slug'] . '.h5p';
}
if (!empty($this->oldfile)) {
$this->oldfile->replace_file_with($file);
$newfile = $this->oldfile;
} else {
$newfile = $fs->create_file_from_storedfile($this->filearea, $file);
}
if (empty($this->oldcontent)) {
$pathnamehash = $newfile->get_pathnamehash();
} else {
$pathnamehash = $this->oldcontent['pathnamehash'];
}
// Update hash fields in the h5p table.
$fields['pathnamehash'] = $pathnamehash;
$this->core->h5pF->updateContentFields($contentarray['id'], $fields);
}
}
/**
* Add required assets for displaying the editor.
*
* @return void
* @throws coding_exception If page header is already printed.
*/
private function add_assets_to_page(): void {
global $PAGE, $CFG;
if ($PAGE->headerprinted) {
throw new coding_exception('H5P assets cannot be added when header is already printed.');
}
$context = \context_system::instance();
$settings = helper::get_core_assets();
// Use jQuery and styles from core.
$assets = [
'css' => $settings['core']['styles'],
'js' => $settings['core']['scripts']
];
// Use relative URL to support both http and https.
$url = autoloader::get_h5p_editor_library_url()->out();
$url = '/' . preg_replace('/^[^:]+:\/\/[^\/]+\//', '', $url);
// Make sure files are reloaded for each plugin update.
$cachebuster = helper::get_cache_buster();
// Add editor styles.
foreach (H5peditor::$styles as $style) {
$assets['css'][] = $url . $style . $cachebuster;
}
// Add editor JavaScript.
foreach (H5peditor::$scripts as $script) {
// We do not want the creator of the iframe inside the iframe.
if ($script !== 'scripts/h5peditor-editor.js') {
$assets['js'][] = $url . $script . $cachebuster;
}
}
// Add JavaScript with library framework integration (editor part).
$PAGE->requires->js(autoloader::get_h5p_editor_library_url('scripts/h5peditor-editor.js' . $cachebuster), true);
$PAGE->requires->js(autoloader::get_h5p_editor_library_url('scripts/h5peditor-init.js' . $cachebuster), true);
// Load editor translations.
$language = framework::get_language();
$editorstrings = $this->get_editor_translations($language);
$PAGE->requires->data_for_js('H5PEditor.language.core', $editorstrings, false);
// Add JavaScript settings.
$root = $CFG->wwwroot;
$filespathbase = \moodle_url::make_draftfile_url(0, '', '');
$factory = new factory();
$contentvalidator = $factory->get_content_validator();
$editorajaxtoken = core::createToken(editor_ajax::EDITOR_AJAX_TOKEN);
$sesskey = sesskey();
$settings['editor'] = [
'filesPath' => $filespathbase->out(),
'fileIcon' => [
'path' => $url . 'images/binary-file.png',
'width' => 50,
'height' => 50,
],
'ajaxPath' => $CFG->wwwroot . "/h5p/ajax.php?sesskey={$sesskey}&token={$editorajaxtoken}&action=",
'libraryUrl' => $url,
'copyrightSemantics' => $contentvalidator->getCopyrightSemantics(),
'metadataSemantics' => $contentvalidator->getMetadataSemantics(),
'assets' => $assets,
'apiVersion' => H5PCore::$coreApi,
'language' => $language,
];
if (!empty($this->id)) {
$settings['editor']['nodeVersionId'] = $this->id;
// Override content URL.
$contenturl = "{$root}/pluginfile.php/{$context->id}/core_h5p/content/{$this->id}";
$settings['contents']['cid-' . $this->id]['contentUrl'] = $contenturl;
}
$PAGE->requires->data_for_js('H5PIntegration', $settings, true);
}
/**
* Get editor translations for the defined language.
* Check if the editor strings have been translated in Moodle.
* If the strings exist, they will override the existing ones in the JS file.
*
* @param string $language The language for the translations to be returned.
* @return array The editor string translations.
*/
private function get_editor_translations(string $language): array {
global $CFG;
// Add translations.
$languagescript = "language/{$language}.js";
if (!file_exists("{$CFG->dirroot}" . autoloader::get_h5p_editor_library_base($languagescript))) {
$languagescript = 'language/en.js';
}
// Check if the editor strings have been translated in Moodle.
// If the strings exist, they will override the existing ones in the JS file.
// Get existing strings from current JS language file.
$langcontent = file_get_contents("{$CFG->dirroot}" . autoloader::get_h5p_editor_library_base($languagescript));
// Get only the content between { } (for instance, ; at the end of the file has to be removed).
$langcontent = substr($langcontent, 0, strpos($langcontent, '}', -0) + 1);
$langcontent = substr($langcontent, strpos($langcontent, '{'));
// Parse the JS language content and get a PHP array.
$editorstrings = helper::parse_js_array($langcontent);
foreach ($editorstrings as $key => $value) {
$stringkey = 'editor:'.strtolower(trim($key));
$value = autoloader::get_h5p_string($stringkey, $language);
if (!empty($value)) {
$editorstrings[$key] = $value;
}
}
return $editorstrings;
}
/**
* Preprocess the data sent through the form to the H5P JS Editor Library.
*
* @return stdClass
*/
private function data_preprocessing(): stdClass {
$defaultvalues = [
'id' => $this->id,
'h5plibrary' => $this->library,
];
// In case both contentid and library have values, content(edition) takes precedence over library(creation).
if (empty($this->oldcontent)) {
$maincontentdata = ['params' => (object)[]];
} else {
$params = $this->core->filterParameters($this->oldcontent);
$maincontentdata = ['params' => json_decode($params)];
if (isset($this->oldcontent['metadata'])) {
$maincontentdata['metadata'] = $this->oldcontent['metadata'];
}
}
$defaultvalues['h5pparams'] = json_encode($maincontentdata, true);
return (object) $defaultvalues;
}
}
+221
View File
@@ -0,0 +1,221 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Class \core_h5p\editor_ajax
*
* @package core_h5p
* @copyright 2020 Victor Deniz <victor@moodle.com>, base on code by Joubel AS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_h5p;
use Moodle\H5PEditorAjaxInterface;
use core\dml\table as dml_table;
/**
* Moodle's implementation of the H5P Editor Ajax interface.
*
* Makes it possible for the editor's core ajax functionality to communicate with the
* database used by Moodle.
*
* @package core_h5p
* @copyright 2020 Victor Deniz <victor@moodle.com>, base on code by Joubel AS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class editor_ajax implements H5PEditorAjaxInterface {
/** The component for H5P. */
public const EDITOR_AJAX_TOKEN = 'editorajax';
/**
* Gets latest library versions that exists locally
*
* @return array Latest version of all local libraries
*/
public function getLatestLibraryVersions(): array {
global $DB;
$sql = "SELECT hl2.id, hl2.machinename as machine_name, hl2.title, hl2.majorversion as major_version,
hl2.minorversion AS minor_version, hl2.patchversion as patch_version, '' as has_icon, 0 as restricted,
hl2.enabled
FROM {h5p_libraries} hl2
LEFT JOIN {h5p_libraries} hl1
ON hl1.machinename = hl2.machinename
AND (hl2.majorversion < hl1.majorversion
OR (hl2.majorversion = hl1.majorversion
AND hl2.minorversion < hl1.minorversion)
)
WHERE hl2.runnable = 1
AND hl1.majorversion is null
ORDER BY hl2.title";
return $DB->get_records_sql($sql);
}
/**
* Get locally stored Content Type Cache.
*
* If machine name is provided it will only get the given content type from the cache.
*
* @param null|string $machinename
*
* @return mixed|null Returns results from querying the database
*/
public function getContentTypeCache($machinename = null) {
global $DB;
// Added some extra fields to the result because they are expected by functions calling this. They have been
// taken from method getCachedLibsMap() in h5peditor.class.php.
$sql = "SELECT l.id, l.machinename AS machine_name, l.majorversion AS major_version,
l.minorversion AS minor_version, l.patchversion AS patch_version, l.coremajor AS h5p_major_version,
l.coreminor AS h5p_minor_version, l.title, l.tutorial, l.example,
'' AS summary, '' AS description, '' AS icon, 0 AS created_at, 0 AS updated_at, 0 AS is_recommended,
0 AS popularity, '' AS screenshots, '' AS license, '' AS owner
FROM {h5p_libraries} l";
$params = [];
if (!empty($machinename)) {
$sql .= ' WHERE l.machinename = :machine_name';
$params = ['machine_name' => $machinename];
}
return $DB->get_records_sql($sql, $params);
}
/**
* Gets recently used libraries for the current author
*
* @return array machine names. The first element in the array is the
* most recently used.
*/
public function getAuthorsRecentlyUsedLibraries(): array {
// This is to be implemented when the Hub client is used.
return [];
}
/**
* Checks if the provided token is valid for this endpoint.
*
* @param string $token The token that will be validated for.
*
* @return bool True if successful validation
*/
public function validateEditorToken($token): bool {
return core::validToken(self::EDITOR_AJAX_TOKEN, $token);
}
/**
* Get translations in one language for a list of libraries.
*
* @param array $libraries An array of libraries, in the form "<machineName> <majorVersion>.<minorVersion>
* @param string $languagecode Language code
*
* @return array Translations in $languagecode available for libraries $libraries
*/
public function getTranslations($libraries, $languagecode): array {
$translations = [];
$langcache = \cache::make('core', 'h5p_content_type_translations');
$missing = [];
foreach ($libraries as $libstring) {
// Check if this library has been saved previously into the cache.
$librarykey = helper::get_cache_librarykey($libstring);
$cachekey = "{$librarykey}/{$languagecode}";
$libtranslation = $langcache->get($cachekey);
if ($libtranslation) {
// The library has this language stored into the cache.
$translations[$libstring] = $libtranslation;
} else {
// This language for the library hasn't been stored previously into the cache, so we need to get it from DB.
$missing[] = $libstring;
}
}
// Get all language files for libraries which aren't stored into the cache and merge them with the cache ones.
return array_merge(
$translations,
$this->get_missing_translations($missing, $languagecode)
);
}
/**
* Get translation for $language for libraries in $missing.
*
* @param array $missing An array of libraries, in the form "<machineName> <majorVersion>.<minorVersion>
* @param string $language Language code
* @return array Translations in $language available for libraries $missing
*/
protected function get_missing_translations(array $missing, string $language): array {
global $DB;
if (empty($missing)) {
return [];
}
$wheres = [];
$params = [
file_storage::COMPONENT,
file_storage::LIBRARY_FILEAREA,
];
$sqllike = $DB->sql_like('f.filepath', '?');
$params[] = '%language%';
foreach ($missing as $library) {
$librarydata = core::libraryFromString($library);
$wheres[] = '(h.machinename = ? AND h.majorversion = ? AND h.minorversion = ?)';
$params[] = $librarydata['machineName'];
$params[] = $librarydata['majorVersion'];
$params[] = $librarydata['minorVersion'];
}
$params[] = "{$language}.json";
$wheresql = implode(' OR ', $wheres);
$filestable = new dml_table('files', 'f', 'f_');
$filestableselect = $filestable->get_field_select();
$libtable = new dml_table('h5p_libraries', 'h', 'h_');
$libtableselect = $libtable->get_field_select();
$sql = "SELECT {$filestableselect}, {$libtableselect}
FROM {h5p_libraries} h
LEFT JOIN {files} f
ON h.id = f.itemid AND f.component = ?
AND f.filearea = ? AND $sqllike
WHERE ($wheresql) AND f.filename = ?";
// Get the content of all these language files and put them into the translations array.
$langcache = \cache::make('core', 'h5p_content_type_translations');
$fs = get_file_storage();
$translations = [];
$results = $DB->get_recordset_sql($sql, $params);
$toset = [];
foreach ($results as $result) {
$file = $fs->get_file_instance($filestable->extract_from_result($result));
$library = $libtable->extract_from_result($result);
$libstring = core::record_to_string($library);
$librarykey = helper::get_cache_librarykey($libstring);
$translations[$libstring] = $file->get_content();
$cachekey = "{$librarykey}/{$language}";
$toset[$cachekey] = $translations[$libstring];
}
$langcache->set_many($toset);
$results->close();
return $translations;
}
}
+357
View File
@@ -0,0 +1,357 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Class \core_h5p\editor_framework
*
* @package core_h5p
* @copyright 2020 Victor Deniz <victor@moodle.com>, base on code by Joubel AS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_h5p;
use Moodle\H5peditorStorage;
use stdClass;
/**
* Moodle's implementation of the H5P Editor storage interface.
*
* Makes it possible for the editor's core library to communicate with the
* database used by Moodle.
*
* @package core_h5p
* @copyright 2020 Victor Deniz <victor@moodle.com>, base on code by Joubel AS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class editor_framework implements H5peditorStorage {
/**
* Retrieve library language file from file storage. Note that parent languages will also be checked until a matching
* record is found (e.g. "de_kids" -> "de_du" -> "de")
*
* @param string $name
* @param int $major
* @param int $minor
* @param string $lang
* @return stdClass|bool Translation record if available, false otherwise
*/
private function get_language_record(string $name, int $major, int $minor, string $lang) {
global $DB;
$params = [
file_storage::COMPONENT,
file_storage::LIBRARY_FILEAREA,
];
$sqllike = $DB->sql_like('f.filepath', '?');
$params[] = '%language%';
$sql = "SELECT hl.id, f.pathnamehash
FROM {h5p_libraries} hl
LEFT JOIN {files} f
ON hl.id = f.itemid AND f.component = ? AND f.filearea = ? AND $sqllike
WHERE ((hl.machinename = ? AND hl.majorversion = ? AND hl.minorversion = ?)
AND f.filename = ?)
ORDER BY hl.patchversion DESC";
$params[] = $name;
$params[] = $major;
$params[] = $minor;
$params[] = $lang.'.json';
// Add translations, based initially on the given H5P language code. If missing then recurse language dependencies
// until we find a matching H5P language file.
$result = $DB->get_record_sql($sql, $params);
if ($result === false) {
// Normalise Moodle language using underscore, as opposed to H5P which uses dash.
$moodlelanguage = str_replace('-', '_', $lang);
$dependencies = get_string_manager()->get_language_dependencies($moodlelanguage);
// If current language has a dependency, then request it.
if (count($dependencies) > 1) {
$parentlanguage = get_html_lang_attribute_value($dependencies[count($dependencies) - 2]);
$result = $this->get_language_record($name, $major, $minor, $parentlanguage);
}
}
return $result;
}
/**
* Load language file(JSON).
* Used to translate the editor fields(title, description etc.)
*
* @param string $name The machine readable name of the library(content type)
* @param int $major Major part of version number
* @param int $minor Minor part of version number
* @param string $lang Language code
*
* @return string|boolean Translation in JSON format if available, false otherwise
*/
public function getLanguage($name, $major, $minor, $lang) {
// Check if this information has been saved previously into the cache.
$langcache = \cache::make('core', 'h5p_content_type_translations');
$library = new stdClass();
$library->machinename = $name;
$library->majorversion = $major;
$library->minorversion = $minor;
$librarykey = helper::get_cache_librarykey(core::record_to_string($library));
$cachekey = "{$librarykey}/{$lang}";
$translation = $langcache->get($cachekey);
if ($translation !== false) {
// When there is no translation we store it in the cache as `null`.
// This API requires it be returned as `false`.
if ($translation === null) {
return false;
}
return $translation;
}
// Get the language file for this library.
$result = $this->get_language_record($name, $major, $minor, $lang);
if (empty($result)) {
// Save the fact that there is no translation into the cache.
// The cache API cannot handle setting a literal `false` value so conver to `null` instead.
$langcache->set($cachekey, null);
return false;
}
// Save translation into the cache, and return its content.
$fs = get_file_storage();
$file = $fs->get_file_by_hash($result->pathnamehash);
$translation = $file->get_content();
$langcache->set($cachekey, $translation);
return $translation;
}
/**
* Load a list of available language codes.
*
* Until translations is implemented, only returns the "en" language.
*
* @param string $machinename The machine readable name of the library(content type)
* @param int $major Major part of version number
* @param int $minor Minor part of version number
*
* @return array List of possible language codes
*/
public function getAvailableLanguages($machinename, $major, $minor): array {
global $DB;
// Check if this information has been saved previously into the cache.
$langcache = \cache::make('core', 'h5p_content_type_translations');
$library = new stdClass();
$library->machinename = $machinename;
$library->majorversion = $major;
$library->minorversion = $minor;
$librarykey = helper::get_cache_librarykey(core::record_to_string($library));
$languages = $langcache->get($librarykey);
if ($languages) {
// This contains a list of all of the available languages for the library.
return $languages;
}
// Get the language files for this library.
$params = [
file_storage::COMPONENT,
file_storage::LIBRARY_FILEAREA,
];
$filepathsqllike = $DB->sql_like('f.filepath', '?');
$params[] = '%language%';
$filenamesqllike = $DB->sql_like('f.filename', '?');
$params[] = '%.json';
$sql = "SELECT DISTINCT f.filename
FROM {h5p_libraries} hl
LEFT JOIN {files} f
ON hl.id = f.itemid AND f.component = ? AND f.filearea = ?
AND $filepathsqllike AND $filenamesqllike
WHERE hl.machinename = ? AND hl.majorversion = ? AND hl.minorversion = ?";
$params[] = $machinename;
$params[] = $major;
$params[] = $minor;
$defaultcode = 'en';
$languages = [];
$results = $DB->get_recordset_sql($sql, $params);
if ($results->valid()) {
// Extract the code language from the JS language files.
foreach ($results as $result) {
if (!empty($result->filename)) {
$lang = substr($result->filename, 0, -5);
$languages[$lang] = $languages;
}
}
$results->close();
// Semantics is 'en' by default. It has to be added always.
if (!array_key_exists($defaultcode, $languages)) {
$languages = array_keys($languages);
array_unshift($languages, $defaultcode);
}
} else {
$results->close();
$params = [
'machinename' => $machinename,
'majorversion' => $major,
'minorversion' => $minor,
];
if ($DB->record_exists('h5p_libraries', $params)) {
// If the library exists (but it doesn't contain any language file), at least defaultcode should be returned.
$languages[] = $defaultcode;
}
}
// Save available languages into the cache.
$langcache->set($librarykey, $languages);
return $languages;
}
/**
* "Callback" for mark the given file as a permanent file.
*
* Used when saving content that has new uploaded files.
*
* @param int $fileid
*/
public function keepFile($fileid): void {
// Temporal files will be removed on a task when they are in the "editor" file area and and are at least one day older.
}
/**
* Return libraries details.
*
* Two use cases:
* 1. No input, will list all the available content types.
* 2. Libraries supported are specified, load additional data and verify
* that the content types are available. Used by e.g. the Presentation Tool
* Editor that already knows which content types are supported in its
* slides.
*
* @param array $libraries List of library names + version to load info for.
*
* @return array List of all libraries loaded.
*/
public function getLibraries($libraries = null): ?array {
if ($libraries !== null) {
// Get details for the specified libraries.
$librariesin = [];
$fields = 'title, runnable, metadatasettings, example, tutorial';
foreach ($libraries as $library) {
$params = [
'machinename' => $library->name,
'majorversion' => $library->majorVersion,
'minorversion' => $library->minorVersion
];
$details = api::get_library_details($params, true, $fields);
if ($details) {
$library->title = $details->title;
$library->runnable = $details->runnable;
$library->metadataSettings = json_decode($details->metadatasettings ?? '');
$library->example = $details->example;
$library->tutorial = $details->tutorial;
$librariesin[] = $library;
}
}
} else {
$fields = 'id, machinename as name, title, majorversion, minorversion, metadatasettings, example, tutorial';
$librariesin = api::get_contenttype_libraries($fields);
}
return $librariesin;
}
/**
* Allow for other plugins to decide which styles and scripts are attached.
*
* This is useful for adding and/or modifying the functionality and look of
* the content types.
*
* @param array $files List of files as objects with path and version as properties.
* @param array $libraries List of libraries indexed by machineName with objects as values. The objects have majorVersion and
* minorVersion as properties.
*/
public function alterLibraryFiles(&$files, $libraries): void {
global $PAGE;
// Refactor dependency list.
$librarylist = [];
foreach ($libraries as $dependency) {
$librarylist[$dependency['machineName']] = [
'majorVersion' => $dependency['majorVersion'],
'minorVersion' => $dependency['minorVersion']
];
}
$renderer = $PAGE->get_renderer('core_h5p');
$embedtype = 'editor';
$renderer->h5p_alter_scripts($files['scripts'], $librarylist, $embedtype);
$renderer->h5p_alter_styles($files['styles'], $librarylist, $embedtype);
}
/**
* Saves a file or moves it temporarily.
*
* This is often necessary in order to validate and store uploaded or fetched H5Ps.
*
* @param string $data Uri of data that should be saved as a temporary file.
* @param bool $movefile Can be set to TRUE to move the data instead of saving it.
*
* @return bool|object Returns false if saving failed or an object with path
* of the directory and file that is temporarily saved.
*/
public static function saveFileTemporarily($data, $movefile = false) {
// This is to be implemented when the Hub client is used to upload libraries.
return false;
}
/**
* Marks a file for later cleanup.
*
* Useful when files are not instantly cleaned up. E.g. for files that are uploaded through the editor.
*
* @param int $file Id of file that should be cleaned up
* @param int|null $contentid Content id of file
*/
public static function markFileForCleanup($file, $contentid = null): ?int {
// Temporal files will be removed on a task when they are in the "editor" file area and and are at least one day older.
return null;
}
/**
* Clean up temporary files
*
* @param string $filepath Path to file or directory
*/
public static function removeTemporarilySavedFiles($filepath): void {
// This is to be implemented when the Hub client is used to upload libraries.
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* H5P deleted event class.
*
* @package core
* @since Moodle 3.8
* @copyright 2019 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_h5p\event;
defined('MOODLE_INTERNAL') || die();
/**
* H5P viewed event class.
*
* @package core_h5p
* @since Moodle 3.8
* @copyright 2019 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class h5p_deleted extends \core\event\base {
/**
* Initialise event parameters.
*/
protected function init() {
$this->data['objecttable'] = 'h5p';
$this->data['crud'] = 'd';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
}
/**
* Returns localised event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventh5pdeleted', 'h5p');
}
/**
* Returns non-localised event description with id's for admin use only.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' deleted the H5P with id '$this->objectid'.";
}
/**
* Custom validations.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->objectid)) {
throw new \coding_exception('The \'objectid\' must be set.');
}
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
// There is no url since the previous event already has the url where the h5p content has been displayed.
return null;
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* H5P viewed event class.
*
* @package core
* @since Moodle 3.8
* @copyright 2019 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_h5p\event;
defined('MOODLE_INTERNAL') || die();
/**
* H5P viewed event class.
*
* @package core_h5p
* @since Moodle 3.8
* @copyright 2019 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class h5p_viewed extends \core\event\base {
/**
* Initialise event parameters.
*/
protected function init() {
$this->data['objecttable'] = 'h5p';
$this->data['crud'] = 'r';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
}
/**
* Returns localised event name.
*
* @return string
*/
public static function get_name() {
return get_string('eventh5pviewed', 'h5p');
}
/**
* Returns non-localised event description with id's for admin use only.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' has viewed the H5P with the id '$this->objectid'.";
}
/**
* Custom validations.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (!isset($this->objectid)) {
throw new \coding_exception('The \'objectid\' must be set.');
}
}
/**
* Returns relevant URL.
*
* @return \moodle_url
*/
public function get_url() {
// There is no url since the previous event already has the url where the h5p content has been displayed.
return null;
}
}
+153
View File
@@ -0,0 +1,153 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_h5p;
use core_external\external_api;
use core_external\external_files;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use core_external\external_warnings;
/**
* This is the external API for this component.
*
* @package core_h5p
* @copyright 2019 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class external extends external_api {
/**
* get_trusted_h5p_file parameters.
*
* @since Moodle 3.8
* @return external_function_parameters
*/
public static function get_trusted_h5p_file_parameters() {
return new external_function_parameters(
[
'url' => new external_value(PARAM_URL, 'H5P file url.', VALUE_REQUIRED),
'frame' => new external_value(PARAM_INT, 'The frame allow to show the bar options below the content', VALUE_DEFAULT, 0),
'export' => new external_value(PARAM_INT, 'The export allow to download the package', VALUE_DEFAULT, 0),
'embed' => new external_value(PARAM_INT, 'The embed allow to copy the code to your site', VALUE_DEFAULT, 0),
'copyright' => new external_value(PARAM_INT, 'The copyright option', VALUE_DEFAULT, 0)
]
);
}
/**
* Return the H5P file trusted.
*
* The Mobile App needs to work with an H5P package which can trust.
* And this H5P package is only trusted by the Mobile App once it's been processed
* by the core checking the right caps, validating the H5P package
* and doing any clean-up process involved.
*
* @since Moodle 3.8
* @param string $url H5P file url
* @param int $frame The frame allow to show the bar options below the content
* @param int $export The export allow to download the package
* @param int $embed The embed allow to copy the code to your site
* @param int $copyright The copyright option
* @return array
* @throws \moodle_exception
*/
public static function get_trusted_h5p_file(string $url, int $frame, int $export, int $embed, int $copyright) {
$result = [];
$warnings = [];
$params = external_api::validate_parameters(self::get_trusted_h5p_file_parameters(), [
'url' => $url,
'frame' => $frame,
'export' => $export,
'embed' => $embed,
'copyright' => $copyright
]);
$url = $params['url'];
$config = new \stdClass();
$config->frame = $params['frame'];
$config->export = $params['export'];
$config->embed = $params['embed'];
$config->copyright = $params['copyright'];
try {
$h5pplayer = new player($url, $config);
$messages = $h5pplayer->get_messages();
} catch (\moodle_exception $e) {
$messages = (object) [
'code' => $e->getCode(),
];
// To mantain the coherence between web coding error and Mobile coding errors.
// We need to return the same message error to Mobile.
// The 'out_al_local_url called on a non-local URL' error is provided by the \moodle_exception.
// We have to translate to h5pinvalidurl which is the same coding error showed in web.
if ($e->errorcode === 'codingerror' &&
$e->a === 'out_as_local_url called on a non-local URL') {
$messages->exception = get_string('h5pinvalidurl', 'core_h5p');
} else {
$messages->exception = $e->getMessage();
}
}
if (empty($messages->error) && empty($messages->exception)) {
// Add H5P assets to the page.
$h5pplayer->add_assets_to_page();
// Check if there is some error when adding assets to the page.
$messages = $h5pplayer->get_messages();
if (empty($messages->error)) {
$fileh5p = $h5pplayer->get_export_file();
$result[] = $fileh5p;
}
}
if (!empty($messages->error)) {
foreach ($messages->error as $error) {
// We have to apply clean_param because warningcode is a PARAM_ALPHANUM.
// And H5P has some error code like 'total-size-too-large'.
$warnings[] = [
'item' => $url,
'warningcode' => clean_param($error->code, PARAM_ALPHANUM),
'message' => $error->message
];
}
} else if (!empty($messages->exception)) {
$warnings[] = [
'item' => $url,
'warningcode' => $messages->code,
'message' => $messages->exception
];
}
return [
'files' => $result,
'warnings' => $warnings
];
}
/**
* get_trusted_h5p_file return
*
* @since Moodle 3.8
* @return \core_external\external_description
*/
public static function get_trusted_h5p_file_returns() {
return new external_single_structure(
[
'files' => new external_files('H5P file trusted.'),
'warnings' => new external_warnings()
]
);
}
}
+185
View File
@@ -0,0 +1,185 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* H5P factory class.
* This class is used to decouple the construction of H5P related objects.
*
* @package core_h5p
* @copyright 2019 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_h5p;
defined('MOODLE_INTERNAL') || die();
use core_h5p\local\library\autoloader;
use Moodle\H5PContentValidator as content_validator;
use Moodle\H5peditor;
use Moodle\H5PStorage as storage;
use Moodle\H5PValidator as validator;
/**
* H5P factory class.
* This class is used to decouple the construction of H5P related objects.
*
* @package core_h5p
* @copyright 2019 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class factory {
/** @var \core_h5p\local\library\autoloader The autoloader */
protected $autoloader;
/** @var \core_h5p\core The Moodle H5PCore implementation */
protected $core;
/** @var \core_h5p\framework The Moodle H5PFramework implementation */
protected $framework;
/** @var \core_h5p\file_storage The Moodle H5PStorage implementation */
protected $storage;
/** @var validator The Moodle H5PValidator implementation */
protected $validator;
/** @var content_validator The Moodle H5PContentValidator implementation */
protected $content_validator;
/** @var editor_framework The Moodle H5peditorStorage implementation */
protected $editorframework;
/** @var H5peditor */
protected $editor;
/** @var editor_ajax The Moodle H5PEditorAjaxInterface implementation */
protected $editorajaxinterface;
/**
* factory constructor.
*/
public function __construct() {
// Loading classes we need from H5P third party library.
$this->autoloader = new autoloader();
autoloader::register();
}
/**
* Returns an instance of the \core_h5p\local\library\autoloader class.
*
* @return \core_h5p\local\library\autoloader
*/
public function get_autoloader(): autoloader {
return $this->autoloader;
}
/**
* Returns an instance of the \core_h5p\framework class.
*
* @return \core_h5p\framework
*/
public function get_framework(): framework {
if (null === $this->framework) {
$this->framework = new framework();
}
return $this->framework;
}
/**
* Returns an instance of the \core_h5p\core class.
*
* @return \core_h5p\core
*/
public function get_core(): core {
if (null === $this->core) {
$fs = new \core_h5p\file_storage();
$language = \core_h5p\framework::get_language();
$context = \context_system::instance();
$url = \moodle_url::make_pluginfile_url($context->id, 'core_h5p', '', null,
'', '')->out();
$this->core = new core($this->get_framework(), $fs, $url, $language, true);
}
return $this->core;
}
/**
* Returns an instance of the H5PStorage class.
*
* @return \Moodle\H5PStorage
*/
public function get_storage(): storage {
if (null === $this->storage) {
$this->storage = new storage($this->get_framework(), $this->get_core());
}
return $this->storage;
}
/**
* Returns an instance of the H5PValidator class.
*
* @return \Moodle\H5PValidator
*/
public function get_validator(): validator {
if (null === $this->validator) {
$this->validator = new validator($this->get_framework(), $this->get_core());
}
return $this->validator;
}
/**
* Returns an instance of the H5PContentValidator class.
*
* @return Moodle\H5PContentValidator
*/
public function get_content_validator(): content_validator {
if (null === $this->content_validator) {
$this->content_validator = new content_validator($this->get_framework(), $this->get_core());
}
return $this->content_validator;
}
/**
* Returns an instance of H5Peditor class.
*
* @return H5peditor
*/
public function get_editor(): H5peditor {
if (null === $this->editor) {
if (empty($this->editorframework)) {
$this->editorframework = new editor_framework();
}
if (empty($this->editorajaxinterface)) {
$this->editorajaxinterface = new editor_ajax();
}
if (empty($this->editor)) {
$this->editor = new H5peditor($this->get_core(), $this->editorframework, $this->editorajaxinterface);
}
}
return $this->editor;
}
}
+984
View File
@@ -0,0 +1,984 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Class \core_h5p\file_storage.
*
* @package core_h5p
* @copyright 2019 Victor Deniz <victor@moodle.com>, base on code by Joubel AS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_h5p;
use stored_file;
use Moodle\H5PCore;
use Moodle\H5peditorFile;
use Moodle\H5PFileStorage;
// phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
/**
* Class to handle storage and export of H5P Content.
*
* @package core_h5p
* @copyright 2019 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class file_storage implements H5PFileStorage {
/** The component for H5P. */
public const COMPONENT = 'core_h5p';
/** The library file area. */
public const LIBRARY_FILEAREA = 'libraries';
/** The content file area */
public const CONTENT_FILEAREA = 'content';
/** The cached assest file area. */
public const CACHED_ASSETS_FILEAREA = 'cachedassets';
/** The export file area */
public const EXPORT_FILEAREA = 'export';
/** The export css file area */
public const CSS_FILEAREA = 'css';
/** The icon filename */
public const ICON_FILENAME = 'icon.svg';
/** The custom CSS filename */
private const CUSTOM_CSS_FILENAME = 'custom_h5p.css';
/**
* @var \context $context Currently we use the system context everywhere.
* Don't feel forced to keep it this way in the future.
*/
protected $context;
/** @var \file_storage $fs File storage. */
protected $fs;
/**
* Initial setup for file_storage.
*/
public function __construct() {
// Currently everything uses the system context.
$this->context = \context_system::instance();
$this->fs = get_file_storage();
}
/**
* Stores a H5P library in the Moodle filesystem.
*
* @param array $library Library properties.
*/
public function saveLibrary($library) {
$options = [
'contextid' => $this->context->id,
'component' => self::COMPONENT,
'filearea' => self::LIBRARY_FILEAREA,
'filepath' => '/' . H5PCore::libraryToFolderName($library) . '/',
'itemid' => $library['libraryId'],
];
// Easiest approach: delete the existing library version and copy the new one.
$this->delete_library($library);
$this->copy_directory($library['uploadDirectory'], $options);
}
/**
* Delete library folder.
*
* @param array $library
*/
public function deleteLibrary($library) {
// Although this class had a method (delete_library()) for removing libraries before this was added to the interface,
// it's not safe to call it from here because looking at the place where it's called, it's not clear what are their
// expectation. This method will be implemented once more information will be added to the H5P technical doc.
}
/**
* Store the content folder.
*
* @param string $source Path on file system to content directory.
* @param array $content Content properties
*/
public function saveContent($source, $content) {
$options = [
'contextid' => $this->context->id,
'component' => self::COMPONENT,
'filearea' => self::CONTENT_FILEAREA,
'itemid' => $content['id'],
'filepath' => '/',
];
$this->delete_directory($this->context->id, self::COMPONENT, self::CONTENT_FILEAREA, $content['id']);
// Copy content directory into Moodle filesystem.
$this->copy_directory($source, $options);
}
/**
* Remove content folder.
*
* @param array $content Content properties
*/
public function deleteContent($content) {
$this->delete_directory($this->context->id, self::COMPONENT, self::CONTENT_FILEAREA, $content['id']);
}
/**
* Creates a stored copy of the content folder.
*
* @param string $id Identifier of content to clone.
* @param int $newid The cloned content's identifier
*/
public function cloneContent($id, $newid) {
// Not implemented in Moodle.
}
/**
* Get path to a new unique tmp folder.
* Please note this needs to not be a directory.
*
* @return string Path
*/
public function getTmpPath(): string {
return make_request_directory() . '/' . uniqid('h5p-');
}
/**
* Fetch content folder and save in target directory.
*
* @param int $id Content identifier
* @param string $target Where the content folder will be saved
*/
public function exportContent($id, $target) {
$this->export_file_tree($target, $this->context->id, self::CONTENT_FILEAREA, '/', $id);
}
/**
* Fetch library folder and save in target directory.
*
* @param array $library Library properties
* @param string $target Where the library folder will be saved
*/
public function exportLibrary($library, $target) {
$folder = H5PCore::libraryToFolderName($library);
$this->export_file_tree($target . '/' . $folder, $this->context->id, self::LIBRARY_FILEAREA,
'/' . $folder . '/', $library['libraryId']);
}
/**
* Save export in file system
*
* @param string $source Path on file system to temporary export file.
* @param string $filename Name of export file.
*/
public function saveExport($source, $filename) {
global $USER;
// Remove old export.
$this->deleteExport($filename);
$filerecord = [
'contextid' => $this->context->id,
'component' => self::COMPONENT,
'filearea' => self::EXPORT_FILEAREA,
'itemid' => 0,
'filepath' => '/',
'filename' => $filename,
'userid' => $USER->id
];
$this->fs->create_file_from_pathname($filerecord, $source);
}
/**
* Removes given export file
*
* @param string $filename filename of the export to delete.
*/
public function deleteExport($filename) {
$file = $this->get_export_file($filename);
if ($file) {
$file->delete();
}
}
/**
* Check if the given export file exists
*
* @param string $filename The export file to check.
* @return boolean True if the export file exists.
*/
public function hasExport($filename) {
return !!$this->get_export_file($filename);
}
/**
* Will concatenate all JavaScrips and Stylesheets into two files in order
* to improve page performance.
*
* @param array $files A set of all the assets required for content to display
* @param string $key Hashed key for cached asset
*/
public function cacheAssets(&$files, $key) {
foreach ($files as $type => $assets) {
if (empty($assets)) {
continue;
}
// Create new file for cached assets.
$ext = ($type === 'scripts' ? 'js' : 'css');
$filename = $key . '.' . $ext;
$fileinfo = [
'contextid' => $this->context->id,
'component' => self::COMPONENT,
'filearea' => self::CACHED_ASSETS_FILEAREA,
'itemid' => 0,
'filepath' => '/',
'filename' => $filename
];
// Store concatenated content.
$this->fs->create_file_from_string($fileinfo, $this->concatenate_files($assets, $type, $this->context));
$files[$type] = [
(object) [
'path' => '/' . self::CACHED_ASSETS_FILEAREA . '/' . $filename,
'version' => ''
]
];
}
}
/**
* Will check if there are cache assets available for content.
*
* @param string $key Hashed key for cached asset
* @return array
*/
public function getCachedAssets($key) {
$files = [];
$js = $this->fs->get_file($this->context->id, self::COMPONENT, self::CACHED_ASSETS_FILEAREA, 0, '/', "{$key}.js");
if ($js && $js->get_filesize() > 0) {
$files['scripts'] = [
(object) [
'path' => '/' . self::CACHED_ASSETS_FILEAREA . '/' . "{$key}.js",
'version' => ''
]
];
}
$css = $this->fs->get_file($this->context->id, self::COMPONENT, self::CACHED_ASSETS_FILEAREA, 0, '/', "{$key}.css");
if ($css && $css->get_filesize() > 0) {
$files['styles'] = [
(object) [
'path' => '/' . self::CACHED_ASSETS_FILEAREA . '/' . "{$key}.css",
'version' => ''
]
];
}
return empty($files) ? null : $files;
}
/**
* Remove the aggregated cache files.
*
* @param array $keys The hash keys of removed files
*/
public function deleteCachedAssets($keys) {
if (empty($keys)) {
return;
}
foreach ($keys as $hash) {
foreach (['js', 'css'] as $type) {
$cachedasset = $this->fs->get_file($this->context->id, self::COMPONENT, self::CACHED_ASSETS_FILEAREA, 0, '/',
"{$hash}.{$type}");
if ($cachedasset) {
$cachedasset->delete();
}
}
}
}
/**
* Read file content of given file and then return it.
*
* @param string $filepath
* @return string contents
*/
public function getContent($filepath) {
list(
'filearea' => $filearea,
'filepath' => $filepath,
'filename' => $filename,
'itemid' => $itemid
) = $this->get_file_elements_from_filepath($filepath);
if (!$itemid) {
throw new \file_serving_exception('Could not retrieve the requested file, check your file permissions.');
}
// Locate file.
$file = $this->fs->get_file($this->context->id, self::COMPONENT, $filearea, $itemid, $filepath, $filename);
// Return content.
return $file->get_content();
}
/**
* Save files uploaded through the editor.
*
* @param H5peditorFile $file
* @param int $contentid
*
* @return int The id of the saved file.
*/
public function saveFile($file, $contentid) {
global $USER;
$context = $this->context->id;
$component = self::COMPONENT;
$filearea = self::CONTENT_FILEAREA;
if ($contentid === 0) {
$usercontext = \context_user::instance($USER->id);
$context = $usercontext->id;
$component = 'user';
$filearea = 'draft';
}
$record = array(
'contextid' => $context,
'component' => $component,
'filearea' => $filearea,
'itemid' => $contentid,
'filepath' => '/' . $file->getType() . 's/',
'filename' => $file->getName()
);
$storedfile = $this->fs->create_file_from_pathname($record, $_FILES['file']['tmp_name']);
return $storedfile->get_id();
}
/**
* Copy a file from another content or editor tmp dir.
* Used when copy pasting content in H5P.
*
* @param string $file path + name
* @param string|int $fromid Content ID or 'editor' string
* @param \stdClass $tocontent Target Content
*
* @return void
*/
public function cloneContentFile($file, $fromid, $tocontent): void {
// Determine source filearea and itemid.
if ($fromid === 'editor') {
$sourcefilearea = 'draft';
$sourceitemid = 0;
} else {
$sourcefilearea = self::CONTENT_FILEAREA;
$sourceitemid = (int)$fromid;
}
$filepath = '/' . dirname($file) . '/';
$filename = basename($file);
// Check to see if source exists.
$sourcefile = $this->get_file($sourcefilearea, $sourceitemid, $file);
if ($sourcefile === null) {
return; // Nothing to copy from.
}
// Check to make sure that file doesn't exist already in target.
$targetfile = $this->get_file(self::CONTENT_FILEAREA, $tocontent->id, $file);
if ( $targetfile !== null) {
return; // File exists, no need to copy.
}
// Create new file record.
$record = [
'contextid' => $this->context->id,
'component' => self::COMPONENT,
'filearea' => self::CONTENT_FILEAREA,
'itemid' => $tocontent->id,
'filepath' => $filepath,
'filename' => $filename,
];
$this->fs->create_file_from_storedfile($record, $sourcefile);
}
/**
* Copy content from one directory to another.
* Defaults to cloning content from the current temporary upload folder to the editor path.
*
* @param string $source path to source directory
* @param string $contentid Id of content
*
*/
public function moveContentDirectory($source, $contentid = null) {
$contentidint = (int)$contentid;
if ($source === null) {
return;
}
// Get H5P and content json.
$contentsource = $source . '/content';
// Move all temporary content files to editor.
$it = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($contentsource, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST
);
$it->rewind();
while ($it->valid()) {
$item = $it->current();
$pathname = $it->getPathname();
if (!$item->isDir() && !($item->getFilename() === 'content.json')) {
$this->move_file($pathname, $contentidint);
}
$it->next();
}
}
/**
* Get the file URL or given library and then return it.
*
* @param int $itemid
* @param string $machinename
* @param int $majorversion
* @param int $minorversion
* @return string url or false if the file doesn't exist
*/
public function get_icon_url(int $itemid, string $machinename, int $majorversion, int $minorversion) {
$filepath = '/' . "{$machinename}-{$majorversion}.{$minorversion}" . '/';
if ($file = $this->fs->get_file(
$this->context->id,
self::COMPONENT,
self::LIBRARY_FILEAREA,
$itemid,
$filepath,
self::ICON_FILENAME)
) {
$iconurl = \moodle_url::make_pluginfile_url(
$this->context->id,
self::COMPONENT,
self::LIBRARY_FILEAREA,
$itemid,
$filepath,
$file->get_filename());
// Return image URL.
return $iconurl->out();
}
return false;
}
/**
* Checks to see if an H5P content has the given file.
*
* @param string $file File path and name.
* @param int $content Content id.
*
* @return int|null File ID or NULL if not found
*/
public function getContentFile($file, $content): ?int {
if (is_object($content)) {
$content = $content->id;
}
$contentfile = $this->get_file(self::CONTENT_FILEAREA, $content, $file);
return ($contentfile === null ? null : $contentfile->get_id());
}
/**
* Remove content files that are no longer used.
*
* Used when saving content.
*
* @param string $file File path and name.
* @param int $contentid Content id.
*
* @return void
*/
public function removeContentFile($file, $contentid): void {
// Although the interface defines $contentid as int, object given in H5peditor::processParameters.
if (is_object($contentid)) {
$contentid = $contentid->id;
}
$existingfile = $this->get_file(self::CONTENT_FILEAREA, $contentid, $file);
if ($existingfile !== null) {
$existingfile->delete();
}
}
/**
* Check if server setup has write permission to
* the required folders
*
* @return bool True if server has the proper write access
*/
public function hasWriteAccess() {
// Moodle has access to the files table which is where all of the folders are stored.
return true;
}
/**
* Check if the library has a presave.js in the root folder
*
* @param string $libraryname
* @param string $developmentpath
* @return bool
*/
public function hasPresave($libraryname, $developmentpath = null) {
return false;
}
/**
* Check if upgrades script exist for library.
*
* @param string $machinename
* @param int $majorversion
* @param int $minorversion
* @return string Relative path
*/
public function getUpgradeScript($machinename, $majorversion, $minorversion) {
$path = '/' . "{$machinename}-{$majorversion}.{$minorversion}" . '/';
$file = 'upgrade.js';
$itemid = $this->get_itemid_for_file(self::LIBRARY_FILEAREA, $path, $file);
if ($this->fs->get_file($this->context->id, self::COMPONENT, self::LIBRARY_FILEAREA, $itemid, $path, $file)) {
return '/' . self::LIBRARY_FILEAREA . $path. $file;
} else {
return null;
}
}
/**
* Store the given stream into the given file.
*
* @param string $path
* @param string $file
* @param resource $stream
* @return bool|int
*/
public function saveFileFromZip($path, $file, $stream) {
$fullpath = $path . '/' . $file;
check_dir_exists(pathinfo($fullpath, PATHINFO_DIRNAME));
return file_put_contents($fullpath, $stream);
}
/**
* Deletes a library from the file system.
*
* @param array $library Library details
*/
public function delete_library(array $library): void {
global $DB;
// A library ID of false would result in all library files being deleted, which we don't want. Return instead.
if (empty($library['libraryId'])) {
return;
}
$areafiles = $this->fs->get_area_files($this->context->id, self::COMPONENT, self::LIBRARY_FILEAREA, $library['libraryId']);
$this->delete_directory($this->context->id, self::COMPONENT, self::LIBRARY_FILEAREA, $library['libraryId']);
$librarycache = \cache::make('core', 'h5p_library_files');
foreach ($areafiles as $file) {
if (!$DB->record_exists('files', array('contenthash' => $file->get_contenthash(),
'component' => self::COMPONENT,
'filearea' => self::LIBRARY_FILEAREA))) {
$librarycache->delete($file->get_contenthash());
}
}
}
/**
* Remove an H5P directory from the filesystem.
*
* @param int $contextid context ID
* @param string $component component
* @param string $filearea file area or all areas in context if not specified
* @param int $itemid item ID or all files if not specified
*/
private function delete_directory(int $contextid, string $component, string $filearea, int $itemid): void {
$this->fs->delete_area_files($contextid, $component, $filearea, $itemid);
}
/**
* Copy an H5P directory from the temporary directory into the file system.
*
* @param string $source Temporary location for files.
* @param array $options File system information.
*/
private function copy_directory(string $source, array $options): void {
$librarycache = \cache::make('core', 'h5p_library_files');
$it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST);
$root = $options['filepath'];
$it->rewind();
while ($it->valid()) {
$item = $it->current();
$subpath = $it->getSubPath();
if (!$item->isDir()) {
$options['filename'] = $it->getFilename();
if (!$subpath == '') {
$options['filepath'] = $root . $subpath . '/';
} else {
$options['filepath'] = $root;
}
$file = $this->fs->create_file_from_pathname($options, $item->getPathName());
if ($options['filearea'] == self::LIBRARY_FILEAREA) {
if (!$librarycache->has($file->get_contenthash())) {
$librarycache->set($file->get_contenthash(), file_get_contents($item->getPathName()));
}
}
}
$it->next();
}
}
/**
* Copies files from storage to temporary folder.
*
* @param string $target Path to temporary folder
* @param int $contextid context where the files are found
* @param string $filearea file area
* @param string $filepath file path
* @param int $itemid Optional item ID
*/
private function export_file_tree(string $target, int $contextid, string $filearea, string $filepath, int $itemid = 0): void {
// Make sure target folder exists.
check_dir_exists($target);
// Read source files.
$files = $this->fs->get_directory_files($contextid, self::COMPONENT, $filearea, $itemid, $filepath, true);
$librarycache = \cache::make('core', 'h5p_library_files');
foreach ($files as $file) {
$path = $target . str_replace($filepath, DIRECTORY_SEPARATOR, $file->get_filepath());
if ($file->is_directory()) {
check_dir_exists(rtrim($path));
} else {
if ($filearea == self::LIBRARY_FILEAREA) {
$cachedfile = $librarycache->get($file->get_contenthash());
if (empty($cachedfile)) {
$file->copy_content_to($path . $file->get_filename());
$librarycache->set($file->get_contenthash(), file_get_contents($path . $file->get_filename()));
} else {
file_put_contents($path . $file->get_filename(), $cachedfile);
}
} else {
$file->copy_content_to($path . $file->get_filename());
}
}
}
}
/**
* Adds all files of a type into one file.
*
* @param array $assets A list of files.
* @param string $type The type of files in assets. Either 'scripts' or 'styles'
* @param \context $context Context
* @return string All of the file content in one string.
*/
private function concatenate_files(array $assets, string $type, \context $context): string {
$content = '';
foreach ($assets as $asset) {
// Find location of asset.
list(
'filearea' => $filearea,
'filepath' => $filepath,
'filename' => $filename,
'itemid' => $itemid
) = $this->get_file_elements_from_filepath($asset->path);
if ($itemid === false) {
continue;
}
// Locate file.
$file = $this->fs->get_file($context->id, self::COMPONENT, $filearea, $itemid, $filepath, $filename);
// Get file content and concatenate.
if ($type === 'scripts') {
$content .= $file->get_content() . ";\n";
} else {
// Rewrite relative URLs used inside stylesheets.
$content .= preg_replace_callback(
'/url\([\'"]?([^"\')]+)[\'"]?\)/i',
function ($matches) use ($filearea, $filepath, $itemid) {
if (preg_match("/^(data:|([a-z0-9]+:)?\/)/i", $matches[1]) === 1) {
return $matches[0]; // Not relative, skip.
}
// Find "../" in matches[1].
// If it exists, we have to remove "../".
// And switch the last folder in the filepath for the first folder in $matches[1].
// For instance:
// $filepath: /H5P.Question-1.4/styles/
// $matches[1]: ../images/plus-one.svg
// We want to avoid this: H5P.Question-1.4/styles/ITEMID/../images/minus-one.svg
// We want this: H5P.Question-1.4/images/ITEMID/minus-one.svg.
if (preg_match('/\.\.\//', $matches[1], $pathmatches)) {
$path = preg_split('/\//', $filepath, -1, PREG_SPLIT_NO_EMPTY);
$pathfilename = preg_split('/\//', $matches[1], -1, PREG_SPLIT_NO_EMPTY);
// Remove the first element: ../.
array_shift($pathfilename);
// Replace pathfilename into the filepath.
$path[count($path) - 1] = $pathfilename[0];
$filepath = '/' . implode('/', $path) . '/';
// Remove the element used to replace.
array_shift($pathfilename);
$matches[1] = implode('/', $pathfilename);
}
return 'url("../' . $filearea . $filepath . $itemid . '/' . $matches[1] . '")';
},
$file->get_content()) . "\n";
}
}
return $content;
}
/**
* Get files ready for export.
*
* @param string $filename File name to retrieve.
* @return bool|\stored_file Stored file instance if exists, false if not
*/
public function get_export_file(string $filename) {
return $this->fs->get_file($this->context->id, self::COMPONENT, self::EXPORT_FILEAREA, 0, '/', $filename);
}
/**
* Converts a relative system file path into Moodle File API elements.
*
* @param string $filepath The system filepath to get information from.
* @return array File information.
*/
private function get_file_elements_from_filepath(string $filepath): array {
$sections = explode('/', $filepath);
// Get the filename.
$filename = array_pop($sections);
// Discard first element.
if (empty($sections[0])) {
array_shift($sections);
}
// Get the filearea.
$filearea = array_shift($sections);
$itemid = array_shift($sections);
// Get the filepath.
$filepath = implode('/', $sections);
$filepath = '/' . $filepath . '/';
return ['filearea' => $filearea, 'filepath' => $filepath, 'filename' => $filename, 'itemid' => $itemid];
}
/**
* Returns the item id given the other necessary variables.
*
* @param string $filearea The file area.
* @param string $filepath The file path.
* @param string $filename The file name.
* @return mixed the specified value false if not found.
*/
private function get_itemid_for_file(string $filearea, string $filepath, string $filename) {
global $DB;
return $DB->get_field('files', 'itemid', ['component' => self::COMPONENT, 'filearea' => $filearea, 'filepath' => $filepath,
'filename' => $filename]);
}
/**
* Helper to make it easy to load content files.
*
* @param string $filearea File area where the file is saved.
* @param int $itemid Content instance or content id.
* @param string $file File path and name.
*
* @return stored_file|null
*/
private function get_file(string $filearea, int $itemid, string $file): ?stored_file {
global $USER;
$component = self::COMPONENT;
$context = $this->context->id;
if ($filearea === 'draft') {
$itemid = 0;
$component = 'user';
$usercontext = \context_user::instance($USER->id);
$context = $usercontext->id;
}
$filepath = '/'. dirname($file). '/';
$filename = basename($file);
// Load file.
$existingfile = $this->fs->get_file($context, $component, $filearea, $itemid, $filepath, $filename);
if (!$existingfile) {
return null;
}
return $existingfile;
}
/**
* Move a single file
*
* @param string $sourcefile Path to source file
* @param int $contentid Content id or 0 if the file is in the editor file area
*
* @return void
*/
private function move_file(string $sourcefile, int $contentid): void {
$pathparts = pathinfo($sourcefile);
$filename = $pathparts['basename'];
$filepath = $pathparts['dirname'];
$foldername = basename($filepath);
// Create file record for content.
$record = array(
'contextid' => $this->context->id,
'component' => $contentid > 0 ? self::COMPONENT : 'user',
'filearea' => $contentid > 0 ? self::CONTENT_FILEAREA : 'draft',
'itemid' => $contentid > 0 ? $contentid : 0,
'filepath' => '/' . $foldername . '/',
'filename' => $filename
);
$file = $this->fs->get_file(
$record['contextid'], $record['component'],
$record['filearea'], $record['itemid'], $record['filepath'],
$record['filename']
);
if ($file) {
// Delete it to make sure that it is replaced with correct content.
$file->delete();
}
$this->fs->create_file_from_pathname($record, $sourcefile);
}
/**
* Generate H5P custom styles if any.
*/
public static function generate_custom_styles(): void {
$record = self::get_custom_styles_file_record();
$cssfile = self::get_custom_styles_file($record);
if ($cssfile) {
// The CSS file needs to be updated, so delete and recreate it
// if there is CSS in the 'h5pcustomcss' setting.
$cssfile->delete();
}
$css = get_config('core_h5p', 'h5pcustomcss');
if (!empty($css)) {
$fs = get_file_storage();
$fs->create_file_from_string($record, $css);
}
}
/**
* Get H5P custom styles if any.
*
* @throws \moodle_exception If the CSS setting is empty but there is a file to serve
* or there is no file but the CSS setting is not empty.
* @return array|null If there is CSS then an array with the keys 'cssurl'
* and 'cssversion' is returned otherwise null. 'cssurl' is a link to the
* generated 'custom_h5p.css' file and 'cssversion' the md5 hash of its contents.
*/
public static function get_custom_styles(): ?array {
$record = self::get_custom_styles_file_record();
$css = get_config('core_h5p', 'h5pcustomcss');
if (self::get_custom_styles_file($record)) {
if (empty($css)) {
// The custom CSS file exists and yet the setting 'h5pcustomcss' is empty.
// This prevents an invalid content hash.
throw new \moodle_exception(
'The H5P \'h5pcustomcss\' setting is empty and yet the custom CSS file \''.
$record['filename'].
'\' exists.',
'core_h5p'
);
}
// File exists, so generate the url and version hash.
$cssurl = \moodle_url::make_pluginfile_url(
$record['contextid'],
$record['component'],
$record['filearea'],
null,
$record['filepath'],
$record['filename']
);
return ['cssurl' => $cssurl, 'cssversion' => md5($css)];
} else if (!empty($css)) {
// The custom CSS file does not exist and yet should do.
throw new \moodle_exception(
'The H5P custom CSS file \''.
$record['filename'].
'\' does not exist and yet there is CSS in the \'h5pcustomcss\' setting.',
'core_h5p'
);
}
return null;
}
/**
* Get H5P custom styles file record.
*
* @return array File record for the CSS custom styles.
*/
private static function get_custom_styles_file_record(): array {
return [
'contextid' => \context_system::instance()->id,
'component' => self::COMPONENT,
'filearea' => self::CSS_FILEAREA,
'itemid' => 0,
'filepath' => '/',
'filename' => self::CUSTOM_CSS_FILENAME,
];
}
/**
* Get H5P custom styles file.
*
* @param array $record The H5P custom styles file record.
*
* @return stored_file|bool stored_file instance if exists, false if not.
*/
private static function get_custom_styles_file($record): stored_file|bool {
$fs = get_file_storage();
return $fs->get_file(
$record['contextid'],
$record['component'],
$record['filearea'],
$record['itemid'],
$record['filepath'],
$record['filename']
);
}
}
+85
View File
@@ -0,0 +1,85 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_h5p\form;
use core_h5p\editor;
/**
* Form to edit an existing H5P content.
*
* @package core_h5p
* @copyright 2020 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class editcontent_form extends \moodleform {
/** @var editor H5P editor object */
private $editor;
/**
* The form definition.
*/
public function definition() {
$mform = $this->_form;
$id = $this->_customdata['id'] ?? null;
$contenturl = $this->_customdata['contenturl'] ?? null;
$returnurl = $this->_customdata['returnurl'] ?? null;
$editor = new editor();
if ($id) {
$mform->addElement('hidden', 'id', $id);
$mform->setType('id', PARAM_INT);
$editor->set_content($id);
}
if ($contenturl) {
$mform->addElement('hidden', 'url', $contenturl);
$mform->setType('url', PARAM_LOCALURL);
}
if ($returnurl) {
$mform->addElement('hidden', 'returnurl', $returnurl);
$mform->setType('returnurl', PARAM_LOCALURL);
}
$this->editor = $editor;
$mformid = 'h5peditor';
$mform->setAttributes(array('id' => $mformid) + $mform->getAttributes());
$this->set_display_vertical();
$this->add_action_buttons();
$editor->add_editor_to_form($mform);
$this->add_action_buttons();
}
/**
* Updates an H5P content.
*
* @param \stdClass $data Object with all the H5P data.
*
* @return void
*/
public function save_h5p(\stdClass $data): void {
$this->editor->save_content($data);
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Upload an h5p content to update the content libraries.
*
* @package core_h5p
* @copyright 2019 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_h5p\form;
defined('MOODLE_INTERNAL') || die();
/**
* Upload a zip or h5p content to update the content libraries.
*
* @copyright 2019 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class uploadlibraries_form extends \moodleform {
/**
* Form definition.
*/
public function definition() {
$mform = $this->_form;
$mform->addElement('header', 'settingsheader', get_string('uploadlibraries', 'core_h5p'));
$filemanageroptions = array(
'accepted_types' => array('.h5p', '.zip'),
'maxbytes' => 0,
'maxfiles' => 1,
'subdirs' => 0
);
$mform->addElement('filepicker', 'h5ppackage', get_string('h5ppackage', 'core_h5p'),
null, $filemanageroptions);
$mform->addHelpButton('h5ppackage', 'h5ppackage', 'core_h5p');
$mform->addRule('h5ppackage', null, 'required');
$mform->addElement('submit', 'uploadlibraries', get_string('uploadlibraries', 'core_h5p'));
}
}
File diff suppressed because it is too large Load Diff
+514
View File
@@ -0,0 +1,514 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains helper class for the H5P area.
*
* @package core_h5p
* @copyright 2019 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_h5p;
use context_system;
use core_h5p\local\library\autoloader;
use core_user;
defined('MOODLE_INTERNAL') || die();
/**
* Helper class for the H5P area.
*
* @copyright 2019 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class helper {
/**
* Store an H5P file.
*
* @param factory $factory The \core_h5p\factory object
* @param \stored_file $file Moodle file instance
* @param \stdClass $config Button options config
* @param bool $onlyupdatelibs Whether new libraries can be installed or only the existing ones can be updated
* @param bool $skipcontent Should the content be skipped (so only the libraries will be saved)?
*
* @return int|false|null The H5P identifier or null if there is an error when saving or false if it's not a valid H5P package
*/
public static function save_h5p(factory $factory, \stored_file $file, \stdClass $config, bool $onlyupdatelibs = false,
bool $skipcontent = false) {
if (api::is_valid_package($file, $onlyupdatelibs, $skipcontent, $factory, false)) {
$core = $factory->get_core();
$h5pvalidator = $factory->get_validator();
$h5pstorage = $factory->get_storage();
$content = [
'pathnamehash' => $file->get_pathnamehash(),
'contenthash' => $file->get_contenthash(),
];
$options = ['disable' => self::get_display_options($core, $config)];
// Add the 'title' if exists from 'h5p.json' data to keep it for the editor.
if (!empty($h5pvalidator->h5pC->mainJsonData['title'])) {
$content['title'] = $h5pvalidator->h5pC->mainJsonData['title'];
}
// If exists, add the metadata from 'h5p.json' to avoid loosing this information.
$data = $h5pvalidator->h5pC->mainJsonData;
if (!empty($data)) {
// The metadata fields are defined in 'joubel/core/h5p-metadata.class.php'.
$metadatafields = [
'title',
'a11yTitle',
'changes',
'authors',
'source',
'license',
'licenseVersion',
'licenseExtras',
'authorComments',
'yearFrom',
'yearTo',
'defaultLanguage',
];
$content['metadata'] = array_reduce($metadatafields, function ($array, $field) use ($data) {
if (array_key_exists($field, $data)) {
$array[$field] = $data[$field];
}
return $array;
}, []);
}
$h5pstorage->savePackage($content, null, $skipcontent, $options);
return $h5pstorage->contentId;
}
return false;
}
/**
* 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.
*
* @param core $core The \core_h5p\core object
* @param stdClass $config Button options config
*
* @return int The representation of display options as int
*/
public static function get_display_options(core $core, \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 $core->getStorableDisplayOptions($disableoptions, 0);
}
/**
* Convert the int representation of display options into stdClass
*
* @param core $core The \core_h5p\core object
* @param int $displayint integer value representing display options
*
* @return int The representation of display options as int
*/
public static function decode_display_options(core $core, int $displayint = null): \stdClass {
$config = new \stdClass();
if ($displayint === null) {
$displayint = self::get_display_options($core, $config);
}
$displayarray = $core->getDisplayOptionsForEdit($displayint);
$config->export = $displayarray[core::DISPLAY_OPTION_DOWNLOAD] ?? 0;
$config->embed = $displayarray[core::DISPLAY_OPTION_EMBED] ?? 0;
$config->copyright = $displayarray[core::DISPLAY_OPTION_COPYRIGHT] ?? 0;
return $config;
}
/**
* Checks if the author of the .h5p file is "trustable". If the file hasn't been uploaded by a user with the
* required capability, the content won't be deployed, unless the user has been deleted, in this
* case we check the capability against current user.
*
* @param stored_file $file The .h5p file to be deployed
* @return bool Returns true if the file can be deployed, false otherwise.
*/
public static function can_deploy_package(\stored_file $file): bool {
$userid = $file->get_userid();
if (null === $userid) {
// If there is no userid, it is owned by the system.
return true;
}
$context = \context::instance_by_id($file->get_contextid());
$fileuser = core_user::get_user($userid);
if (empty($fileuser) || $fileuser->deleted) {
$userid = null;
}
return has_capability('moodle/h5p:deploy', $context, $userid);
}
/**
* Checks if the content-type libraries can be upgraded.
* The H5P content-type libraries can only be upgraded if the author of the .h5p file can manage content-types or if all the
* content-types exist, to avoid users without the required capability to upload malicious content. If user has been deleted
* we check against current user.
*
* @param stored_file $file The .h5p file to be deployed
* @return bool Returns true if the content-type libraries can be created/updated, false otherwise.
*/
public static function can_update_library(\stored_file $file): bool {
$userid = $file->get_userid();
if (null === $userid) {
// If there is no userid, it is owned by the system.
return true;
}
// Check if the owner of the .h5p file has the capability to manage content-types.
$context = \context::instance_by_id($file->get_contextid());
$fileuser = core_user::get_user($userid);
if (empty($fileuser) || $fileuser->deleted) {
$userid = null;
}
return has_capability('moodle/h5p:updatelibraries', $context, $userid);
}
/**
* Convenience to take a fixture test file and create a stored_file.
*
* @param string $filepath The filepath of the file
* @param int $userid The author of the file
* @param \context $context The context where the file will be created
* @return \stored_file The file created
*/
public static function create_fake_stored_file_from_path(string $filepath, int $userid = 0,
\context $context = null): \stored_file {
if (is_null($context)) {
$context = context_system::instance();
}
$filerecord = [
'contextid' => $context->id,
'component' => 'core_h5p',
'filearea' => 'unittest',
'itemid' => rand(),
'filepath' => '/',
'filename' => basename($filepath),
];
if (!is_null($userid)) {
$filerecord['userid'] = $userid;
}
$fs = get_file_storage();
return $fs->create_file_from_pathname($filerecord, $filepath);
}
/**
* Get information about different H5P tools and their status.
*
* @return array Data to render by the template
*/
public static function get_h5p_tools_info(): array {
$tools = array();
// Getting information from available H5P tools one by one because their enabled/disabled options are totally different.
// Check the atto button status.
$link = \editor_atto\plugininfo\atto::get_manage_url();
$status = strpos(get_config('editor_atto', 'toolbar'), 'h5p') > -1;
$tools[] = self::convert_info_into_array('atto_h5p', $link, $status);
// Check the Display H5P filter status.
$link = \core\plugininfo\filter::get_manage_url();
$status = filter_get_active_state('displayh5p', context_system::instance()->id);
$tools[] = self::convert_info_into_array('filter_displayh5p', $link, $status);
// Check H5P scheduled task.
$link = '';
$status = 0;
$statusaction = '';
if ($task = \core\task\manager::get_scheduled_task('\core\task\h5p_get_content_types_task')) {
$status = !$task->get_disabled();
$link = new \moodle_url(
'/admin/tool/task/scheduledtasks.php',
array('action' => 'edit', 'task' => get_class($task))
);
if ($status && \core\task\manager::is_runnable() && get_config('tool_task', 'enablerunnow')) {
$statusaction = \html_writer::link(
new \moodle_url('/admin/tool/task/schedule_task.php',
array('task' => get_class($task))),
get_string('runnow', 'tool_task'));
}
}
$tools[] = self::convert_info_into_array('task_h5p', $link, $status, $statusaction);
return $tools;
}
/**
* Convert information into needed mustache template data array
* @param string $tool The name of the tool
* @param \moodle_url $link The URL to management page
* @param int $status The current status of the tool
* @param string $statusaction A link to 'Run now' option for the task
* @return array
*/
private static function convert_info_into_array(string $tool,
\moodle_url $link,
int $status,
string $statusaction = ''): array {
$statusclasses = array(
TEXTFILTER_DISABLED => 'badge bg-danger text-white',
TEXTFILTER_OFF => 'badge bg-warning text-dark',
0 => 'badge bg-danger text-white',
TEXTFILTER_ON => 'badge bg-success text-white',
);
$statuschoices = array(
TEXTFILTER_DISABLED => get_string('disabled', 'admin'),
TEXTFILTER_OFF => get_string('offbutavailable', 'core_filters'),
0 => get_string('disabled', 'admin'),
1 => get_string('enabled', 'admin'),
);
return [
'tool' => get_string($tool, 'h5p'),
'tool_description' => get_string($tool . '_description', 'h5p'),
'link' => $link,
'status' => $statuschoices[$status],
'status_class' => $statusclasses[$status],
'status_action' => $statusaction,
];
}
/**
* Get a query string with the theme revision number to include at the end
* of URLs. This is used to force the browser to reload the asset when the
* theme caches are cleared.
*
* @return string
*/
public static function get_cache_buster(): string {
global $CFG;
return '?ver=' . $CFG->themerev;
}
/**
* Get the settings needed by the H5P library.
*
* @param string|null $component
* @return array The settings.
*/
public static function get_core_settings(?string $component = null): array {
global $CFG, $USER;
$basepath = $CFG->wwwroot . '/';
$systemcontext = context_system::instance();
// H5P doesn't currently support xAPI State. It implements a mechanism in contentUserDataAjax() in h5p.js to update user
// data. However, in our case, we're overriding this method to call the xAPI State web services.
$ajaxpaths = [
'contentUserData' => '',
];
$factory = new factory();
$core = $factory->get_core();
// When there is a logged in user, her information will be passed to the player. It will be used for tracking.
$usersettings = [];
if (isloggedin()) {
$usersettings['name'] = fullname($USER, has_capability('moodle/site:viewfullnames', $systemcontext));
$usersettings['id'] = $USER->id;
}
$savefreq = false;
if ($component !== null && get_config($component, 'enablesavestate')) {
$savefreq = get_config($component, 'savestatefreq');
}
$settings = array(
'baseUrl' => $basepath,
'url' => "{$basepath}pluginfile.php/{$systemcontext->instanceid}/core_h5p",
'urlLibraries' => "{$basepath}pluginfile.php/{$systemcontext->id}/core_h5p/libraries",
'postUserStatistics' => false,
'ajax' => $ajaxpaths,
'saveFreq' => $savefreq,
'siteUrl' => $CFG->wwwroot,
'l10n' => array('H5P' => $core->getLocalization()),
'user' => $usersettings,
'hubIsEnabled' => false,
'reportingIsEnabled' => false,
'crossorigin' => !empty($CFG->h5pcrossorigin) ? $CFG->h5pcrossorigin : null,
'libraryConfig' => $core->h5pF->getLibraryConfig(),
'pluginCacheBuster' => self::get_cache_buster(),
'libraryUrl' => autoloader::get_h5p_core_library_url('js')->out(),
);
return $settings;
}
/**
* Get the core H5P assets, including all core H5P JavaScript and CSS.
*
* @param string|null $component
* @return Array core H5P assets.
*/
public static function get_core_assets(?string $component = null): array {
global $PAGE;
// Get core settings.
$settings = self::get_core_settings($component);
$settings['core'] = [
'styles' => [],
'scripts' => []
];
$settings['loadedJs'] = [];
$settings['loadedCss'] = [];
// Make sure files are reloaded for each plugin update.
$cachebuster = self::get_cache_buster();
// Use relative URL to support both http and https.
$liburl = autoloader::get_h5p_core_library_url()->out();
$relpath = '/' . preg_replace('/^[^:]+:\/\/[^\/]+\//', '', $liburl);
// Add core stylesheets.
foreach (core::$styles as $style) {
$settings['core']['styles'][] = $relpath . $style . $cachebuster;
$PAGE->requires->css(new \moodle_url($liburl . $style . $cachebuster));
}
// Add core JavaScript.
foreach (core::get_scripts() as $script) {
$settings['core']['scripts'][] = $script->out(false);
$PAGE->requires->js($script, true);
}
return $settings;
}
/**
* Prepare the library name to be used as a cache key (remove whitespaces and replace dots to underscores).
*
* @param string $library Library name.
* @return string Library name in a cache simple key format (a-zA-Z0-9_).
*/
public static function get_cache_librarykey(string $library): string {
// Remove whitespaces and replace '.' to '_'.
return str_replace('.', '_', str_replace(' ', '', $library));
}
/**
* Parse a JS array to a PHP array.
*
* @param string $jscontent The JS array to parse to PHP array.
* @return array The JS array converted to PHP array.
*/
public static function parse_js_array(string $jscontent): array {
// Convert all line-endings to UNIX format first.
$jscontent = str_replace(array("\r\n", "\r"), "\n", $jscontent);
$jsarray = preg_split('/,\n\s+/', substr($jscontent, 0, -1));
$jsarray = preg_replace('~{?\\n~', '', $jsarray);
$strings = [];
foreach ($jsarray as $key => $value) {
$splitted = explode(":", $value, 2);
$value = preg_replace("/^['|\"](.*)['|\"]$/", "$1", trim($splitted[1], ' ,'));
$strings[ trim($splitted[0]) ] = str_replace("\'", "'", $value);
}
return $strings;
}
/**
* Get the information related to the H5P export file.
* The information returned will be:
* - filename, filepath, mimetype, filesize, timemodified and fileurl.
*
* @param string $exportfilename The H5P export filename (with slug).
* @param \moodle_url $url The URL of the exported file.
* @param factory $factory The \core_h5p\factory object
* @return array|null The information export file otherwise null.
*/
public static function get_export_info(string $exportfilename, \moodle_url $url = null, ?factory $factory = null): ?array {
if (!$factory) {
$factory = new factory();
}
$core = $factory->get_core();
// Get export file.
if (!$fileh5p = $core->fs->get_export_file($exportfilename)) {
return null;
}
// Build the export info array.
$file = [];
$file['filename'] = $fileh5p->get_filename();
$file['filepath'] = $fileh5p->get_filepath();
$file['mimetype'] = $fileh5p->get_mimetype();
$file['filesize'] = $fileh5p->get_filesize();
$file['timemodified'] = $fileh5p->get_timemodified();
if (!$url) {
$url = \moodle_url::make_webservice_pluginfile_url(
$fileh5p->get_contextid(),
$fileh5p->get_component(),
$fileh5p->get_filearea(),
'',
'',
$fileh5p->get_filename()
);
}
$file['fileurl'] = $url->out(false);
return $file;
}
}
+169
View File
@@ -0,0 +1,169 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* H5P autoloader management class.
*
* @package core_h5p
* @copyright 2019 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_h5p\local\library;
/**
* H5P autoloader management class.
*
* @package core_h5p
* @copyright 2019 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class autoloader {
/**
* Returns the list of plugins that can work as H5P library handlers (have class PLUGINNAME\local\library\handler)
* @return array with the format: pluginname => class
*/
public static function get_all_handlers(): array {
$handlers = [];
$plugins = \core_component::get_plugin_list_with_class('h5plib', 'local\library\handler') +
\core_component::get_plugin_list_with_class('h5plib', 'local_library_handler');
// Allow plugins to have the class either with namespace or without (useful for unittest).
foreach ($plugins as $pname => $class) {
$handlers[$pname] = $class;
}
return $handlers;
}
/**
* Returns the default H5P library handler class.
*
* @return string|null H5P library handler class
*/
public static function get_default_handler(): ?string {
$default = null;
$handlers = self::get_all_handlers();
if (!empty($handlers)) {
// The default handler will be the first value in the list.
$default = array_shift($handlers);
}
return $default;
}
/**
* Returns the default H5P library handler.
*
* @return string|null H5P library handler
*/
public static function get_default_handler_library(): ?string {
$default = null;
$handlers = self::get_all_handlers();
if (!empty($handlers)) {
// The default handler will be the first in the list.
$keys = array_keys($handlers);
$default = array_shift($keys);
}
return $default;
}
/**
* Returns the current H5P library handler class.
*
* @return string H5P library handler class
* @throws \moodle_exception
*/
public static function get_handler_classname(): string {
global $CFG;
$handlers = self::get_all_handlers();
if (!empty($CFG->h5plibraryhandler)) {
if (isset($handlers[$CFG->h5plibraryhandler])) {
return $handlers[$CFG->h5plibraryhandler];
}
}
// If no handler has been defined, return the default one.
$defaulthandler = self::get_default_handler();
if (empty($defaulthandler)) {
// If there is no default handler, throw an exception.
throw new \moodle_exception('noh5plibhandlerdefined', 'core_h5p');
}
return $defaulthandler;
}
/**
* Get the current version of the H5P core library.
*
* @return string
*/
public static function get_h5p_version(): string {
return component_class_callback(self::get_handler_classname(), 'get_h5p_version', []);
}
/**
* Get a URL for the current H5P Core Library.
*
* @param string $filepath The path within the h5p root
* @param array $params these params override current params or add new
* @return null|moodle_url
*/
public static function get_h5p_core_library_url(?string $filepath = null, ?array $params = null): ?\moodle_url {
return component_class_callback(self::get_handler_classname(), 'get_h5p_core_library_url', [$filepath, $params]);
}
/**
* Get a URL for the current H5P Editor Library.
*
* @param string $filepath The path within the h5p root.
* @param array $params These params override current params or add new.
* @return null|\moodle_url The moodle_url instance to a file in the H5P Editor library.
*/
public static function get_h5p_editor_library_url(?string $filepath = null, ?array $params = null): ?\moodle_url {
return component_class_callback(self::get_handler_classname(), 'get_h5p_editor_library_url', [$filepath, $params]);
}
/**
* Get the base path for the current H5P Editor Library.
*
* @param string $filepath The path within the h5p root.
* @return string Path to a file in the H5P Editor library.
*/
public static function get_h5p_editor_library_base(?string $filepath = null): string {
return component_class_callback(self::get_handler_classname(), 'get_h5p_editor_library_base', [$filepath]);
}
/**
* Returns a localized string, if it exists in the h5plib plugin and the value it's different from the English version.
*
* @param string $identifier The key identifier for the localized string
* @param string $language Language to get the localized string.
* @return string|null The localized string or null if it doesn't exist in this H5P library plugin.
*/
public static function get_h5p_string(string $identifier, string $language): ?string {
return component_class_callback(self::get_handler_classname(), 'get_h5p_string', [$identifier, $language]);
}
/**
* Register the H5P autoloader.
*/
public static function register(): void {
component_class_callback(self::get_handler_classname(), 'register', []);
}
}
+180
View File
@@ -0,0 +1,180 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Base class for library handlers.
*
* @package core_h5p
* @copyright 2019 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_h5p\local\library;
defined('MOODLE_INTERNAL') || die();
/**
* Base class for library handlers.
*
* If a new H5P libraries handler plugin has to be created, it has to define class
* PLUGINNAME\local\library\handler that extends \core_h5p\local\library\handler.
*
* @package core_h5p
* @copyright 2019 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class handler {
/**
* Get the current version of the H5P core library.
*
* @return string
*/
abstract public static function get_h5p_version(): string;
/**
* Get the base path for the H5P Libraries.
*
* @return null|string
*/
public static function get_h5p_library_base(): ?string {
$h5pversion = static::get_h5p_version();
return "/h5p/h5plib/v{$h5pversion}/joubel";
}
/**
* Get the base path for the current H5P Core Library.
*
* @param string $filepath The path within the H5P root
* @return null|string
*/
public static function get_h5p_core_library_base(?string $filepath = null): ?string {
return static::get_h5p_library_base() . "/core/{$filepath}";
}
/**
* Get the base path for the current H5P Editor Library.
*
* @param null|string $filepath The path within the H5P root.
* @return string Path to a file in the H5P Editor library.
*/
public static function get_h5p_editor_library_base(?string $filepath = null): string {
return static::get_h5p_library_base() . "/editor/{$filepath}";
}
/**
* Register the H5P autoloader.
*/
public static function register(): void {
// Prepend H5P libraries in order to guarantee they are loaded first. Plugins using same libraries will need to use a
// different namespace if they want to use a different version.
spl_autoload_register([static::class, 'autoload'], true, true);
}
/**
* SPL Autoloading function for H5P.
*
* @param string $classname The name of the class to load
*/
public static function autoload($classname): void {
global $CFG;
$classes = static::get_class_list();
if (isset($classes[$classname])) {
if (file_exists($CFG->dirroot . static::get_h5p_core_library_base($classes[$classname]))) {
require_once($CFG->dirroot . static::get_h5p_core_library_base($classes[$classname]));
} else {
require_once($CFG->dirroot . static::get_h5p_editor_library_base($classes[$classname]));
}
}
}
/**
* Get a URL for the current H5P Core Library.
*
* @param string $filepath The path within the h5p root
* @param array $params these params override current params or add new
* @return null|\moodle_url
*/
public static function get_h5p_core_library_url(?string $filepath = null, ?array $params = null): ?\moodle_url {
return new \moodle_url(static::get_h5p_core_library_base($filepath), $params);
}
/**
* Get a URL for the current H5P Editor Library.
*
* @param string $filepath The path within the h5p root.
* @param array $params These params override current params or add new.
* @return null|\moodle_url The moodle_url to a file in the H5P Editor library.
*/
public static function get_h5p_editor_library_url(?string $filepath = null, ?array $params = null): ?\moodle_url {
return new \moodle_url(static::get_h5p_editor_library_base($filepath), $params);
}
/**
* Returns a localized string, if it exists in the h5plib plugin and the value it's different from the English version.
*
* @param string $identifier The key identifier for the localized string
* @param string $language Language to get the localized string.
* @return string|null The localized string or null if it doesn't exist in this H5P library plugin.
*/
public static function get_h5p_string(string $identifier, string $language): ?string {
$value = null;
$h5pversion = static::get_h5p_version();
$component = 'h5plib_v' . $h5pversion;
// Composed code languages, such as 'Spanish, Mexican' are different in H5P and Moodle:
// - In H5P, they use '-' to separate language from the country. For instance: es-mx.
// - However, in Moodle, they have '_' instead of '-'. For instance: es_mx.
$language = str_replace('-', '_', $language);
if (get_string_manager()->string_exists($identifier, $component)) {
$defaultmoodlelang = 'en';
// In Moodle, all the English strings always will exist because they have to be declared in order to let users
// to translate them. That's why, this method will only replace existing key if the value is different from
// the English version and the current language is not English.
$string = new \lang_string($identifier, $component);
if ($language === $defaultmoodlelang || $string->out($language) !== $string->out($defaultmoodlelang)) {
$value = $string->out($language);
}
}
return $value;
}
/**
* Return the list of classes with their location within the joubel directory.
*
* @return array
*/
protected static function get_class_list(): array {
return [
'Moodle\H5PCore' => 'h5p.classes.php',
'Moodle\H5PFrameworkInterface' => 'h5p.classes.php',
'Moodle\H5PContentValidator' => 'h5p.classes.php',
'Moodle\H5PValidator' => 'h5p.classes.php',
'Moodle\H5PStorage' => 'h5p.classes.php',
'Moodle\H5PDevelopment' => 'h5p-development.class.php',
'Moodle\H5PFileStorage' => 'h5p-file-storage.interface.php',
'Moodle\H5PDefaultStorage' => 'h5p-default-storage.class.php',
'Moodle\H5PMetadata' => 'h5p-metadata.class.php',
'Moodle\H5peditor' => 'h5peditor.class.php',
'Moodle\H5peditorStorage' => 'h5peditor-storage.interface.php',
'Moodle\H5PEditorAjaxInterface' => 'h5peditor-ajax.interface.php',
'Moodle\H5PEditorAjax' => 'h5peditor-ajax.class.php',
'Moodle\H5peditorFile' => 'h5peditor-file.class.php',
];
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Provides {@link \core_h5p\output\h5peditor} class.
*
* @package core_h5p
* @copyright 2020 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_h5p\output;
defined('MOODLE_INTERNAL') || die();
/**
* Displays the H5P Editor
*
* @copyright 2020 Victor Deniz <victor@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class h5peditor implements \renderable, \templatable {
/** @var \stdClass Context in which the H5P Editor is being used. */
protected $context;
/**
* h5peditor constructor.
*
* @param \stdClass $context H5P editor context generated by core_h5p\editor.
*/
public function __construct(\stdClass $context) {
$this->context = $context;
}
/**
* Exports the data required for the H5P Editor.
*
* @param \renderer_base $output
* @return \stdClass
*/
public function export_for_template(\renderer_base $output) {
return $this->context;
}
}
+111
View File
@@ -0,0 +1,111 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Contains class core_h5p\output\libraries
*
* @package core_h5p
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_h5p\output;
defined('MOODLE_INTERNAL') || die();
use renderable;
use templatable;
use renderer_base;
use stdClass;
use moodle_url;
use action_menu;
use action_menu_link;
use pix_icon;
/**
* Class to help display H5P library management table.
*
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class libraries implements renderable, templatable {
/** @var H5P factory */
protected $factory;
/** @var H5P library list */
protected $libraries;
/**
* Constructor.
*
* @param factory $factory The H5P factory
* @param array $libraries array of h5p libraries records
*/
public function __construct(\core_h5p\factory $factory, array $libraries) {
$this->factory = $factory;
$this->libraries = $libraries;
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param renderer_base $output
* @return stdClass
*/
public function export_for_template(renderer_base $output) {
$installed = [];
$filestorage = $this->factory->get_core()->fs;
foreach ($this->libraries as $libraryname => $versions) {
foreach ($versions as $version) {
// Get the icon URL.
$version->icon = $filestorage->get_icon_url(
$version->id,
$version->machine_name,
$version->major_version,
$version->minor_version
);
// Get the action menu options.
$actionmenu = new action_menu();
$actionmenu->set_menu_trigger(get_string('actions', 'core_h5p'));
$actionmenu->prioritise = true;
$actionmenu->add_primary_action(new action_menu_link(
new moodle_url('/h5p/libraries.php', ['deletelibrary' => $version->id]),
new pix_icon('t/delete', get_string('deletelibraryversion', 'core_h5p')),
get_string('deletelibraryversion', 'core_h5p')
));
$version->actionmenu = $actionmenu->export_for_template($output);
if ($version->enabled) {
$version->toggleenabledurl = new moodle_url('/h5p/libraries.php', [
'id' => $version->id,
'action' => 'disable',
'sesskey' => sesskey(),
]);
} else {
$version->toggleenabledurl = new moodle_url('/h5p/libraries.php', [
'id' => $version->id,
'action' => 'enable',
'sesskey' => sesskey(),
]);
}
$installed[] = $version;
}
}
$r = new stdClass();
$r->contenttypes = $installed;
return $r;
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_h5p\output;
use plugin_renderer_base;
/**
* Renderer class.
*
* @package core_h5p
* @copyright 2020 Victor Deniz {victor@moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class renderer extends plugin_renderer_base {
/**
* Alter which stylesheets are loaded for H5P.
* This is useful for adding custom styles or replacing existing ones.
*
* This method can be overridden by other themes if the styles must be loaded from
* a different place than the "Raw initial SCSS" and "Raw SCSS" theme settings.
*
* @param \stdClass[] $styles List of stylesheets that will be loaded
* @param array $libraries Array of libraries indexed by the library's machineName
* @param string $embedtype Possible values: div, iframe, external, editor
*/
public function h5p_alter_styles(&$styles, array $libraries, string $embedtype) {
$customcss = \core_h5p\file_storage::get_custom_styles();
if (!empty($customcss)) {
// Add the CSS file to the styles array, to load it from the H5P player.
$styles[] = (object) [
'path' => $customcss['cssurl']->out(),
'version' => '?ver='.$customcss['cssversion'],
];
}
}
/**
* Alter which scripts are loaded for H5P.
* This is useful for adding custom scripts or replacing existing ones.
*
* @param array|object $scripts List of JavaScripts that will be loaded
* @param array $libraries Array of libraries indexed by the library's machineName
* @param string $embedtype Possible values: div, iframe, external, editor
*/
public function h5p_alter_scripts(&$scripts, array $libraries, string $embedtype) {
}
/**
* Alter semantics before they are processed. This is useful for changing
* how the editor looks and how content parameters are filtered.
*
* @param object|object $semantics Semantics as object
* @param string $name Machine name of library
* @param int $majorversion Major version of library
* @param int $minorversion Minor version of library
*/
public function h5p_alter_semantics(&$semantics, $name, $majorversion, $minorversion) {
}
/**
* Alter parameters of H5P content after it has been filtered through semantics.
* This is useful for adapting the content to the current context.
*
* @param array|object $parameters The content parameters for the library
* @param string $name The machine readable name of the library
* @param int $majorversion Major version of the library
* @param int $minorversion Minor version of the library
*/
public function h5p_alter_filtered_parameters(&$parameters, string $name, int $majorversion, int $minorversion) {
}
}
+636
View File
@@ -0,0 +1,636 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* H5P player class.
*
* @package core_h5p
* @copyright 2019 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_h5p;
defined('MOODLE_INTERNAL') || die();
use core_h5p\local\library\autoloader;
use core_xapi\handler;
use core_xapi\local\state;
use core_xapi\local\statement\item_activity;
use core_xapi\local\statement\item_agent;
use core_xapi\xapi_exception;
/**
* H5P player class, for displaying any local H5P content.
*
* @package core_h5p
* @copyright 2019 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class player {
/**
* @var string The local H5P URL containing the .h5p file to display.
*/
private $url;
/**
* @var core The H5PCore object.
*/
private $core;
/**
* @var int H5P DB id.
*/
private $h5pid;
/**
* @var array JavaScript requirements for this H5P.
*/
private $jsrequires = [];
/**
* @var array CSS requirements for this H5P.
*/
private $cssrequires = [];
/**
* @var array H5P content to display.
*/
private $content;
/**
* @var string optional component name to send xAPI statements.
*/
private $component;
/**
* @var string Type of embed object, div or iframe.
*/
private $embedtype;
/**
* @var context The context object where the .h5p belongs.
*/
private $context;
/**
* @var factory The \core_h5p\factory object.
*/
private $factory;
/**
* @var stdClass The error, exception and info messages, raised while preparing and running the player.
*/
private $messages;
/**
* @var bool Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions.
*/
private $preventredirect;
/**
* Inits the H5P player for rendering the content.
*
* @param string $url Local URL of the H5P file to display.
* @param \stdClass $config Configuration for H5P buttons.
* @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions
* @param string $component optional moodle component to sent xAPI tracking
* @param bool $skipcapcheck Whether capabilities should be checked or not to get the pluginfile URL because sometimes they
* might be controlled before calling this method.
*/
public function __construct(string $url, \stdClass $config, bool $preventredirect = true, string $component = '',
bool $skipcapcheck = false) {
if (empty($url)) {
throw new \moodle_exception('h5pinvalidurl', 'core_h5p');
}
$this->url = new \moodle_url($url);
$this->preventredirect = $preventredirect;
$this->factory = new \core_h5p\factory();
$this->messages = new \stdClass();
$this->component = $component;
// Create \core_h5p\core instance.
$this->core = $this->factory->get_core();
// Get the H5P identifier linked to this URL.
list($file, $this->h5pid) = api::create_content_from_pluginfile_url(
$url,
$config,
$this->factory,
$this->messages,
$this->preventredirect,
$skipcapcheck
);
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 encoded URL for embeding this H5P content.
*
* @param string $url Local URL of the H5P file to display.
* @param \stdClass $config Configuration for H5P buttons.
* @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions
* @param string $component optional moodle component to sent xAPI tracking
* @param bool $displayedit Whether the edit button should be displayed below the H5P content.
* @param \action_link[] $extraactions Extra actions to display above the H5P content.
*
* @return string The embedable code to display a H5P file.
*/
public static function display(
string $url,
\stdClass $config,
bool $preventredirect = true,
string $component = '',
bool $displayedit = false,
array $extraactions = [],
): string {
global $OUTPUT, $CFG;
$params = [
'url' => $url,
'preventredirect' => $preventredirect,
'component' => $component,
];
$optparams = ['frame', 'export', 'embed', 'copyright'];
foreach ($optparams as $optparam) {
if (!empty($config->$optparam)) {
$params[$optparam] = $config->$optparam;
}
}
$fileurl = new \moodle_url('/h5p/embed.php', $params);
$template = new \stdClass();
$template->embedurl = $fileurl->out(false);
if ($displayedit) {
list($originalfile, $h5p) = api::get_original_content_from_pluginfile_url($url, $preventredirect, true);
if ($originalfile) {
// Check if the user can edit this content.
if (api::can_edit_content($originalfile)) {
$template->editurl = (new \moodle_url('/h5p/edit.php', ['url' => $url]))->out(false);
}
}
}
$template->extraactions = [];
foreach ($extraactions as $action) {
$template->extraactions[] = $action->export_for_template($OUTPUT);
}
$result = $OUTPUT->render_from_template('core_h5p/h5pembed', $template);
$result .= self::get_resize_code();
return $result;
}
/**
* Get the error messages stored in our H5P framework.
*
* @return stdClass with framework error messages.
*/
public function get_messages(): \stdClass {
return helper::get_messages($this->messages, $this->factory);
}
/**
* Create the H5PIntegration variable that will be included in the page. This variable is used as the
* main H5P config variable.
*/
public function add_assets_to_page() {
global $PAGE, $USER;
$cid = $this->get_cid();
$systemcontext = \context_system::instance();
$disable = array_key_exists('disable', $this->content) ? $this->content['disable'] : core::DISABLE_NONE;
$displayoptions = $this->core->getDisplayOptionsForView($disable, $this->h5pid);
$contenturl = \moodle_url::make_pluginfile_url($systemcontext->id, \core_h5p\file_storage::COMPONENT,
\core_h5p\file_storage::CONTENT_FILEAREA, $this->h5pid, null, null);
$exporturl = $this->get_export_settings($displayoptions[ core::DISPLAY_OPTION_DOWNLOAD ]);
$xapiobject = item_activity::create_from_id($this->context->id);
$contentsettings = [
'library' => core::libraryToString($this->content['library']),
'fullScreen' => $this->content['library']['fullscreen'],
'exportUrl' => ($exporturl instanceof \moodle_url) ? $exporturl->out(false) : '',
'embedCode' => $this->get_embed_code($this->url->out(),
$displayoptions[ core::DISPLAY_OPTION_EMBED ]),
'resizeCode' => self::get_resize_code(),
'title' => $this->content['slug'],
'displayOptions' => $displayoptions,
'url' => $xapiobject->get_data()->id,
'contentUrl' => $contenturl->out(),
'metadata' => $this->content['metadata'],
'contentUserData' => [0 => ['state' => $this->get_state_data($xapiobject)]],
];
// Get the core H5P assets, needed by the H5P classes to render the H5P content.
$settings = $this->get_assets();
$settings['contents'][$cid] = array_merge($settings['contents'][$cid], $contentsettings);
// Print JavaScript settings to page.
$PAGE->requires->data_for_js('H5PIntegration', $settings, true);
}
/**
* Get the stored xAPI state to use as user data.
*
* @param item_activity $xapiobject
* @return string The state data to pass to the player frontend
*/
private function get_state_data(item_activity $xapiobject): string {
global $USER;
// Initialize the H5P content with the saved state (if it's enabled and the user has some stored state).
$emptystatedata = '{}';
$savestate = (bool) get_config($this->component, 'enablesavestate');
if (!$savestate) {
return $emptystatedata;
}
$xapihandler = handler::create($this->component);
if (!$xapihandler) {
return $emptystatedata;
}
// The component implements the xAPI handler, so the state can be loaded.
$state = new state(
item_agent::create_from_user($USER),
$xapiobject,
'state',
null,
null
);
try {
$state = $xapihandler->load_state($state);
if (!$state) {
// Check if the state has been restored from a backup for the current user.
$state = new state(
item_agent::create_from_user($USER),
$xapiobject,
'restored',
null,
null
);
$state = $xapihandler->load_state($state);
if ($state && !is_null($state->get_state_data())) {
// A restored state has been found. It will be replaced with one with the proper stateid and statedata.
$xapihandler->delete_state($state);
$state = new state(
item_agent::create_from_user($USER),
$xapiobject,
'state',
$state->jsonSerialize(),
null
);
$xapihandler->save_state($state);
}
}
if (!$state) {
return $emptystatedata;
}
if (is_null($state->get_state_data())) {
// The state content should be reset because, for instance, the content has changed.
return 'RESET';
}
$statedata = $state->jsonSerialize();
if (is_null($statedata)) {
return $emptystatedata;
}
if (property_exists($statedata, 'h5p')) {
// As the H5P state doesn't always use JSON, we have added this h5p object to jsonize it.
return $statedata->h5p;
}
} catch (xapi_exception $exception) {
return $emptystatedata;
}
return $emptystatedata;
}
/**
* Outputs H5P wrapper HTML.
*
* @return string The HTML code to display this H5P content.
*/
public function output(): string {
global $OUTPUT, $USER;
$template = new \stdClass();
$template->h5pid = $this->h5pid;
if ($this->embedtype === 'div') {
$h5phtml = $OUTPUT->render_from_template('core_h5p/h5pdiv', $template);
} else {
$h5phtml = $OUTPUT->render_from_template('core_h5p/h5piframe', $template);
}
// Trigger capability_assigned event.
\core_h5p\event\h5p_viewed::create([
'objectid' => $this->h5pid,
'userid' => $USER->id,
'context' => $this->get_context(),
'other' => [
'url' => $this->url->out(),
'time' => time()
]
])->trigger();
return $h5phtml;
}
/**
* Get the title of the H5P content to display.
*
* @return string the title
*/
public function get_title(): string {
return $this->content['title'];
}
/**
* Get the context where the .h5p file belongs.
*
* @return context The context.
*/
public function get_context(): \context {
return $this->context;
}
/**
* Delete an H5P package.
*
* @param stdClass $content The H5P package to delete.
*/
private function delete_h5p(\stdClass $content) {
$h5pstorage = $this->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);
}
/**
* Export path for settings
*
* @param bool $downloadenabled Whether the option to export the H5P content is enabled.
*
* @return \moodle_url|null The URL of the exported file.
*/
private function get_export_settings(bool $downloadenabled): ?\moodle_url {
if (!$downloadenabled) {
return null;
}
$systemcontext = \context_system::instance();
$slug = $this->content['slug'] ? $this->content['slug'] . '-' : '';
$filename = "{$slug}{$this->content['id']}.h5p";
// We have to build the right URL.
// Depending the request was made through webservice/pluginfile.php or pluginfile.php.
if (strpos($this->url, '/webservice/pluginfile.php')) {
$url = \moodle_url::make_webservice_pluginfile_url(
$systemcontext->id,
\core_h5p\file_storage::COMPONENT,
\core_h5p\file_storage::EXPORT_FILEAREA,
'',
'',
$filename
);
} else {
// If the request is made by tokenpluginfile.php we need to indicates to generate a token for current user.
$includetoken = false;
if (strpos($this->url, '/tokenpluginfile.php')) {
$includetoken = true;
}
$url = \moodle_url::make_pluginfile_url(
$systemcontext->id,
\core_h5p\file_storage::COMPONENT,
\core_h5p\file_storage::EXPORT_FILEAREA,
'',
'',
$filename,
false,
$includetoken
);
}
// Get the required info from the export file to be able to get the export file by third apps.
$file = helper::get_export_info($filename, $url);
if ($file) {
$url->param('modified', $file['timemodified']);
}
return $url;
}
/**
* Get the identifier for the H5P content, to be used in the arrays as index.
*
* @return string The identifier.
*/
private function get_cid(): string {
return 'cid-' . $this->h5pid;
}
/**
* Get the core H5P assets, including all core H5P JavaScript and CSS.
*
* @return Array core H5P assets.
*/
private function get_assets(): array {
// Get core assets.
$settings = helper::get_core_assets($this->component);
// Added here because in the helper we don't have the h5p content id.
$settings['moodleLibraryPaths'] = $this->core->get_dependency_roots($this->h5pid);
// Add also the Moodle component where the results will be tracked.
$settings['moodleComponent'] = $this->component;
if (!empty($settings['moodleComponent'])) {
$settings['reportingIsEnabled'] = true;
}
$cid = $this->get_cid();
// The filterParameters function should be called before getting the dependencyfiles because it rebuild content
// dependency cache and export file.
$settings['contents'][$cid]['jsonContent'] = $this->get_filtered_parameters();
$files = $this->get_dependency_files();
if ($this->embedtype === 'div') {
$systemcontext = \context_system::instance();
$h5ppath = "/pluginfile.php/{$systemcontext->id}/core_h5p";
// Schedule JavaScripts for loading through Moodle.
foreach ($files['scripts'] as $script) {
$url = $script->path . $script->version;
// Add URL prefix if not external.
$isexternal = strpos($script->path, '://');
if ($isexternal === false) {
$url = $h5ppath . $url;
}
$settings['loadedJs'][] = $url;
$this->jsrequires[] = new \moodle_url($isexternal ? $url : $CFG->wwwroot . $url);
}
// Schedule stylesheets for loading through Moodle.
foreach ($files['styles'] as $style) {
$url = $style->path . $style->version;
// Add URL prefix if not external.
$isexternal = strpos($style->path, '://');
if ($isexternal === false) {
$url = $h5ppath . $url;
}
$settings['loadedCss'][] = $url;
$this->cssrequires[] = new \moodle_url($isexternal ? $url : $CFG->wwwroot . $url);
}
} else {
// JavaScripts and stylesheets will be loaded through h5p.js.
$settings['contents'][$cid]['scripts'] = $this->core->getAssetsUrls($files['scripts']);
$settings['contents'][$cid]['styles'] = $this->core->getAssetsUrls($files['styles']);
}
return $settings;
}
/**
* Get filtered parameters, modifying them by the renderer if the theme implements the h5p_alter_filtered_parameters function.
*
* @return string Filtered parameters.
*/
private function get_filtered_parameters(): string {
global $PAGE;
$safeparams = $this->core->filterParameters($this->content);
$decodedparams = json_decode($safeparams);
$h5poutput = $PAGE->get_renderer('core_h5p');
$h5poutput->h5p_alter_filtered_parameters(
$decodedparams,
$this->content['library']['name'],
$this->content['library']['majorVersion'],
$this->content['library']['minorVersion']
);
$safeparams = json_encode($decodedparams);
return $safeparams;
}
/**
* Finds library dependencies of view
*
* @return array Files that the view has dependencies to
*/
private function get_dependency_files(): array {
global $PAGE;
$preloadeddeps = $this->core->loadContentDependencies($this->h5pid, 'preloaded');
$files = $this->core->getDependenciesFiles($preloadeddeps);
// Add additional asset files if required.
$h5poutput = $PAGE->get_renderer('core_h5p');
$h5poutput->h5p_alter_scripts($files['scripts'], $preloadeddeps, $this->embedtype);
$h5poutput->h5p_alter_styles($files['styles'], $preloadeddeps, $this->embedtype);
return $files;
}
/**
* Resizing script for settings
*
* @return string The HTML code with the resize script.
*/
private static function get_resize_code(): string {
global $OUTPUT;
$template = new \stdClass();
$template->resizeurl = autoloader::get_h5p_core_library_url('js/h5p-resizer.js');
return $OUTPUT->render_from_template('core_h5p/h5presize', $template);
}
/**
* Embed code for settings
*
* @param string $url The URL of the .h5p file.
* @param bool $embedenabled Whether the option to embed the H5P content is enabled.
*
* @return string The HTML code to reuse this H5P content in a different place.
*/
private function get_embed_code(string $url, bool $embedenabled): string {
global $OUTPUT;
if ( ! $embedenabled) {
return '';
}
$template = new \stdClass();
$template->embedurl = self::get_embed_url($url, $this->component)->out(false);
return $OUTPUT->render_from_template('core_h5p/h5pembed', $template);
}
/**
* Get the encoded URL for embeding this H5P content.
* @param string $url The URL of the .h5p file.
* @param string $component optional Moodle component to send xAPI tracking
*
* @return \moodle_url The embed URL.
*/
public static function get_embed_url(string $url, string $component = ''): \moodle_url {
$params = ['url' => $url];
if (!empty($component)) {
// If component is not empty, it will be passed too, in order to allow tracking too.
$params['component'] = $component;
}
return new \moodle_url('/h5p/embed.php', $params);
}
/**
* Return the info export file for Mobile App.
*
* @return array or null
*/
public function get_export_file(): ?array {
// Get the export url.
$exporturl = $this->get_export_settings(true);
// Get the filename of the export url.
$path = $exporturl->out_as_local_url();
// Check if the URL has parameters.
$parts = explode('?', $path);
$path = array_shift($parts);
$parts = explode('/', $path);
$filename = array_pop($parts);
// Get the required info from the export file to be able to get the export file by third apps.
$file = helper::get_export_info($filename, $exporturl);
return $file;
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy provider implementation for h5p core subsytem.
*
* @package core_h5p
* @copyright 2019 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_h5p\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy provider implementation for h5p core subsystem.
*
* @copyright 2019 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}