diff --git a/h5p/classes/autoloader.php b/h5p/classes/autoloader.php index 9088ac2b2fd..3e8daea83ef 100644 --- a/h5p/classes/autoloader.php +++ b/h5p/classes/autoloader.php @@ -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', diff --git a/h5p/classes/core.php b/h5p/classes/core.php index 37021611473..2bbb73dab1a 100644 --- a/h5p/classes/core.php +++ b/h5p/classes/core.php @@ -24,13 +24,14 @@ 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; + } } diff --git a/h5p/tests/generator/lib.php b/h5p/tests/generator/lib.php index 22cdb54b80c..26ba067de82 100644 --- a/h5p/tests/generator/lib.php +++ b/h5p/tests/generator/lib.php @@ -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 index 00000000000..4477f1bb90a --- /dev/null +++ b/h5p/tests/h5p_core_test.php @@ -0,0 +1,152 @@ +. + +/** + * Testing the H5P core methods. + * + * @package core_h5p + * @category test + * @copyright 2019 Victor Deniz + * @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 + * @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); + } +} diff --git a/lang/en/admin.php b/lang/en/admin.php index 02427a6c97d..602cb1aa5ed 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -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'; diff --git a/lang/en/h5p.php b/lang/en/h5p.php index 93101bb8323..5f34212d0a6 100644 --- a/lang/en/h5p.php +++ b/lang/en/h5p.php @@ -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 index 00000000000..e41d40286bb --- /dev/null +++ b/lib/classes/task/h5p_get_content_types_task.php @@ -0,0 +1,64 @@ +. + +/** + * Task to get the latest content types from the official H5P repository. + * + * @package core + * @copyright 2019 Victor Deniz + * @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 + * @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"); + } + } +} diff --git a/lib/db/tasks.php b/lib/db/tasks.php index 14c1b600713..171d0a5b87e 100644 --- a/lib/db/tasks.php +++ b/lib/db/tasks.php @@ -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 index 00000000000..29d6390ef31 --- /dev/null +++ b/lib/tests/h5p_get_content_types_task_test.php @@ -0,0 +1,65 @@ +. + +/** + * Unit tests for the task that fetch the latest version of H5P content types. + * + * @package core + * @copyright 2019 Victor Deniz + * @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 + * @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/'); + } +} diff --git a/version.php b/version.php index 767843c84f1..7f8e2798614 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2019110500.02; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2019110500.03; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.