From 6da050d72aae2b7ba0fd5d21e012ff5154609a23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20D=C3=A9niz=20Falc=C3=B3n?= Date: Sun, 12 Apr 2020 10:50:39 +0100 Subject: [PATCH] MDL-67814 core_h5p: implemented H5P Core/Editor library interfaces --- h5p/ajax.php | 115 +++++++++++ h5p/classes/api.php | 71 +++++++ h5p/classes/core.php | 21 ++ h5p/classes/editor_ajax.php | 116 +++++++++++ h5p/classes/editor_framework.php | 185 ++++++++++++++++++ h5p/classes/factory.php | 37 +++- h5p/classes/file_storage.php | 206 +++++++++++++++++--- h5p/classes/framework.php | 14 +- h5p/classes/local/library/autoloader.php | 35 +++- h5p/classes/local/library/handler.php | 34 +++- h5p/lib.php | 1 + h5p/tests/editor_ajax_test.php | 99 ++++++++++ h5p/tests/editor_framework_test.php | 99 ++++++++++ h5p/tests/framework_test.php | 10 +- h5p/tests/generator/lib.php | 35 ++++ h5p/tests/generator_test.php | 74 +++++++ h5p/tests/h5p_file_storage_test.php | 233 +++++++++++++++++++++++ lang/en/h5p.php | 1 + 18 files changed, 1345 insertions(+), 41 deletions(-) create mode 100644 h5p/ajax.php create mode 100644 h5p/classes/editor_ajax.php create mode 100644 h5p/classes/editor_framework.php create mode 100644 h5p/tests/editor_ajax_test.php create mode 100644 h5p/tests/editor_framework_test.php diff --git a/h5p/ajax.php b/h5p/ajax.php new file mode 100644 index 00000000000..88e8e17d606 --- /dev/null +++ b/h5p/ajax.php @@ -0,0 +1,115 @@ +. + +/** + * Responsible for handling AJAX requests related to H5P. + * + * @package core_h5p + * @copyright 2020 Victor Deniz , 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; +} diff --git a/h5p/classes/api.php b/h5p/classes/api.php index fdd5b29328d..983619d2a83 100644 --- a/h5p/classes/api.php +++ b/h5p/classes/api.php @@ -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; + } } diff --git a/h5p/classes/core.php b/h5p/classes/core.php index 6b0c3a4c24c..e76d6b21e6a 100644 --- a/h5p/classes/core.php +++ b/h5p/classes/core.php @@ -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); + } } diff --git a/h5p/classes/editor_ajax.php b/h5p/classes/editor_ajax.php new file mode 100644 index 00000000000..dc1291b730d --- /dev/null +++ b/h5p/classes/editor_ajax.php @@ -0,0 +1,116 @@ +. + +/** + * Class \core_h5p\editor_ajax + * + * @package core_h5p + * @copyright 2020 Victor Deniz , 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 , 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 " . + * @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 []; + } +} diff --git a/h5p/classes/editor_framework.php b/h5p/classes/editor_framework.php new file mode 100644 index 00000000000..c3eb18ca77b --- /dev/null +++ b/h5p/classes/editor_framework.php @@ -0,0 +1,185 @@ +. + +/** + * Class \core_h5p\editor_framework + * + * @package core_h5p + * @copyright 2020 Victor Deniz , 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 , 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. + } +} diff --git a/h5p/classes/factory.php b/h5p/classes/factory.php index f1151799c96..9d6908a5625 100644 --- a/h5p/classes/factory.php +++ b/h5p/classes/factory.php @@ -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; + } } diff --git a/h5p/classes/file_storage.php b/h5p/classes/file_storage.php index ee2f215d03b..675645c40a0 100644 --- a/h5p/classes/file_storage.php +++ b/h5p/classes/file_storage.php @@ -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); + } } diff --git a/h5p/classes/framework.php b/h5p/classes/framework.php index e6a13edc6e8..286f16fa9a3 100644 --- a/h5p/classes/framework.php +++ b/h5p/classes/framework.php @@ -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; } diff --git a/h5p/classes/local/library/autoloader.php b/h5p/classes/local/library/autoloader.php index 76ef5c92fe2..e046c2a06aa 100644 --- a/h5p/classes/local/library/autoloader.php +++ b/h5p/classes/local/library/autoloader.php @@ -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. */ diff --git a/h5p/classes/local/library/handler.php b/h5p/classes/local/library/handler.php index 9bb48649424..ca59674934b 100644 --- a/h5p/classes/local/library/handler.php +++ b/h5p/classes/local/library/handler.php @@ -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', ]; } } diff --git a/h5p/lib.php b/h5p/lib.php index b640925fde9..025c58bed7f 100644 --- a/h5p/lib.php +++ b/h5p/lib.php @@ -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; diff --git a/h5p/tests/editor_ajax_test.php b/h5p/tests/editor_ajax_test.php new file mode 100644 index 00000000000..573289a62e2 --- /dev/null +++ b/h5p/tests/editor_ajax_test.php @@ -0,0 +1,99 @@ +. + +/** + * Testing the H5PEditorAjaxInterface interface implementation. + * + * @package core_h5p + * @category test + * @copyright 2020 Victor Deniz + * @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 + * @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); + } +} diff --git a/h5p/tests/editor_framework_test.php b/h5p/tests/editor_framework_test.php new file mode 100644 index 00000000000..27f485eeccb --- /dev/null +++ b/h5p/tests/editor_framework_test.php @@ -0,0 +1,99 @@ +. + +/** + * Testing the H5peditorStorage interface implementation. + * + * @package core_h5p + * @category test + * @copyright 2020 Victor Deniz + * @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 + * @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); + } +} diff --git a/h5p/tests/framework_test.php b/h5p/tests/framework_test.php index 8f72d80b548..de4be46e66a 100644 --- a/h5p/tests/framework_test.php +++ b/h5p/tests/framework_test.php @@ -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); } diff --git a/h5p/tests/generator/lib.php b/h5p/tests/generator/lib.php index 5d7ee9f547c..c23babafdc9 100644 --- a/h5p/tests/generator/lib.php +++ b/h5p/tests/generator/lib.php @@ -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); + } } diff --git a/h5p/tests/generator_test.php b/h5p/tests/generator_test.php index 60fd74fbd40..478ab57d5ed 100644 --- a/h5p/tests/generator_test.php +++ b/h5p/tests/generator_test.php @@ -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' + ] + ] + ]; + } } diff --git a/h5p/tests/h5p_file_storage_test.php b/h5p/tests/h5p_file_storage_test.php index fa83984b541..d94d395831b 100644 --- a/h5p/tests/h5p_file_storage_test.php +++ b/h5p/tests/h5p_file_storage_test.php @@ -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)); + } } diff --git a/lang/en/h5p.php b/lang/en/h5p.php index c1a44946295..c7235eb7450 100644 --- a/lang/en/h5p.php +++ b/lang/en/h5p.php @@ -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.';