MDL-67814 core_h5p: implemented H5P Core/Editor library interfaces

This commit is contained in:
Víctor Déniz Falcón
2020-04-16 11:29:35 +08:00
committed by Adrian Greeve
parent 8d702491bc
commit 6da050d72a
18 changed files with 1345 additions and 41 deletions
+115
View File
@@ -0,0 +1,115 @@
<?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/>.
/**
* Responsible for handling AJAX requests related to H5P.
*
* @package core_h5p
* @copyright 2020 Victor Deniz <[email protected]>, based on code by Joubel AS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core_h5p\factory;
use core_h5p\framework;
define('AJAX_SCRIPT', true);
require(__DIR__ . '/../config.php');
require_once($CFG->libdir . '/filelib.php');
require_login();
$action = required_param('action', PARAM_ALPHA);
$contextid = required_param('contextId', PARAM_INT);
$context = context::instance_by_id($contextid);
if (!has_capability('moodle/h5p:updatelibraries', $context)) {
H5PCore::ajaxError(get_string('nopermissiontoedit', 'h5p'));
header('HTTP/1.1 403 Forbidden');
return;
}
$factory = new factory();
$editor = $factory->get_editor();
switch ($action) {
// Load list of libraries or details for library.
case 'libraries':
// Get parameters.
$name = optional_param('machineName', '', PARAM_TEXT);
$major = optional_param('majorVersion', 0, PARAM_INT);
$minor = optional_param('minorVersion', 0, PARAM_INT);
$language = optional_param('default-language', null, PARAM_ALPHA);
if (!empty($name)) {
$editor->ajax->action(H5PEditorEndpoints::SINGLE_LIBRARY, $name,
$major, $minor, framework::get_language(), '', '', $language);
} else {
$editor->ajax->action(H5PEditorEndpoints::LIBRARIES);
}
break;
// Load content type cache list to display available libraries in hub.
case 'contenttypecache':
$editor->ajax->action(H5PEditorEndpoints::CONTENT_TYPE_CACHE);
break;
// Handle file upload through the editor.
case 'files':
$token = required_param('token', PARAM_RAW);
$contentid = required_param('contentId', PARAM_INT);
$editor->ajax->action(H5PEditorEndpoints::FILES, $token, $contentid);
break;
// Install libraries from H5P and retrieve content json.
case 'libraryinstall':
$token = required_param('token', PARAM_RAW);
$machinename = required_param('id', PARAM_TEXT);
$editor->ajax->action(H5PEditorEndpoints::LIBRARY_INSTALL, $token, $machinename);
break;
// Handle file upload through the editor.
case 'libraryupload':
$token = required_param('token', PARAM_RAW);
$uploadpath = $_FILES['h5p']['tmp_name'];
$contentid = optional_param('contentId', 0, PARAM_INT);
$editor->ajax->action(H5PEditorEndpoints::LIBRARY_UPLOAD, $token, $uploadpath, $contentid);
break;
// Get the $language libraries translations.
case 'translations':
$language = required_param('language', PARAM_RAW);
$editor->ajax->action(H5PEditorEndpoints::TRANSLATIONS, $language);
break;
// Handle filtering of parameters through AJAX.
case 'filter':
$token = required_param('token', PARAM_RAW);
$libraryparameters = required_param('libraryParameters', PARAM_RAW);
$editor->ajax->action(H5PEditorEndpoints::FILTER, $token, $libraryparameters);
break;
// Throw error if AJAX action is not handled.
default:
throw new coding_exception('Unhandled AJAX');
break;
}
+71
View File
@@ -105,4 +105,75 @@ class api {
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);
// 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;
}
}
+21
View File
@@ -356,4 +356,25 @@ class core extends \H5PCore {
}
return true;
}
/**
* Use sesskey instead of the H5P security token.
*
* @param string $action Not used.
* @return string sesskey
*/
public static function createToken($action) {
return sesskey();
}
/**
* Check if the token matches the sesskey.
*
* @param string $action Not used.
* @param string $token Token submitted.
* @return boolean valid token
*/
public static function validToken($action, $token) {
return confirm_sesskey($token);
}
}
+116
View File
@@ -0,0 +1,116 @@
<?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 <[email protected]>, base on code by Joubel AS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_h5p;
use H5PEditorAjaxInterface;
/**
* 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 <[email protected]>, 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
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.machinename";
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) {
// This is to be implemented when the Hub client is used.
return [];
}
/**
* 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 {
// To be implemented when translations are introduced.
return [];
}
}
+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/>.
/**
* Class \core_h5p\editor_framework
*
* @package core_h5p
* @copyright 2020 Victor Deniz <[email protected]>, base on code by Joubel AS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_h5p;
use 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 <[email protected]>, base on code by Joubel AS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class editor_framework implements H5peditorStorage {
/**
* 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) {
// To be implemented when translations are introduced.
return false;
}
/**
* 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 {
$defaultcode = 'en';
$codes = [];
// Semantics is 'en' by default.
array_unshift($codes, $defaultcode);
return $codes;
}
/**
* "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';
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;
$librariesin[] = $library;
}
}
} else {
$fields = 'id, machinename as name, title, majorversion, minorversion';
$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 {
// This is to be implemented when the renderer is used.
}
/**
* 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.
}
}
+34 -3
View File
@@ -28,11 +28,10 @@ namespace core_h5p;
defined('MOODLE_INTERNAL') || die();
use core_h5p\local\library\autoloader;
use core_h5p\framework;
use core_h5p\core;
use H5PContentValidator as content_validator;
use H5peditor;
use H5PStorage as storage;
use H5PValidator as validator;
use H5PContentValidator as content_validator;
/**
* H5P factory class.
@@ -62,6 +61,15 @@ class factory {
/** @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.
*/
@@ -151,4 +159,27 @@ class factory {
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;
}
}
+182 -24
View File
@@ -24,7 +24,8 @@
namespace core_h5p;
defined('MOODLE_INTERNAL') || die();
use H5peditorFile;
use stored_file;
/**
* Class to handle storage and export of H5P Content.
@@ -47,6 +48,8 @@ class file_storage implements \H5PFileStorage {
public const EXPORT_FILEAREA = 'export';
/** The icon filename */
public const ICON_FILENAME = 'icon.svg';
/** The editor file area */
public const EDITOR_FILEAREA = 'editor';
/**
* @var \context $context Currently we use the system context everywhere.
@@ -164,13 +167,19 @@ class file_storage implements \H5PFileStorage {
* @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
'filename' => $filename,
'userid' => $USER->id
];
$this->fs->create_file_from_pathname($filerecord, $source);
}
@@ -315,13 +324,25 @@ class file_storage implements \H5PFileStorage {
/**
* Save files uploaded through the editor.
* The files must be marked as temporary until the content form is saved.
*
* @param \H5peditorFile $file
* @param H5peditorFile $file
* @param int $contentid
*
* @return int The id of the saved file.
*/
public function saveFile($file, $contentid) {
// This is to be implemented when the h5p editor is introduced / created.
$record = array(
'contextid' => $this->context->id,
'component' => self::COMPONENT,
'filearea' => $contentid === 0 ? self::EDITOR_FILEAREA : self::CONTENT_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();
}
/**
@@ -330,23 +351,81 @@ class file_storage implements \H5PFileStorage {
*
* @param string $file path + name
* @param string|int $fromid Content ID or 'editor' string
* @param int $toid Target Content ID
* @param \stdClass $tocontent Target Content
*
* @return void
*/
public function cloneContentFile($file, $fromid, $toid) {
// This is to be implemented when the h5p editor is introduced / created.
public function cloneContentFile($file, $fromid, $tocontent): void {
// Determine source filearea and itemid.
if ($fromid === self::EDITOR_FILEAREA) {
$sourcefilearea = self::EDITOR_FILEAREA;
$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.
* 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
*
* @return object Object containing h5p json and content json data
*/
public function moveContentDirectory($source, $contentid = null) {
// This is to be implemented when the h5p editor is introduced / created.
$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();
}
}
/**
@@ -384,26 +463,41 @@ class file_storage implements \H5PFileStorage {
}
/**
* Checks to see if content has the given file.
* Used when saving content.
* Checks to see if an H5P content has the given file.
*
* @param string $file path + name
* @param int $contentid
* @return string|int File ID or NULL if not found
* @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, $contentid) {
// This is to be implemented when the h5p editor is introduced / created.
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 path + name
* @param int $contentid
* @param string $file File path and name.
* @param int $contentid Content id.
*
* @return void
*/
public function removeContentFile($file, $contentid) {
// This is to be implemented when the h5p editor is introduced / created.
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();
}
}
/**
@@ -616,7 +710,7 @@ class file_storage implements \H5PFileStorage {
* @param string $filename File name to retrieve.
* @return bool|\stored_file Stored file instance if exists, false if not
*/
private function get_export_file(string $filename) {
public function get_export_file(string $filename) {
return $this->fs->get_file($this->context->id, self::COMPONENT, self::EXPORT_FILEAREA, 0, '/', $filename);
}
@@ -657,4 +751,68 @@ class file_storage implements \H5PFileStorage {
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 {
if ($filearea === 'editor') {
$itemid = 0;
}
$filepath = '/'. dirname($file). '/';
$filename = basename($file);
// Load file.
$existingfile = $this->fs->get_file($this->context->id, self::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' => self::COMPONENT,
'filearea' => $contentid > 0 ? self::CONTENT_FILEAREA : self::EDITOR_FILEAREA,
'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);
}
}
+12 -2
View File
@@ -795,6 +795,8 @@ class framework implements \H5PFrameworkInterface {
$content['library']['libraryId'] = $mainlibrary->id;
}
$content['disable'] = $content['disable'] ?? null;
$data = [
'jsoncontent' => $content['params'],
'displayoptions' => $content['disable'],
@@ -1162,7 +1164,7 @@ class framework implements \H5PFrameworkInterface {
$sql = "SELECT hc.id, hc.jsoncontent, hc.displayoptions, hl.id AS libraryid,
hl.machinename, hl.title, hl.majorversion, hl.minorversion, hl.fullscreen,
hl.embedtypes, hl.semantics, hc.filtered
hl.embedtypes, hl.semantics, hc.filtered, hc.pathnamehash
FROM {h5p} hc
JOIN {h5p_libraries} hl ON hl.id = hc.mainlibraryid
WHERE hc.id = :h5pid";
@@ -1196,9 +1198,17 @@ class framework implements \H5PFrameworkInterface {
'libraryMinorVersion' => $data->minorversion,
'libraryEmbedTypes' => $data->embedtypes,
'libraryFullscreen' => $data->fullscreen,
'metadata' => ''
'metadata' => '',
'pathnamehash' => $data->pathnamehash
);
$params = json_decode($data->jsoncontent);
if (empty($params->metadata)) {
$params->metadata = new \stdClass();
}
$content['metadata'] = $params->metadata;
$content['params'] = json_encode($params->params ?? $params);
return $content;
}
+26 -9
View File
@@ -24,8 +24,6 @@
namespace core_h5p\local\library;
defined('MOODLE_INTERNAL') || die();
/**
* H5P autoloader management class.
*
@@ -41,13 +39,11 @@ class autoloader {
*/
public static function get_all_handlers(): array {
$handlers = [];
foreach (\core_component::get_plugin_types() as $ptype => $unused) {
$plugins = \core_component::get_plugin_list_with_class($ptype, 'local\library\handler') +
\core_component::get_plugin_list_with_class($ptype, '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;
}
$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;
@@ -115,6 +111,27 @@ class autoloader {
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]);
}
/**
* Register the H5P autoloader.
*/
+32 -2
View File
@@ -65,6 +65,16 @@ abstract class handler {
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.
*/
@@ -83,7 +93,11 @@ abstract class handler {
$classes = static::get_class_list();
if (isset($classes[$classname])) {
require_once($CFG->dirroot . static::get_h5p_core_library_base($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]));
}
}
}
@@ -92,12 +106,23 @@ abstract class handler {
*
* @param string $filepath The path within the h5p root
* @param array $params these params override current params or add new
* @return null|moodle_url
* @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);
}
/**
* Return the list of classes with their location within the joubel directory.
*
@@ -113,6 +138,11 @@ abstract class handler {
'H5PDevelopment' => 'h5p-development.class.php',
'H5PFileStorage' => 'h5p-file-storage.interface.php',
'H5PMetadata' => 'h5p-metadata.class.php',
'H5peditor' => 'h5peditor.class.php',
'H5peditorStorage' => 'h5peditor-storage.interface.php',
'H5PEditorAjaxInterface' => 'h5peditor-ajax.interface.php',
'H5PEditorAjax' => 'h5peditor-ajax.class.php',
'H5peditorFile' => 'h5peditor-file.class.php',
];
}
}
+1
View File
@@ -94,6 +94,7 @@ function core_h5p_pluginfile($course, $cm, $context, string $filearea, array $ar
}
$itemid = array_shift($args);
break;
case \core_h5p\file_storage::EDITOR_FILEAREA:
case \core_h5p\file_storage::CACHED_ASSETS_FILEAREA:
case \core_h5p\file_storage::EXPORT_FILEAREA:
$itemid = 0;
+99
View File
@@ -0,0 +1,99 @@
<?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/>.
/**
* Testing the H5PEditorAjaxInterface interface implementation.
*
* @package core_h5p
* @category test
* @copyright 2020 Victor Deniz <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_h5p;
use core_h5p\local\library\autoloader;
use ReflectionMethod;
/**
*
* Test class covering the H5PEditorAjaxInterface interface implementation.
*
* @package core_h5p
* @copyright 2020 Victor Deniz <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*
* @runTestsInSeparateProcesses
*/
class editor_ajax_testcase extends \advanced_testcase {
/** @var editor_ajax H5P editor ajax instance */
protected $editorajax;
/**
* Set up function for tests.
*/
protected function setUp() {
parent::setUp();
autoloader::register();
$this->editorajax = new editor_ajax();
}
/**
* Test that getLatestLibraryVersions method retrieves the latest installed library versions.
*/
public function test_getLatestLibraryVersions(): void {
$this->resetAfterTest();
$generator = \testing_util::get_data_generator();
$h5pgenerator = $generator->get_plugin_generator('core_h5p');
// Create several libraries records.
$h5pgenerator->create_library_record('Library1', 'Lib1', 2, 0);
$lib2 = $h5pgenerator->create_library_record('Library2', 'Lib2', 2, 1);
$expectedlibraries[] = $lib2->id;
$lib3 = $h5pgenerator->create_library_record('Library3', 'Lib3', 1, 3);
$expectedlibraries[] = $lib3->id;
$h5pgenerator->create_library_record('Library1', 'Lib1', 2, 1);
$lib12 = $h5pgenerator->create_library_record('Library1', 'Lib1', 3, 0);
$expectedlibraries[] = $lib12->id;
$actuallibraries = $this->editorajax->getLatestLibraryVersions();
ksort($actuallibraries);
$this->assertEquals($expectedlibraries, array_keys($actuallibraries));
}
/**
* Test that the method validateEditorToken validates an existing token.
*/
public function test_validateEditorToken(): void {
// The action param is not used at all.
$token = core::createToken('editorajax');
$wrongaction = core::createToken('wrongaction');
$badtoken = 'xkadfpuealkdjsflkajsñf';
$validtoken = $this->editorajax->validateEditorToken($token);
$invalidaction = $this->editorajax->validateEditorToken($wrongaction);
$invalidtoken = $this->editorajax->validateEditorToken($badtoken);
$this->assertTrue($validtoken);
$this->assertTrue($invalidaction);
$this->assertFalse($invalidtoken);
}
}
+99
View File
@@ -0,0 +1,99 @@
<?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/>.
/**
* Testing the H5peditorStorage interface implementation.
*
* @package core_h5p
* @category test
* @copyright 2020 Victor Deniz <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_h5p;
use core_h5p\local\library\autoloader;
/**
*
* Test class covering the H5peditorStorage interface implementation.
*
* @package core_h5p
* @copyright 2020 Victor Deniz <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*
* @runTestsInSeparateProcesses
*/
class editor_framework_testcase extends \advanced_testcase {
/** @var editor_framework H5P editor_framework instance */
protected $editorframework;
/**
* Set up function for tests.
*/
protected function setUp() {
parent::setUp();
autoloader::register();
$this->editorframework = new editor_framework();
}
/**
* Test that the method getLibraries get the specified libraries or all the content types (runnable = 1).
*/
public function test_getLibraries(): void {
$this->resetAfterTest(true);
$generator = \testing_util::get_data_generator();
$h5pgenerator = $generator->get_plugin_generator('core_h5p');
// Generate some h5p related data.
$data = $h5pgenerator->generate_h5p_data();
$expectedlibraries = [];
foreach ($data as $key => $value) {
if (isset($value->data)) {
$value->data->name = $value->data->machinename;
$value->data->majorVersion = $value->data->majorversion;
$value->data->minorVersion = $value->data->minorversion;
$expectedlibraries[$value->data->title] = $value->data;
}
}
ksort($expectedlibraries);
// Get all libraries.
$libraries = $this->editorframework->getLibraries();
foreach ($libraries as $library) {
$actuallibraries[] = $library->title;
}
sort($actuallibraries);
$this->assertEquals(array_keys($expectedlibraries), $actuallibraries);
// Get a subset of libraries.
$librariessubset = array_slice($expectedlibraries, 0, 4);
$actuallibraries = [];
$libraries = $this->editorframework->getLibraries($librariessubset);
foreach ($libraries as $library) {
$actuallibraries[] = $library->title;
}
$this->assertEquals(array_keys($librariessubset), $actuallibraries);
}
}
+9 -1
View File
@@ -1609,9 +1609,17 @@ class framework_testcase extends \advanced_testcase {
'libraryMinorVersion' => $mainlibrary->minorversion,
'libraryEmbedTypes' => $mainlibrary->embedtypes,
'libraryFullscreen' => $mainlibrary->fullscreen,
'metadata' => ''
'metadata' => '',
'pathnamehash' => $h5p->pathnamehash
);
$params = json_decode($h5p->jsoncontent);
if (empty($params->metadata)) {
$params->metadata = new \stdClass();
}
$expected['metadata'] = $params->metadata;
$expected['params'] = json_encode($params->params ?? $params);
// The returned content should match the expected array.
$this->assertEquals($expected, $content);
}
+35
View File
@@ -381,4 +381,39 @@ class core_h5p_generator extends \component_generator_base {
return [$installedtypes, count($typestonotinstall)];
}
/**
* Add a record on files table for a file that belongs to
*
* @param string $file File name and path inside the filearea.
* @param string $filearea The filearea in which the file is ("editor" or "content").
* @param int $contentid Id of the H5P content to which the file belongs. null if the file is in the editor.
*
* @return stored_file;
* @throws coding_exception
*/
public function create_content_file(string $file, string $filearea, int $contentid = 0): stored_file {
$filepath = '/'.dirname($file).'/';
$filename = basename($file);
if (($filearea === 'content') && ($contentid == 0)) {
throw new coding_exception('Files belonging to an H5P content must specify the H5P content id');
}
$content = 'fake content';
$systemcontext = context_system::instance();
$filerecord = array(
'contextid' => $systemcontext->id,
'component' => \core_h5p\file_storage::COMPONENT,
'filearea' => $filearea,
'itemid' => ($filearea === 'editor') ? 0 : $contentid,
'filepath' => $filepath,
'filename' => $filename,
);
$fs = new file_storage();
return $fs->create_file_from_string($filerecord, $content);
}
}
+74
View File
@@ -489,4 +489,78 @@ class generator_testcase extends \advanced_testcase {
]
];
}
/**
* Test the behaviour of create_content_file(). Test whether a file belonging to a content is created.
*
* @dataProvider test_create_content_file_provider
* @param array $filedata Data from the file to be created.
* @param array $expecteddata Data expected.Data from the file to be created.
*/
public function test_create_content_file($filedata, $expecteddata): void {
$this->resetAfterTest();
$generator = self::getDataGenerator()->get_plugin_generator('core_h5p');
if ($expecteddata[1] === 'exception') {
$this->expectException('coding_exception');
}
call_user_func_array([$generator, 'create_content_file'], $filedata);
$systemcontext = \context_system::instance();
$filearea = $filedata[1];
$filepath = '/'. dirname($filedata[0]). '/';
$filename = basename($filedata[0]);
$itemid = $expecteddata[0];
$fs = new \file_storage();
$exists = $fs->file_exists($systemcontext->id, file_storage::COMPONENT, $filearea, $itemid, $filepath,
$filename);
if ($expecteddata[1] === true) {
$this->assertTrue($exists);
} else if ($expecteddata[1] === false) {
$this->assertFalse($exists);
}
}
/**
* Data provider for test_create_content_file(). Data from different files to be created.
*
* @return array
**/
public function test_create_content_file_provider(): array {
return [
'Create file in content with id 4' => [
[
'images/img1.png',
'content',
4
],
[
4,
true
]
],
'Create file in the editor' => [
[
'images/img1.png',
'editor'
],
[
0,
true
]
],
'Create file in content without id' => [
[
'images/img1.png',
'content'
],
[
0,
'exception'
]
]
];
}
}
+233
View File
@@ -29,6 +29,8 @@ use core_h5p\file_storage;
use core_h5p\local\library\autoloader;
use core_h5p\helper;
use file_archive;
use moodle_exception;
use ReflectionMethod;
use zip_archive;
defined('MOODLE_INTERNAL') || die();
@@ -617,4 +619,235 @@ class h5p_file_storage_testcase extends \advanced_testcase {
],
];
}
/**
* Test the private method get_file, a wrapper for getting an H5P content file.
*/
public function test_get_file(): void {
$file = 'img/fake.png';
$h5pcontentid = 3;
// Add a file to a H5P content.
$this->h5p_generator->create_content_file($file, file_storage::CONTENT_FILEAREA, $h5pcontentid);
// Set get_file method accessibility.
$method = new ReflectionMethod(file_storage::class, 'get_file');
$method->setAccessible(true);
$contentfile = $method->invoke(new file_storage(), file_storage::CONTENT_FILEAREA, $h5pcontentid, $file);
// Check that it returns an instance of store_file.
$this->assertInstanceOf('stored_file', $contentfile);
// Add a file to editor.
$this->h5p_generator->create_content_file($file, file_storage::EDITOR_FILEAREA, $h5pcontentid);
$editorfile = $method->invoke(new file_storage(), file_storage::EDITOR_FILEAREA, $h5pcontentid, $file);
// Check that it returns an instance of store_file.
$this->assertInstanceOf('stored_file', $editorfile);
}
/**
* Test that a single file is added to Moodle files.
*/
public function test_move_file(): void {
// Create temp folder.
$tempfolder = make_request_directory(false);
// Create H5P content folder.
$filepath = '/img/';
$filename = 'fake.png';
$h5pcontentfolder = $tempfolder . '/fakeH5Pcontent/content' . $filepath;
if (!check_dir_exists($h5pcontentfolder, true, true)) {
throw new moodle_exception('error_creating_temp_dir', 'error', $h5pcontentfolder);
}
$file = $h5pcontentfolder . $filename;
touch($file);
$h5pcontentid = 3;
// Check the file doesn't exist in Moodle files.
$this->assertFalse($this->h5p_fs_fs->file_exists($this->h5p_fs_context->id, file_storage::COMPONENT,
file_storage::CONTENT_FILEAREA, $h5pcontentid, $filepath, $filename));
// Set get_file method accessibility.
$method = new ReflectionMethod(file_storage::class, 'move_file');
$method->setAccessible(true);
$method->invoke(new file_storage(), $file, $h5pcontentid);
// Check the file exist in Moodle files.
$this->assertTrue($this->h5p_fs_fs->file_exists($this->h5p_fs_context->id, file_storage::COMPONENT,
file_storage::CONTENT_FILEAREA, $h5pcontentid, $filepath, $filename));
}
/**
* Test that a file is copied from another H5P content or the H5P editor.
*
* @return void
*/
public function test_cloneContentFile(): void {
// Upload a file to the editor.
$file = 'images/fake.jpg';
$filepath = '/'.dirname($file).'/';
$filename = basename($file);
$content = 'abcd';
$filerecord = array(
'contextid' => $this->h5p_fs_context->id,
'component' => file_storage::COMPONENT,
'filearea' => file_storage::EDITOR_FILEAREA,
'itemid' => 0,
'filepath' => $filepath,
'filename' => $filename,
);
$this->h5p_fs_fs->create_file_from_string($filerecord, $content);
// Target H5P content, where the file will be cloned.
$targetcontent = new \stdClass();
$targetcontent->id = 999;
// Check the file doesn't exists before cloning.
$this->assertFalse($this->h5p_fs_fs->get_file($this->h5p_fs_context->id, file_storage::COMPONENT,
file_storage::CONTENT_FILEAREA, $targetcontent->id, $filepath, $filename));
// Copy file from the editor.
$this->h5p_file_storage->cloneContentFile($file, 'editor', $targetcontent);
// Check the file exists after cloning.
$this->assertInstanceOf(\stored_file::class, $this->h5p_fs_fs->get_file($this->h5p_fs_context->id, file_storage::COMPONENT,
file_storage::CONTENT_FILEAREA, $targetcontent->id, $filepath, $filename));
// Simulate that an H5P content, with id $sourcecontentid, has a file.
$file = 'images/fake2.jpg';
$filepath = '/'.dirname($file).'/';
$filename = basename($file);
$sourcecontentid = 111;
$filerecord['filearea'] = 'content';
$filerecord['itemid'] = $sourcecontentid;
$filerecord['filepath'] = $filepath;
$filerecord['filename'] = $filename;
$this->h5p_fs_fs->create_file_from_string($filerecord, $content);
// Check the file doesn't exists before cloning.
$this->assertFalse($this->h5p_fs_fs->get_file($this->h5p_fs_context->id, file_storage::COMPONENT,
file_storage::CONTENT_FILEAREA, $targetcontent->id, $filepath, $filename));
// Copy file from another H5P content.
$this->h5p_file_storage->cloneContentFile($file, $sourcecontentid, $targetcontent);
// Check the file exists after cloning.
$this->assertInstanceOf(\stored_file::class, $this->h5p_fs_fs->get_file($this->h5p_fs_context->id, file_storage::COMPONENT,
file_storage::CONTENT_FILEAREA, $targetcontent->id, $filepath, $filename));
}
/**
* Test that a given file exists in an H5P content.
*
* @return void
*/
public function test_getContentFile(): void {
$file = 'img/fake.png';
$contentid = 3;
// Add a file to a H5P content.
$this->h5p_generator->create_content_file($file, file_storage::CONTENT_FILEAREA, $contentid);
// Get an existing file id.
$fileid = $this->h5p_file_storage->getContentFile($file, $contentid);
$this->assertNotNull($fileid);
// Try to get a nonexistent file.
$fileid = $this->h5p_file_storage->getContentFile($file, 5);
$this->assertNull($fileid);
}
/**
* Tests that the content folder of an H5P content is imported in the Moodle filesystem.
*/
public function test_moveContentDiretory(): void {
global $DB;
// Create temp folder.
$tempfolder = make_request_directory(false);
// Create H5P content folder.
$h5pcontentfolder = $tempfolder . '/fakeH5Pcontent';
$contentfolder = $h5pcontentfolder . '/content';
if (!check_dir_exists($contentfolder, true, true)) {
throw new moodle_exception('error_creating_temp_dir', 'error', $contentfolder);
}
// Add content.json file.
touch($contentfolder . 'content.json');
// Create several folders and files inside content folder.
$filesexpected = array();
$numfolders = random_int(2, 5);
for ($numfolder = 1; $numfolder < $numfolders; $numfolder++) {
$foldername = '/folder' . $numfolder;
$newfolder = $contentfolder . $foldername;
if (!check_dir_exists($newfolder, true, true)) {
throw new moodle_exception('error_creating_temp_dir', 'error', $newfolder);
}
$numfiles = random_int(2, 5);
for ($numfile = 1; $numfile < $numfiles; $numfile++) {
$filename = '/file' . $numfile . '.ext';
touch($newfolder . $filename);
$filesexpected[] = $foldername . $filename;
}
}
$targeth5pcontentid = 111;
$this->h5p_file_storage->moveContentDirectory($h5pcontentfolder, $targeth5pcontentid);
// Get database records.
$sql = "SELECT concat(filepath, filename)
FROM {files}
WHERE filearea = :filearea AND itemid = :itemid AND component = :component AND filename != '.'";
$params = [
'component' => file_storage::COMPONENT,
'filearea' => file_storage::CONTENT_FILEAREA,
'itemid' => $targeth5pcontentid
];
$filesdb = $DB->get_fieldset_sql($sql, $params);
sort($filesdb);
// Check that created files match with database records.
$this->assertEquals($filesexpected, $filesdb);
}
/**
* Test that an H5P content file is removed.
*/
public function test_removeContentFile(): void {
$file = 'img/fake.png';
$filepath = '/' . dirname($file) . '/';
$filename = basename($file);
$h5pcontentid = 3;
// Add a file to a H5P content.
$this->h5p_generator->create_content_file($file, file_storage::CONTENT_FILEAREA, $h5pcontentid);
// Check the file exists.
$this->assertTrue($this->h5p_fs_fs->file_exists($this->h5p_fs_context->id, file_storage::COMPONENT,
file_storage::CONTENT_FILEAREA, $h5pcontentid, $filepath, $filename));
$this->h5p_file_storage->removeContentFile($file, $h5pcontentid);
// Check the file doesn't exists.
$this->assertFalse($this->h5p_fs_fs->file_exists($this->h5p_fs_context->id, file_storage::COMPONENT,
file_storage::CONTENT_FILEAREA, $h5pcontentid, $filepath, $filename));
}
}
+1
View File
@@ -151,6 +151,7 @@ $string['noextension'] = 'The file you uploaded is not a valid HTML5 Package. (I
$string['noh5plibhandlerdefined'] = 'There isn\'t any H5P framework handler installed, so H5P content can\'t be displayed.';
$string['nojson'] = 'The main h5p.json file is not valid';
$string['nopermissiontodeploy'] = 'This file can\'t be displayed because it has been uploaded by a user without the required capability to deploy H5P content.';
$string['nopermissiontoedit'] = 'You do not have permission to edit H5P content';
$string['notrustablefile'] = 'This file can\'t be displayed because it has been uploaded by a user without the capability to update H5P content types. Please contact your administrator to ask for the content type to be installed.';
$string['nounzip'] = 'The file you uploaded is not a valid HTML5 Package. (It is not possible to unzip it.)';
$string['offlineDialogBody'] = 'We were unable to send information about your completion of this task. Please check your internet connection.';