MDL-67814 core_h5p: implemented H5P Core/Editor library interfaces
authorVíctor Déniz Falcón <victor@moodle.com>
Sun, 12 Apr 2020 09:50:39 +0000 (10:50 +0100)
committerAdrian Greeve <abgreeve@gmail.com>
Thu, 16 Apr 2020 03:29:35 +0000 (11:29 +0800)
18 files changed:
h5p/ajax.php [new file with mode: 0644]
h5p/classes/api.php
h5p/classes/core.php
h5p/classes/editor_ajax.php [new file with mode: 0644]
h5p/classes/editor_framework.php [new file with mode: 0644]
h5p/classes/factory.php
h5p/classes/file_storage.php
h5p/classes/framework.php
h5p/classes/local/library/autoloader.php
h5p/classes/local/library/handler.php
h5p/lib.php
h5p/tests/editor_ajax_test.php [new file with mode: 0644]
h5p/tests/editor_framework_test.php [new file with mode: 0644]
h5p/tests/framework_test.php
h5p/tests/generator/lib.php
h5p/tests/generator_test.php
h5p/tests/h5p_file_storage_test.php
lang/en/h5p.php

diff --git a/h5p/ajax.php b/h5p/ajax.php
new file mode 100644 (file)
index 0000000..88e8e17
--- /dev/null
@@ -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 <victor@moodle.com>, 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;
+}
index fdd5b29..983619d 100644 (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;
+    }
 }
index 6b0c3a4..e76d6b2 100644 (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);
+    }
 }
diff --git a/h5p/classes/editor_ajax.php b/h5p/classes/editor_ajax.php
new file mode 100644 (file)
index 0000000..dc1291b
--- /dev/null
@@ -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 <victor@moodle.com>, base on code by Joubel AS
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_h5p;
+
+use 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 <victor@moodle.com>, base on code by Joubel AS
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class editor_ajax implements H5PEditorAjaxInterface {
+
+    /** The component for H5P. */
+    public const EDITOR_AJAX_TOKEN = 'editorajax';
+
+    /**
+     * Gets latest library versions that exists locally
+     *
+     * @return array Latest version of all local libraries
+     */
+    public function getLatestLibraryVersions(): array {
+        global $DB;
+
+        $sql = "SELECT hl2.id, hl2.machinename as machine_name, hl2.title, hl2.majorversion as major_version,
+                       hl2.minorversion AS minor_version, hl2.patchversion as patch_version, '' as has_icon, 0 as restricted
+                  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 [];
+    }
+}
diff --git a/h5p/classes/editor_framework.php b/h5p/classes/editor_framework.php
new file mode 100644 (file)
index 0000000..c3eb18c
--- /dev/null
@@ -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 <victor@moodle.com>, base on code by Joubel AS
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_h5p;
+
+use H5peditorStorage;
+use stdClass;
+
+/**
+ * Moodle's implementation of the H5P Editor storage interface.
+ *
+ * Makes it possible for the editor's core library to communicate with the
+ * database used by Moodle.
+ *
+ * @package    core_h5p
+ * @copyright  2020 Victor Deniz <victor@moodle.com>, base on code by Joubel AS
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class editor_framework implements H5peditorStorage {
+
+    /**
+     * 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.
+    }
+}
index f115179..9d6908a 100644 (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;
+    }
 }
index ee2f215..675645c 100644 (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);
+    }
 }
index e6a13ed..286f16f 100644 (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;
     }
 
index 76ef5c9..e046c2a 100644 (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.
      */
index 9bb4864..ca59674 100644 (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',
         ];
     }
 }
index b640925..025c58b 100644 (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;
diff --git a/h5p/tests/editor_ajax_test.php b/h5p/tests/editor_ajax_test.php
new file mode 100644 (file)
index 0000000..573289a
--- /dev/null
@@ -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 <victor@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_h5p;
+
+use core_h5p\local\library\autoloader;
+use ReflectionMethod;
+
+/**
+ *
+ * Test class covering the H5PEditorAjaxInterface interface implementation.
+ *
+ * @package    core_h5p
+ * @copyright  2020 Victor Deniz <victor@moodle.com>
+ * @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 (file)
index 0000000..27f485e
--- /dev/null
@@ -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 <victor@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_h5p;
+
+use core_h5p\local\library\autoloader;
+
+/**
+ *
+ * Test class covering the H5peditorStorage interface implementation.
+ *
+ * @package    core_h5p
+ * @copyright  2020 Victor Deniz <victor@moodle.com>
+ * @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);
+    }
+}
index 8f72d80..de4be46 100644 (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);
     }
index 5d7ee9f..c23baba 100644 (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);
+    }
 }
index 60fd74f..478ab57 100644 (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'
+                ]
+            ]
+        ];
+    }
 }
index fa83984..d94d395 100644 (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));
+    }
 }
index c1a4494..c7235eb 100644 (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.';