MDL-67058 core_h5p: task to get latest content types version from H5P
authorVictor Deniz Falcon <victor@moodle.com>
Wed, 6 Nov 2019 18:17:34 +0000 (18:17 +0000)
committerVictor Deniz Falcon <victor@moodle.com>
Wed, 6 Nov 2019 18:17:34 +0000 (18:17 +0000)
h5p/classes/autoloader.php
h5p/classes/core.php
h5p/tests/generator/lib.php
h5p/tests/h5p_core_test.php [new file with mode: 0644]
lang/en/admin.php
lang/en/h5p.php
lib/classes/task/h5p_get_content_types_task.php [new file with mode: 0644]
lib/db/tasks.php
lib/tests/h5p_get_content_types_task_test.php [new file with mode: 0644]
version.php

index 9088ac2..3e8daea 100644 (file)
@@ -43,6 +43,7 @@ class autoloader {
 
         $classes = [
             'H5PCore' => '/lib/h5p/h5p.classes.php',
+            'H5PHubEndpoints' => '/lib/h5p/h5p.classes.php',
             'H5PFrameworkInterface' => '/lib/h5p/h5p.classes.php',
             'H5PContentValidator' => 'lib/h5p/h5p.classes.php',
             'H5PValidator' => '/lib/h5p/h5p.classes.php',
index 3702161..2bbb73d 100644 (file)
 
 namespace core_h5p;
 
+defined('MOODLE_INTERNAL') || die();
+
+require_once("$CFG->libdir/filelib.php");
 use H5PCore;
 use H5PFrameworkInterface;
 use stdClass;
 use moodle_url;
 
-defined('MOODLE_INTERNAL') || die();
-
 /**
  * H5P core class, containing functions and storage shared by the other H5P classes.
  *
@@ -153,4 +154,141 @@ class core extends \H5PCore {
 
         return $urls;
     }
+
+    /**
+     * Fetch and install the latest H5P content types libraries from the official H5P repository.
+     * If the latest version of a content type library is present in the system, nothing is done for that content type.
+     *
+     * @return stdClass
+     */
+    public function fetch_latest_content_types(): ?\stdClass {
+
+        $contenttypes = self::get_latest_content_types();
+        if (!empty($contenttypes->error)) {
+            return $contenttypes;
+        }
+
+        $typesinstalled = [];
+
+        foreach ($contenttypes->contentTypes as $type) {
+            $library = [
+                'machineName' => $type->id,
+                'majorVersion' => $type->version->major,
+                'minorVersion' => $type->version->minor,
+                'patchVersion' => $type->version->patch,
+            ];
+
+            $factory = new \core_h5p\factory();
+            $framework = $factory->get_framework();
+
+            $shoulddownload = true;
+            if ($framework->getLibraryId($type->id, $type->version->major, $type->version->minor)) {
+                if (!$framework->isPatchedLibrary($library)) {
+                    $shoulddownload = false;
+                }
+            }
+
+            if ($shoulddownload) {
+                $installed['id'] = $this->fetch_content_type($library);
+                if ($installed['id']) {
+                    $installed['name'] = $librarykey = \H5PCore::libraryToString($library);
+                    $typesinstalled[] = $installed;
+                }
+            }
+        }
+
+        $result = new stdClass();
+        $result->error = '';
+        $result->typesinstalled = $typesinstalled;
+
+        return $result;
+    }
+
+    /**
+     * Given an H5P content type machine name, fetch and install the required library from the official H5P repository.
+     *
+     * @param array $library Library machineName, majorversion and minorversion.
+     * @return int|null Returns the id of the content type library installed, null otherwise.
+     */
+    public function fetch_content_type(array $library): ?int {
+
+        $factory = new \core_h5p\factory();
+        $framework = $factory->get_framework();
+
+        // Get a temp path to download the content type.
+        $temppath = make_request_directory();
+        $tempfile = "{$temppath}/" . $library['machineName'] . ".h5p";
+
+        // Download the latest content type from the H5P official repository.
+        $endpoint = $this->get_api_endpoint($library['machineName']);
+        $result = download_file_content(
+            $endpoint,
+            null,
+            null,
+            true,
+            300,
+            20,
+            false,
+            $tempfile
+        );
+
+        if (!empty($result->error) || $result->status == '404') {
+            return null;
+        }
+
+        $framework->getUploadedH5pPath($tempfile);
+        $framework->getUploadedH5pFolderPath($temppath);
+
+        $validator = $factory->get_validator();
+
+        // Check if the h5p file is valid before saving it.
+        if ($validator->isValidPackage(false, false)) {
+            $h5pstorage = $factory->get_storage();
+            $h5pstorage->savePackage([], null, true);
+            $librarykey = \H5PCore::libraryToString($library);
+            return $h5pstorage->h5pC->librariesJsonData[$librarykey]["libraryId"];
+        }
+
+        return null;
+    }
+
+    /**
+     * Get H5P endpoints.
+     *
+     * If $library is null, moodle_url is the endpoint of the latest version of the H5P content types. If library is the
+     * machine name of a content type, moodle_url is the endpoint to download the content type.
+     *
+     * @param string|null $library The machineName of the library whose endpoint is requested.
+     * @return moodle_url The endpoint moodle_url object.
+     */
+    public function get_api_endpoint(?string $library): moodle_url {
+        $h5purl = \H5PHubEndpoints::createURL(\H5PHubEndpoints::CONTENT_TYPES ) . $library;
+        return new moodle_url($h5purl);
+    }
+
+    /**
+     * Get the latest version of the H5P content types available in the official repository.
+     *
+     * @return stdClass An object with 2 properties:
+     *     - string error: error message when there is any problem, empty otherwise
+     *     - array contentTypes: an object for each H5P content type with its information
+     */
+    public function get_latest_content_types(): \stdClass {
+        // Get the latest content-types json.
+        $postdata = ['uuid' => 'foo'];
+        $endpoint = $this->get_api_endpoint(null);
+        $request = download_file_content($endpoint, null, $postdata, true);
+
+        if (!empty($request->error) || $request->status != '200' || empty($request->results)) {
+            if (empty($request->error)) {
+                $request->error = get_string('fetchtypesfailure', 'core_h5p');
+            }
+            return $request;
+        }
+
+        $contenttypes = json_decode($request->results);
+        $contenttypes->error = '';
+
+        return $contenttypes;
+    }
 }
index 22cdb54..26ba067 100644 (file)
@@ -23,6 +23,8 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+use core_h5p\factory;
+
 defined('MOODLE_INTERNAL') || die();
 
 /**
@@ -339,4 +341,42 @@ class core_h5p_generator extends \component_generator_base {
             )
         );
     }
+
+    /**
+     * Create content type records in the h5p_libraries database table.
+     *
+     * @param int $pending Number of content types not installed
+     * @return array Data of the content types not installed.
+     */
+    public function create_content_types(int $pending): array {
+        global $DB;
+
+        $factory = new factory();
+        $core = $factory->get_core();
+
+        // Get info of latest content types versions.
+        $contenttypes = $core->get_latest_content_types()->contentTypes;
+
+        $size = count($contenttypes) - $pending;
+
+        // Avoid to install 2 content types.
+        $chunks = array_chunk($contenttypes, $size);
+
+        $contenttypes = $chunks[0];
+        $pendingtypes = $chunks[1];
+
+        // Fake installation of all other H5P content types.
+        foreach ($contenttypes as $contenttype) {
+            $library = [
+                'machinename' => $contenttype->id,
+                'majorversion' => $contenttype->version->major,
+                'minorversion' => $contenttype->version->minor,
+                'patchversion' => $contenttype->version->patch,
+                'runnable' => 1
+            ];
+            $DB->insert_record('h5p_libraries', (object) $library);
+        }
+
+        return [$contenttypes, $pendingtypes];
+    }
 }
diff --git a/h5p/tests/h5p_core_test.php b/h5p/tests/h5p_core_test.php
new file mode 100644 (file)
index 0000000..4477f1b
--- /dev/null
@@ -0,0 +1,152 @@
+<?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 H5P core methods.
+ *
+ * @package    core_h5p
+ * @category   test
+ * @copyright  2019 Victor Deniz <victor@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_h5p\local\tests;
+
+use core_h5p\factory;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Test class covering the H5PFileStorage interface implementation.
+ *
+ * @package    core_h5p
+ * @copyright  2019 Victor Deniz <victor@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ *
+ * @runTestsInSeparateProcesses
+ */
+class h5p_core_test extends \advanced_testcase {
+
+    protected function setup() {
+        parent::setUp();
+
+        $factory = new factory();
+        $this->core = $factory->get_core();
+    }
+
+    /**
+     * Check that given an H5P content type machine name, the required library are fetched and installed from the official H5P
+     * repository.
+     *
+     * This test require access to an external URL (H5P libraries repository), so can take long time to execute.
+     * PHPUNIT_LONGTEST constant should be set in phpunit.xml or directly in config.php.
+     * define('PHPUNIT_LONGTEST', true);
+     *
+     * return void
+     */
+    public function test_fetch_content_type(): void {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        if (!defined('PHPUNIT_LONGTEST')) {
+            $this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
+        }
+
+        $library = [
+                'machineName' => 'H5P.Accordion',
+                'majorVersion' => 1,
+                'minorVersion' => 0,
+                'patchVersion' => 0,
+        ];
+
+        $sql = 'SELECT count(id)
+                  FROM {files}
+                 WHERE ' . $DB->sql_like('filepath', ':filepath');
+        $params['filepath'] = "/{$library['machineName']}-%";
+
+        $contentfiles = $DB->count_records_sql($sql, $params);
+
+        $this->assertEquals(0, $contentfiles);
+
+        $this->core->fetch_content_type($library);
+
+        $contentfiles = $DB->count_records_sql($sql, $params);
+        $this->assertGreaterThan(0, $contentfiles);
+    }
+
+    /**
+     * Test that latest version of non installed H5P content type libraries are fetched and installed from the
+     * official H5P repository. To speed up the test, only if checked that one content type is installed.
+     *
+     * This test require access to an external URL (H5P libraries repository), so can take long time to execute.
+     * PHPUNIT_LONGTEST constant should be set in phpunit.xml or directly in config.php.
+     * define('PHPUNIT_LONGTEST', true);
+     *
+     * return void
+     */
+    public function test_fetch_latest_content_types(): void {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        if (!defined('PHPUNIT_LONGTEST')) {
+            $this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
+        }
+
+        $contentfiles = $DB->count_records('h5p_libraries');
+
+        // Initially there are no h5p records in database.
+        $this->assertEquals(0, $contentfiles);
+
+        // Fetch generator.
+        $generator = \testing_util::get_data_generator();
+        $h5pgenerator = $generator->get_plugin_generator('core_h5p');
+
+        // Get info of latest content types versions.
+        [$contenttypes, $contenttoinstall] = $h5pgenerator->create_content_types(1);
+        // Number of H5P content types.
+        $numcontenttypes = count($contenttypes) + count($contenttoinstall);
+
+        $contenttoinstall = $contenttoinstall[0];
+
+        // Content type libraries has runnable set to 1.
+        $conditions = ['runnable' => 1];
+        $contentfiles = $DB->get_records('h5p_libraries', $conditions, '', 'machinename');
+
+        // There is a record for each installed content type, except the one that was hold for later.
+        $this->assertEquals($numcontenttypes - 1, count($contentfiles));
+        $this->assertArrayNotHasKey($contenttoinstall->id, $contentfiles);
+
+        $result = $this->core->fetch_latest_content_types();
+
+        $contentfiles = $DB->get_records('h5p_libraries', $conditions, '', 'machinename');
+
+        // There is a new record for the new installed content type.
+        $this->assertCount($numcontenttypes, $contentfiles);
+        $this->assertArrayHasKey($contenttoinstall->id, $contentfiles);
+        $this->assertCount(1, $result->typesinstalled);
+        $this->assertStringStartsWith($contenttoinstall->id, $result->typesinstalled[0]['name']);
+
+        // New execution doesn't install any content type.
+        $result = $this->core->fetch_latest_content_types();
+
+        $contentfiles = $DB->get_records('h5p_libraries', $conditions, '', 'machinename');
+
+        $this->assertEquals($numcontenttypes, count($contentfiles));
+        $this->assertCount(0, $result->typesinstalled);
+    }
+}
index 02427a6..602cb1a 100644 (file)
@@ -647,6 +647,7 @@ $string['change'] = 'change';
 $string['checkboxno'] = 'No';
 $string['checkboxyes'] = 'Yes';
 $string['choosefiletoedit'] = 'Choose file to edit';
+$string['h5pgetcontenttypestask'] = 'Download available H5P content types from h5p.org';
 $string['iconvrequired'] = 'Installing ICONV extension is required.';
 $string['ignore'] = 'Ignore';
 $string['includemoduleuserdata'] = 'Include module user data';
index a65724f..7352fe8 100644 (file)
@@ -71,6 +71,7 @@ $string['downloadtitle'] = 'Download this content as a H5P file.';
 $string['editor'] = 'Editor';
 $string['embed'] = 'Embed';
 $string['embedtitle'] = 'View the embed code for this content.';
+$string['fetchtypesfailure'] = 'No information could be obtained on the H5P content types available. H5P repository connection failure';
 $string['fileExceedsMaxSize'] = 'One of the files inside the package exceeds the maximum file size allowed. ({$a->%file} {$a->%used} > {$a->%max})';
 $string['fullscreen'] = 'Fullscreen';
 $string['gpl'] = 'General Public License v3';
diff --git a/lib/classes/task/h5p_get_content_types_task.php b/lib/classes/task/h5p_get_content_types_task.php
new file mode 100644 (file)
index 0000000..e41d402
--- /dev/null
@@ -0,0 +1,64 @@
+<?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/>.
+
+/**
+ * Task to get the latest content types from the official H5P repository.
+ *
+ * @package    core
+ * @copyright  2019 Victor Deniz <victor@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core\task;
+
+use core_h5p\factory;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A task to get the latest content types from the official H5P repository.
+ *
+ * @copyright  2019 Victor Deniz <victor@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class h5p_get_content_types_task extends scheduled_task {
+
+    /**
+     * Get a descriptive name for this task (shown to admins).
+     *
+     * @return string
+     */
+    public function get_name() {
+        return get_string('h5pgetcontenttypestask', 'admin');
+    }
+
+    /**
+     * Execute the task.
+     */
+    public function execute() {
+
+        $factory = new factory();
+        $core = $factory->get_core();
+
+        $result = $core->fetch_latest_content_types();
+
+        if (!empty($result->error)) {
+            mtrace($result->error);
+        } else {
+            $numtypesinstalled = count($result->typesinstalled);
+            mtrace("{$numtypesinstalled} new content types installed");
+        }
+    }
+}
index 14c1b60..171d0a5 100644 (file)
@@ -392,4 +392,13 @@ $tasks = array(
         'dayofweek' => '*',
         'month' => '*'
     ),
+    array(
+        'classname' => 'core\task\h5p_get_content_types_task',
+        'blocking' => 0,
+        'minute' => 'R',
+        'hour' => 'R',
+        'day' => '1',
+        'dayofweek' => '*',
+        'month' => '*'
+    ),
 );
diff --git a/lib/tests/h5p_get_content_types_task_test.php b/lib/tests/h5p_get_content_types_task_test.php
new file mode 100644 (file)
index 0000000..29d6390
--- /dev/null
@@ -0,0 +1,65 @@
+<?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/>.
+
+/**
+ * Unit tests for the task that fetch the latest version of H5P content types.
+ *
+ * @package   core
+ * @copyright  2019 Victor Deniz <victor@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class containing unit tests for the task that fetch the latest version of H5P content types.
+ *
+ * @package   core
+ * @copyright  2019 Victor Deniz <victor@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ *
+ * @runTestsInSeparateProcesses
+ */
+class h5p_get_content_types_task_test extends advanced_testcase {
+
+    /**
+     * Test task execution
+     *
+     * This test require access to an external URL (H5P libraries repository), so can take long time to execute.
+     * PHPUNIT_LONGTEST constant should be set in phpunit.xml or directly in config.php.
+     * define('PHPUNIT_LONGTEST', true);
+     *
+     * return void
+     */
+    public function test_task_execution(): void {
+
+        if (!defined('PHPUNIT_LONGTEST')) {
+            $this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
+        }
+
+        $this->resetAfterTest();
+
+        // Fetch generator.
+        $generator = \testing_util::get_data_generator();
+        $h5pgenerator = $generator->get_plugin_generator('core_h5p');
+
+        $h5pgenerator->create_content_types(2);
+
+        $task = new \core\task\h5p_get_content_types_task();
+        $task->execute();
+        $this->expectOutputRegex('/2 new content types/');
+    }
+}
index 38541a3..3177b09 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2019110500.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2019110500.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.