From 5a266a579c5c41736bb7ae05f3162ea841635c98 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Tue, 29 Oct 2019 20:19:33 +0100 Subject: [PATCH 1/5] MDL-67057 core_h5p: Add helper class --- h5p/classes/helper.php | 185 +++++++++++++++ h5p/tests/fixtures/greeting-card-887.h5p | Bin 0 -> 35659 bytes h5p/tests/helper_test.php | 286 +++++++++++++++++++++++ 3 files changed, 471 insertions(+) create mode 100644 h5p/classes/helper.php create mode 100644 h5p/tests/fixtures/greeting-card-887.h5p create mode 100644 h5p/tests/helper_test.php diff --git a/h5p/classes/helper.php b/h5p/classes/helper.php new file mode 100644 index 00000000000..7d50a27a314 --- /dev/null +++ b/h5p/classes/helper.php @@ -0,0 +1,185 @@ +. + +/** + * Contains helper class for the H5P area. + * + * @package core_h5p + * @copyright 2019 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_h5p; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Helper class for the H5P area. + * + * @copyright 2019 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class helper { + + /** + * Store an H5P file. + * + * @param factory $factory The \core_h5p\factory object + * @param stored_file $file Moodle file instance + * @param stdClass $config Button options config + * @param bool $onlyupdatelibs Whether new libraries can be installed or only the existing ones can be updated + * @param bool $skipcontent Should the content be skipped (so only the libraries will be saved)? + * + * @return int|false|null The H5P identifier or null if there is an error when saving or false if it's not a valid H5P package + */ + public static function save_h5p(factory $factory, \stored_file $file, \stdClass $config, bool $onlyupdatelibs = false, + bool $skipcontent = false) { + // This may take a long time. + \core_php_time_limit::raise(); + + $core = $factory->get_core(); + $path = $core->fs->getTmpPath(); + $core->h5pF->getUploadedH5pFolderPath($path); + // Add manually the extension to the file to avoid the validation fails. + $path .= '.h5p'; + $core->h5pF->getUploadedH5pPath($path); + + // Copy the .h5p file to the temporary folder. + $file->copy_content_to($path); + + // Check if the h5p file is valid before saving it. + $h5pvalidator = $factory->get_validator(); + if ($h5pvalidator->isValidPackage($skipcontent, $onlyupdatelibs)) { + $h5pstorage = $factory->get_storage(); + + $content = [ + 'pathnamehash' => $file->get_pathnamehash(), + 'contenthash' => $file->get_contenthash(), + ]; + $options = ['disable' => self::get_display_options($core, $config)]; + + $h5pstorage->savePackage($content, null, $skipcontent, $options); + + return $h5pstorage->contentId; + } + + return false; + } + + + /** + * Get the representation of display options as int. + * + * @param core $core The \core_h5p\core object + * @param stdClass $config Button options config + * + * @return int The representation of display options as int + */ + public static function get_display_options(core $core, \stdClass $config): int { + $export = isset($config->export) ? $config->export : 0; + $embed = isset($config->embed) ? $config->embed : 0; + $copyright = isset($config->copyright) ? $config->copyright : 0; + $frame = ($export || $embed || $copyright); + if (!$frame) { + $frame = isset($config->frame) ? $config->frame : 0; + } + + $disableoptions = [ + core::DISPLAY_OPTION_FRAME => $frame, + core::DISPLAY_OPTION_DOWNLOAD => $export, + core::DISPLAY_OPTION_EMBED => $embed, + core::DISPLAY_OPTION_COPYRIGHT => $copyright, + ]; + + return $core->getStorableDisplayOptions($disableoptions, 0); + } + + /** + * Checks if the author of the .h5p file is "trustable". If the file hasn't been uploaded by a user with the + * required capability, the content won't be deployed. + * + * @param stored_file $file The .h5p file to be deployed + * @return bool Returns true if the file can be deployed, false otherwise. + */ + public static function can_deploy_package(\stored_file $file): bool { + if (is_null($file)) { + debugging('\core_h5p\h5p::can_deploy_package() now expects a \'file\' to be passed.', + DEBUG_DEVELOPER); + return false; + } + + $context = \context::instance_by_id($file->get_contextid()); + if (has_capability('moodle/h5p:deploy', $context, $file->get_userid())) { + return true; + } + + return false; + } + + /** + * Checks if the content-type libraries can be upgraded. + * The H5P content-type libraries can only be upgraded if the author of the .h5p file can manage content-types or if all the + * content-types exist, to avoid users without the required capability to upload malicious content. + * + * @param stored_file $file The .h5p file to be deployed + * @return bool Returns true if the content-type libraries can be created/updated, false otherwise. + */ + public static function can_update_library(\stored_file $file): bool { + if (is_null($file)) { + debugging('\core_h5p\h5p::can_update_library() now expects a \'file\' to be passed.', + DEBUG_DEVELOPER); + return false; + } + + $context = \context::instance_by_id($file->get_contextid()); + // Check if the owner of the .h5p file has the capability to manage content-types. + if (has_capability('moodle/h5p:updatelibraries', $context, $file->get_userid())) { + return true; + } + + return false; + } + + /** + * Convenience to take a fixture test file and create a stored_file. + * + * @param string $filepath The filepath of the file + * @param int $userid The author of the file + * @param \context $context The context where the file will be created + * @return stored_file The file created + */ + public static function create_fake_stored_file_from_path(string $filepath, int $userid = 0, + \context $context = null): \stored_file { + if (is_null($context)) { + $context = \context_system::instance(); + } + $filerecord = [ + 'contextid' => $context->id, + 'component' => 'core_h5p', + 'filearea' => 'unittest', + 'itemid' => rand(), + 'filepath' => '/', + 'filename' => basename($filepath), + ]; + if (!is_null($userid)) { + $filerecord['userid'] = $userid; + } + + $fs = get_file_storage(); + return $fs->create_file_from_pathname($filerecord, $filepath); + } + +} diff --git a/h5p/tests/fixtures/greeting-card-887.h5p b/h5p/tests/fixtures/greeting-card-887.h5p new file mode 100644 index 0000000000000000000000000000000000000000..40d3093ed4e350f0bad9c95898bdbc9d03038351 GIT binary patch literal 35659 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 literal 0 HcmV?d00001 diff --git a/h5p/tests/helper_test.php b/h5p/tests/helper_test.php new file mode 100644 index 00000000000..932375720af --- /dev/null +++ b/h5p/tests/helper_test.php @@ -0,0 +1,286 @@ +. + +/** + * Testing the H5P helper. + * + * @package core_h5p + * @category test + * @copyright 2019 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +declare(strict_types = 1); + +namespace core_h5p; + +use advanced_testcase; + +/** + * Test class covering the H5P helper. + * + * @package core_h5p + * @copyright 2019 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class helper_testcase extends \advanced_testcase { + + /** + * Test the behaviour of get_display_options(). + * + * @dataProvider get_display_options_provider + * @param bool $frame Whether the frame should be displayed or not + * @param bool $export Whether the export action button should be displayed or not + * @param bool $embed Whether the embed action button should be displayed or not + * @param bool $copyright Whether the copyright action button should be displayed or not + * @param int $expected The expectation with the displayoptions value + */ + public function test_get_display_options(bool $frame, bool $export, bool $embed, bool $copyright, int $expected): void { + $this->setRunTestInSeparateProcess(true); + $this->resetAfterTest(); + + $factory = new \core_h5p\factory(); + $core = $factory->get_core(); + $config = (object)[ + 'frame' => $frame, + 'export' => $export, + 'embed' => $embed, + 'copyright' => $copyright, + ]; + $displayoptions = helper::get_display_options($core, $config); + + $this->assertEquals($expected, $displayoptions); + } + + /** + * Data provider for test_get_display_options(). + * + * @return array + */ + public function get_display_options_provider(): array { + return [ + 'All display options disabled' => [ + false, + false, + false, + false, + 15, + ], + 'All display options enabled' => [ + true, + true, + true, + true, + 0, + ], + 'Frame disabled and the rest enabled' => [ + false, + true, + true, + true, + 0, + ], + 'Only export enabled' => [ + false, + true, + false, + false, + 12, + ], + 'Only embed enabled' => [ + false, + false, + true, + false, + 10, + ], + 'Only copyright enabled' => [ + false, + false, + false, + true, + 6, + ], + ]; + } + + /** + * Test the behaviour of save_h5p() when there are some missing libraries in the system. + * @runInSeparateProcess + */ + public function test_save_h5p_missing_libraries(): void { + $this->resetAfterTest(); + $factory = new \core_h5p\factory(); + + // Create a user. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + // This is a valid .H5P file. + $path = __DIR__ . '/fixtures/greeting-card-887.h5p'; + $file = helper::create_fake_stored_file_from_path($path, (int)$user->id); + $factory->get_framework()->set_file($file); + + $config = (object)[ + 'frame' => 1, + 'export' => 1, + 'embed' => 0, + 'copyright' => 0, + ]; + + // There are some missing libraries in the system, so an error should be returned. + $h5pid = helper::save_h5p($factory, $file, $config); + $this->assertFalse($h5pid); + $errors = $factory->get_framework()->getMessages('error'); + $this->assertCount(1, $errors); + $error = reset($errors); + $this->assertEquals('missing-required-library', $error->code); + $this->assertEquals('Missing required library H5P.GreetingCard 1.0', $error->message); + } + + /** + * Test the behaviour of save_h5p() when the libraries exist in the system. + * @runInSeparateProcess + */ + public function test_save_h5p_existing_libraries(): void { + global $DB; + + $this->resetAfterTest(); + $factory = new \core_h5p\factory(); + + // Create a user. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + // This is a valid .H5P file. + $path = __DIR__ . '/fixtures/greeting-card-887.h5p'; + $file = helper::create_fake_stored_file_from_path($path, (int)$user->id); + $factory->get_framework()->set_file($file); + + $config = (object)[ + 'frame' => 1, + 'export' => 1, + 'embed' => 0, + 'copyright' => 0, + ]; + // The required libraries exist in the system before saving the .h5p file. + $generator = $this->getDataGenerator()->get_plugin_generator('core_h5p'); + $lib = $generator->create_library_record('H5P.GreetingCard', 'GreetingCard', 1, 0); + $h5pid = helper::save_h5p($factory, $file, $config); + $this->assertNotEmpty($h5pid); + + // No errors are raised. + $errors = $factory->get_framework()->getMessages('error'); + $this->assertCount(0, $errors); + + // And the content in the .h5p file has been saved as expected. + $h5p = $DB->get_record('h5p', ['id' => $h5pid]); + $this->assertEquals($lib->id, $h5p->mainlibraryid); + $this->assertEquals(helper::get_display_options($factory->get_core(), $config), $h5p->displayoptions); + $this->assertContains('Hello world!', $h5p->jsoncontent); + } + + /** + * Test the behaviour of save_h5p() when the .h5p file is invalid. + * @runInSeparateProcess + */ + public function test_save_h5p_invalid_file(): void { + $this->resetAfterTest(); + $factory = new \core_h5p\factory(); + + // Create a user. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + // Prepare an invalid .H5P file. + $path = __DIR__ . '/fixtures/h5ptest.zip'; + $file = helper::create_fake_stored_file_from_path($path, (int)$user->id); + $factory->get_framework()->set_file($file); + $config = (object)[ + 'frame' => 1, + 'export' => 1, + 'embed' => 0, + 'copyright' => 0, + ]; + + // When saving an invalid .h5p file, an error should be raised. + $h5pid = helper::save_h5p($factory, $file, $config); + $this->assertFalse($h5pid); + $errors = $factory->get_framework()->getMessages('error'); + $this->assertCount(2, $errors); + + $expectederrorcodes = ['invalid-content-folder', 'invalid-h5p-json-file']; + foreach ($errors as $error) { + $this->assertContains($error->code, $expectederrorcodes); + } + } + + /** + * Test the behaviour of can_deploy_package(). + */ + public function test_can_deploy_package(): void { + $this->resetAfterTest(); + $factory = new \core_h5p\factory(); + + // Create a user. + $user = $this->getDataGenerator()->create_user(); + $admin = get_admin(); + + // Prepare a valid .H5P file. + $path = __DIR__ . '/fixtures/greeting-card-887.h5p'; + + // Files created by users can't be deployed. + $file = helper::create_fake_stored_file_from_path($path, (int)$user->id); + $factory->get_framework()->set_file($file); + $candeploy = helper::can_deploy_package($file); + $this->assertFalse($candeploy); + + // Files created by admins can be deployed, even when the current user is not the admin. + $this->setUser($user); + $file = helper::create_fake_stored_file_from_path($path, (int)$admin->id); + $factory->get_framework()->set_file($file); + $candeploy = helper::can_deploy_package($file); + $this->assertTrue($candeploy); + } + + /** + * Test the behaviour of can_update_library(). + */ + public function can_update_library(): void { + $this->resetAfterTest(); + $factory = new \core_h5p\factory(); + + // Create a user. + $user = $this->getDataGenerator()->create_user(); + $admin = get_admin(); + + // Prepare a valid .H5P file. + $path = __DIR__ . '/fixtures/greeting-card-887.h5p'; + + // Libraries can't be updated when the file has been created by users. + $file = helper::create_fake_stored_file_from_path($path, (int)$user->id); + $factory->get_framework()->set_file($file); + $candeploy = helper::can_update_library($file); + $this->assertFalse($candeploy); + + // Libraries can be updated when the file has been created by admin, even when the current user is not the admin. + $this->setUser($user); + $file = helper::create_fake_stored_file_from_path($path, (int)$admin->id); + $factory->get_framework()->set_file($file); + $candeploy = helper::can_update_library($file); + $this->assertTrue($candeploy); + } +} From 285976f1bdf490b1c42e4e0150687bfc5bb54fc4 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Wed, 30 Oct 2019 08:32:50 +0100 Subject: [PATCH 2/5] MDL-67057 core_h5p: Improve displaying errors --- h5p/embed.php | 24 ++++++++++++------------ h5p/templates/h5perror.mustache | 13 ++++++++++--- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/h5p/embed.php b/h5p/embed.php index af236293bea..f62e324b1e3 100644 --- a/h5p/embed.php +++ b/h5p/embed.php @@ -61,15 +61,18 @@ if (empty($messages->error) && empty($messages->exception)) { // Add H5P assets to the page. $h5pplayer->add_assets_to_page(); - // Check if there is some error when adding assets to the page. + // Print page HTML. + echo $OUTPUT->header(); + + // Check if some error has been raised when adding assets to the page. If that's the case, display them above the H5P content. $messages = $h5pplayer->get_messages(); - if (empty($messages->error) && empty($messages->exception)) { - - // Print page HTML. - echo $OUTPUT->header(); - - echo $h5pplayer->output(); + if (!empty($messages->exception) || !empty($messages->error)) { + $messages->h5picon = new \moodle_url('/h5p/pix/icon.svg'); + echo $OUTPUT->render_from_template('core_h5p/h5perror', $messages); } + + // Display the H5P content. + echo $h5pplayer->output(); } else { // If there is any error or exception when creating the player, it should be displayed. $PAGE->set_context(context_system::instance()); @@ -80,12 +83,9 @@ if (empty($messages->error) && empty($messages->exception)) { $PAGE->add_body_class('h5p-embed'); $PAGE->set_pagelayout('embedded'); - // Errors can't be printed yet, because some more errors might been added while preparing the output -} - -if (!empty($messages->error) || !empty($messages->exception)) { - // Print all the errors. echo $OUTPUT->header(); + + // Print all the errors. $messages->h5picon = new \moodle_url('/h5p/pix/icon.svg'); echo $OUTPUT->render_from_template('core_h5p/h5perror', $messages); } diff --git a/h5p/templates/h5perror.mustache b/h5p/templates/h5perror.mustache index 9c0d9d9c313..76107c97b2a 100644 --- a/h5p/templates/h5perror.mustache +++ b/h5p/templates/h5perror.mustache @@ -21,7 +21,7 @@ Variables required for this template: * h5picon - The icon - * message - The error message to display. + * message - The error messages to display. Example context (json): { @@ -33,7 +33,9 @@
- {{#str}}h5p, core_h5p{{/str}} + {{#h5picon}} + {{#str}}h5p, core_h5p{{/str}} + {{/h5picon}}
@@ -42,9 +44,14 @@ {{{ exception }}}
{{/exception}} + {{#info}} + + {{/info}} {{#error}} {{/error}}
From 7a0d9bd5e46a26efc6d711a8983250d7c60fb630 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Wed, 30 Oct 2019 08:45:24 +0100 Subject: [PATCH 3/5] MDL-67057 core_h5p: New method to get latest library version --- h5p/classes/framework.php | 34 +++++++++++++++- h5p/tests/framework_test.php | 79 ++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 2 deletions(-) diff --git a/h5p/classes/framework.php b/h5p/classes/framework.php index f043e939937..3831f274dc0 100644 --- a/h5p/classes/framework.php +++ b/h5p/classes/framework.php @@ -757,7 +757,19 @@ class framework implements \H5PFrameworkInterface { $content['contenthash'] = ''; } - $data = array( + // If the libraryid declared in the package is empty, get the latest version. + if (empty($content['library']['libraryId'])) { + $mainlibrary = $this->get_latest_library_version($content['library']['machineName']); + if (empty($mainlibrary)) { + // Raise an error if the main library is not defined and the latest version doesn't exist. + $message = $this->t('Missing required library @library', ['@library' => $content['library']['machineName']]); + $this->setErrorMessage($message, 'missing-required-library'); + return false; + } + $content['library']['libraryId'] = $mainlibrary->id; + } + + $data = [ 'jsoncontent' => $content['params'], 'displayoptions' => $content['disable'], 'mainlibraryid' => $content['library']['libraryId'], @@ -765,7 +777,7 @@ class framework implements \H5PFrameworkInterface { 'filtered' => null, 'pathnamehash' => $content['pathnamehash'], 'contenthash' => $content['contenthash'] - ); + ]; if (!isset($content['id'])) { $data['timecreated'] = $data['timemodified']; @@ -1585,4 +1597,22 @@ class framework implements \H5PFrameworkInterface { } return ''; } + + /** + * Get the latest library version. + * + * @param string $machinename The library's machine name + * @return stdClass|null An object with the latest library version + */ + public function get_latest_library_version(string $machinename): ?\stdClass { + global $DB; + + $libraries = $DB->get_records('h5p_libraries', ['machinename' => $machinename], + 'majorversion DESC, minorversion DESC, patchversion DESC', '*', 0, 1); + if ($libraries) { + return reset($libraries); + } + + return null; + } } diff --git a/h5p/tests/framework_test.php b/h5p/tests/framework_test.php index 0eb5ee01b1a..8cef8457b45 100644 --- a/h5p/tests/framework_test.php +++ b/h5p/tests/framework_test.php @@ -845,6 +845,41 @@ class framework_testcase extends \advanced_testcase { $this->assertEquals($content['disable'], $dbcontent->displayoptions); } + /** + * Test the behaviour of insertContent(). + */ + public function test_insertContent_latestlibrary() { + global $DB; + + $this->resetAfterTest(); + + $generator = $this->getDataGenerator()->get_plugin_generator('core_h5p'); + // Create a library record. + $lib = $generator->create_library_record('TestLibrary', 'Test', 1, 1, 2); + + $content = array( + 'params' => json_encode(['param1' => 'Test']), + 'library' => array( + 'libraryId' => 0, + 'machineName' => 'TestLibrary', + ), + 'disable' => 8 + ); + + // Insert h5p content. + $contentid = $this->framework->insertContent($content); + + // Get the entered content from the db. + $dbcontent = $DB->get_record('h5p', ['id' => $contentid]); + + // Make sure the h5p content was properly inserted. + $this->assertNotEmpty($dbcontent); + $this->assertEquals($content['params'], $dbcontent->jsoncontent); + $this->assertEquals($content['disable'], $dbcontent->displayoptions); + // As the libraryId was empty, the latest library has been used. + $this->assertEquals($lib->id, $dbcontent->mainlibraryid); + } + /** * Test the behaviour of updateContent(). */ @@ -2014,4 +2049,48 @@ class framework_testcase extends \advanced_testcase { ] ]; } + + + /** + * Test the behaviour of get_latest_library_version(). + */ + public function test_get_latest_library_version() { + global $DB; + + $this->resetAfterTest(); + + $generator = $this->getDataGenerator()->get_plugin_generator('core_h5p'); + // Create a library record. + $machinename = 'TestLibrary'; + $lib1 = $generator->create_library_record($machinename, 'Test', 1, 1, 2); + $lib2 = $generator->create_library_record($machinename, 'Test', 1, 2, 1); + + $content = array( + 'params' => json_encode(['param1' => 'Test']), + 'library' => array( + 'libraryId' => 0, + 'machineName' => 'TestLibrary', + ), + 'disable' => 8 + ); + + // Get the latest id (at this point, should be lib2). + $latestlib = $this->framework->get_latest_library_version($machinename); + $this->assertEquals($lib2->id, $latestlib->id); + + // Get the latest id (at this point, should be lib3). + $lib3 = $generator->create_library_record($machinename, 'Test', 2, 1, 0); + $latestlib = $this->framework->get_latest_library_version($machinename); + $this->assertEquals($lib3->id, $latestlib->id); + + // Get the latest id (at this point, should be still lib3). + $lib4 = $generator->create_library_record($machinename, 'Test', 1, 1, 3); + $latestlib = $this->framework->get_latest_library_version($machinename); + $this->assertEquals($lib3->id, $latestlib->id); + + // Get the latest id (at this point, should be lib5). + $lib5 = $generator->create_library_record($machinename, 'Test', 2, 1, 6); + $latestlib = $this->framework->get_latest_library_version($machinename); + $this->assertEquals($lib5->id, $latestlib->id); + } } From c07f31ae2c1d0ce9f1c0d8aaf191dced60ac3bd6 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Wed, 30 Oct 2019 09:07:51 +0100 Subject: [PATCH 4/5] MDL-67057 core_h5p: Add capability to update content-type libraries --- h5p/classes/framework.php | 28 ++++++++- h5p/classes/player.php | 108 +++++++++++++++++++---------------- h5p/tests/framework_test.php | 101 +++++++++++++++++++++++++++++++- lang/en/h5p.php | 2 + lang/en/role.php | 3 +- lib/db/access.php | 10 ++++ version.php | 2 +- 7 files changed, 199 insertions(+), 55 deletions(-) diff --git a/h5p/classes/framework.php b/h5p/classes/framework.php index 3831f274dc0..e5293cf2c3b 100644 --- a/h5p/classes/framework.php +++ b/h5p/classes/framework.php @@ -41,6 +41,9 @@ class framework implements \H5PFrameworkInterface { /** @var string The path to the last uploaded h5p file */ private $lastuploadedfile; + /** @var stored_file The .h5p file */ + private $file; + /** * Returns info for the current platform. * Implements getPlatformInfo. @@ -634,8 +637,29 @@ class framework implements \H5PFrameworkInterface { * FALSE if the user is not allowed to update libraries. */ public function mayUpdateLibraries() { - // Currently, capabilities are not being set/used, so everyone can update libraries. - return true; + return helper::can_update_library($this->get_file()); + } + + /** + * Get the .h5p file. + * + * @return stored_file The .h5p file. + */ + public function get_file(): \stored_file { + if (!isset($this->file)) { + throw new \coding_exception('Using get_file() before file is set'); + } + + return $this->file; + } + + /** + * Set the .h5p file. + * + * @param stored_file $file The .h5p file. + */ + public function set_file(\stored_file $file): void { + $this->file = $file; } /** diff --git a/h5p/classes/player.php b/h5p/classes/player.php index 5605a972481..224e0bf411c 100644 --- a/h5p/classes/player.php +++ b/h5p/classes/player.php @@ -76,10 +76,15 @@ class player { private $context; /** - * @var context The \core_h5p\factory object. + * @var factory The \core_h5p\factory object. */ private $factory; + /** + * @var stdClass The error, exception and info messages, raised while preparing and running the player. + */ + private $messages; + /** * Inits the H5P player for rendering the content. * @@ -94,6 +99,8 @@ class player { $this->factory = new \core_h5p\factory(); + $this->messages = new \stdClass(); + // Create \core_h5p\core instance. $this->core = $this->factory->get_core(); @@ -113,13 +120,20 @@ class player { * @return stdClass with framework error messages. */ public function get_messages() : \stdClass { - $messages = new \stdClass(); - $messages->error = $this->core->h5pF->getMessages('error'); - - if (empty($messages->error)) { - $messages->error = false; + // Check if there are some errors and store them in $messages. + if (empty($this->messages->error)) { + $this->messages->error = $this->core->h5pF->getMessages('error') ?: false; + } else { + $this->messages->error = array_merge($this->messages->error, $this->core->h5pF->getMessages('error')); } - return $messages; + + if (empty($this->messages->info)) { + $this->messages->info = $this->core->h5pF->getMessages('info') ?: false; + } else { + $this->messages->info = array_merge($this->messages->info, $this->core->h5pF->getMessages('info')); + } + + return $this->messages; } /** @@ -214,7 +228,7 @@ class player { * @return int|false H5P DB identifier. */ private function get_h5p_id(string $url, \stdClass $config) { - global $DB; + global $DB, $USER; $fs = get_file_storage(); @@ -255,13 +269,48 @@ class player { // Check if the user uploading the H5P content is "trustable". If the file hasn't been uploaded by a user with this // capability, the content won't be deployed and an error message will be displayed. - if (!has_capability('moodle/h5p:deploy', $this->context, $file->get_userid())) { + if (!helper::can_deploy_package($file)) { $this->core->h5pF->setErrorMessage(get_string('nopermissiontodeploy', 'core_h5p')); return false; } + // The H5P content can be only deployed if the author of the .h5p file can update libraries or if all the + // content-type libraries exist, to avoid users without the h5p:updatelibraries capability upload malicious content. + $onlyupdatelibs = !helper::can_update_library($file); + + // Set the .h5p file, in order to check later the permissions to update libraries. + $this->core->h5pF->set_file($file); + // Validate and store the H5P content before displaying it. - return $this->save_h5p($file, $config); + $h5pid = helper::save_h5p($this->factory, $file, $config, $onlyupdatelibs, false); + if (!$h5pid && $file->get_userid() != $USER->id && has_capability('moodle/h5p:updatelibraries', $this->context)) { + // The user has permission to update libraries but the package has been uploaded by a different + // user without this permission. Check if there is some missing required library error. + $missingliberror = false; + $messages = $this->get_messages(); + if (!empty($messages->error)) { + foreach ($messages->error as $error) { + if ($error->code == 'missing-required-library') { + $missingliberror = true; + break; + } + } + } + if ($missingliberror) { + // The message about the permissions to upload libraries should be removed. + $infomsg = "Note that the libraries may exist in the file you uploaded, but you're not allowed to upload " . + "new libraries. Contact the site administrator about this."; + if (($key = array_search($infomsg, $messages->info)) !== false) { + unset($messages->info[$key]); + } + + // No library will be installed and an error will be displayed, because this content is not trustable. + $this->core->h5pF->setInfoMessage(get_string('notrustablefile', 'core_h5p')); + } + return false; + + } + return $h5pid; } } @@ -362,45 +411,6 @@ class player { return $fs->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename); } - /** - * Store an H5P file - * - * @param stored_file $file Moodle file instance - * @param stdClass $config Button options config. - * - * @return int|false The H5P identifier or false if it's not a valid H5P package. - */ - private function save_h5p($file, \stdClass $config) : int { - // This may take a long time. - \core_php_time_limit::raise(); - - $path = $this->core->fs->getTmpPath(); - $this->core->h5pF->getUploadedH5pFolderPath($path); - // Add manually the extension to the file to avoid the validation fails. - $path .= '.h5p'; - $this->core->h5pF->getUploadedH5pPath($path); - - // Copy the .h5p file to the temporary folder. - $file->copy_content_to($path); - - // Check if the h5p file is valid before saving it. - $h5pvalidator = $this->factory->get_validator(); - if ($h5pvalidator->isValidPackage(false, false)) { - $h5pstorage = $this->factory->get_storage(); - - $options = ['disable' => $this->get_display_options($config)]; - $content = [ - 'pathnamehash' => $file->get_pathnamehash(), - 'contenthash' => $file->get_contenthash(), - ]; - - $h5pstorage->savePackage($content, null, false, $options); - return $h5pstorage->contentId; - } - - return false; - } - /** * Get the representation of display options as int. * @param stdClass $config Button options config. diff --git a/h5p/tests/framework_test.php b/h5p/tests/framework_test.php index 8cef8457b45..555dbba02cf 100644 --- a/h5p/tests/framework_test.php +++ b/h5p/tests/framework_test.php @@ -702,10 +702,107 @@ class framework_testcase extends \advanced_testcase { /** * Test the behaviour of mayUpdateLibraries(). */ - public function test_mayUpdateLibraries() { - $mayupdatelib = $this->framework->mayUpdateLibraries(); + public function test_mayUpdateLibraries(): void { + global $DB; + $this->resetAfterTest(); + + // Create some users. + $contextsys = \context_system::instance(); + $user = $this->getDataGenerator()->create_user(); + $admin = get_admin(); + $managerrole = $DB->get_record('role', ['shortname' => 'manager'], '*', MUST_EXIST); + $studentrole = $DB->get_record('role', ['shortname' => 'student'], '*', MUST_EXIST); + $manager = $this->getDataGenerator()->create_user(); + role_assign($managerrole->id, $manager->id, $contextsys); + + // Create a course with a label and enrol the user. + $course = $this->getDataGenerator()->create_course(); + $label = $this->getDataGenerator()->create_module('label', ['course' => $course->id]); + list(, $labelcm) = get_course_and_cm_from_instance($label->id, 'label'); + $contextlabel = \context_module::instance($labelcm->id); + $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); + + // Create the .h5p file. + $path = __DIR__ . '/fixtures/h5ptest.zip'; + + // Admin and manager should have permission to update libraries. + $file = helper::create_fake_stored_file_from_path($path, $admin->id, $contextsys); + $this->framework->set_file($file); + $mayupdatelib = $this->framework->mayUpdateLibraries(); $this->assertTrue($mayupdatelib); + + $file = helper::create_fake_stored_file_from_path($path, $manager->id, $contextsys); + $this->framework->set_file($file); + $mayupdatelib = $this->framework->mayUpdateLibraries(); + $this->assertTrue($mayupdatelib); + + // By default, normal user hasn't permission to update libraries (in both contexts, system and module label). + $file = helper::create_fake_stored_file_from_path($path, $user->id, $contextsys); + $this->framework->set_file($file); + $mayupdatelib = $this->framework->mayUpdateLibraries(); + $this->assertFalse($mayupdatelib); + + $file = helper::create_fake_stored_file_from_path($path, $user->id, $contextlabel); + $this->framework->set_file($file); + $mayupdatelib = $this->framework->mayUpdateLibraries(); + $this->assertFalse($mayupdatelib); + + // If the current user (admin) can update libraries, the method should return true (even if the file userid hasn't the + // required capabilility in the file context). + $file = helper::create_fake_stored_file_from_path($path, $admin->id, $contextlabel); + $this->framework->set_file($file); + $mayupdatelib = $this->framework->mayUpdateLibraries(); + $this->assertTrue($mayupdatelib); + + // If the update capability is assigned to the user, they should be able to update the libraries (only in the context + // where the capability has been assigned). + $file = helper::create_fake_stored_file_from_path($path, $user->id, $contextlabel); + $this->framework->set_file($file); + $mayupdatelib = $this->framework->mayUpdateLibraries(); + $this->assertFalse($mayupdatelib); + assign_capability('moodle/h5p:updatelibraries', CAP_ALLOW, $studentrole->id, $contextlabel); + $mayupdatelib = $this->framework->mayUpdateLibraries(); + $this->assertTrue($mayupdatelib); + $file = helper::create_fake_stored_file_from_path($path, $user->id, $contextsys); + $this->framework->set_file($file); + $mayupdatelib = $this->framework->mayUpdateLibraries(); + $this->assertFalse($mayupdatelib); + } + + /** + * Test the behaviour of get_file() and set_file(). + */ + public function test_get_file(): void { + $this->resetAfterTest(); + + // Create some users. + $contextsys = \context_system::instance(); + $user = $this->getDataGenerator()->create_user(); + + // The H5P file. + $path = __DIR__ . '/fixtures/h5ptest.zip'; + + // An error should be raised when it's called before initialitzing it. + $this->expectException('coding_exception'); + $this->expectExceptionMessage('Using get_file() before file is set'); + $this->framework->get_file(); + + // Check the value when only path and user are set. + $file = helper::create_fake_stored_file_from_path($path, $user->id); + $this->framework->set_file($file); + $file = $this->framework->get_file(); + $this->assertEquals($user->id, $$file->get_userid()); + $this->assertEquals($contextsys->id, $file->get_contextid()); + + // Check the value when also the context is set. + $course = $this->getDataGenerator()->create_course(); + $contextcourse = \context_course::instance($course->id); + $file = helper::create_fake_stored_file_from_path($path, $user->id, $contextcourse); + $this->framework->set_file($file); + $file = $this->framework->get_file(); + $this->assertEquals($user->id, $$file->get_userid()); + $this->assertEquals($contextcourse->id, $file->get_contextid()); } /** diff --git a/lang/en/h5p.php b/lang/en/h5p.php index a65724f2e55..d873142aeaf 100644 --- a/lang/en/h5p.php +++ b/lang/en/h5p.php @@ -125,6 +125,8 @@ $string['nocopyright'] = 'No copyright information available for this content.'; $string['noextension'] = 'The file you uploaded is not a valid HTML5 Package (It does not have the .h5p file extension)'; $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['nojson'] = 'The main h5p.json file is not valid'; +$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 (We are unable to unzip it)'; $string['offlineDialogBody'] = 'We were unable to send information about your completion of this task. Please check your internet connection.'; $string['offlineDialogHeader'] = 'Your connection to the server was lost'; diff --git a/lang/en/role.php b/lang/en/role.php index d9ec365eb29..72edb507613 100644 --- a/lang/en/role.php +++ b/lang/en/role.php @@ -261,6 +261,7 @@ $string['grade:view'] = 'View own grades'; $string['grade:viewall'] = 'View grades of other users'; $string['grade:viewhidden'] = 'View hidden grades for owner'; $string['h5p:deploy'] = 'Deploy H5P content'; +$string['h5p:updatelibraries'] = 'Manage H5P content types'; $string['h5p:setdisplayoptions'] = 'Set H5P display options'; $string['highlightedcellsshowdefault'] = 'The permissions highlighted in the table below are the defaults for the role archetype currently selected above.'; $string['highlightedcellsshowinherit'] = 'The highlighted cells in the table below show the permission (if any) that will be inherited. Apart from the capabilities whose permission you actually want to alter, you should leave everything set to Inherit.'; @@ -496,4 +497,4 @@ $string['privacy:metadata:role_cohortroles'] = 'Roles to cohort'; $string['course:togglecompletion'] = 'Manually mark activities as complete'; // Deprecated since Moodle 3.8. -$string['eventrolecapabilitiesupdated'] = 'Role capabilities updated'; \ No newline at end of file +$string['eventrolecapabilitiesupdated'] = 'Role capabilities updated'; diff --git a/lib/db/access.php b/lib/db/access.php index 9b7668e7c8d..ee6ee0906bc 100644 --- a/lib/db/access.php +++ b/lib/db/access.php @@ -2450,4 +2450,14 @@ $capabilities = array( 'editingteacher' => CAP_ALLOW, ) ), + + // Allow to update H5P content-type libraries. + 'moodle/h5p:updatelibraries' => [ + 'riskbitmask' => RISK_XSS, + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'manager' => CAP_ALLOW, + ] + ], ); diff --git a/version.php b/version.php index 38541a31ef4..3177b09c3fb 100644 --- a/version.php +++ b/version.php @@ -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. From 021c180c83da630866bfd0b55ddc2ea526aa27a2 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 7 Nov 2019 09:13:25 +0800 Subject: [PATCH 5/5] MDL-67057 h5p: Remove unreachable code --- h5p/classes/helper.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/h5p/classes/helper.php b/h5p/classes/helper.php index 7d50a27a314..de3d85700e6 100644 --- a/h5p/classes/helper.php +++ b/h5p/classes/helper.php @@ -115,12 +115,6 @@ class helper { * @return bool Returns true if the file can be deployed, false otherwise. */ public static function can_deploy_package(\stored_file $file): bool { - if (is_null($file)) { - debugging('\core_h5p\h5p::can_deploy_package() now expects a \'file\' to be passed.', - DEBUG_DEVELOPER); - return false; - } - $context = \context::instance_by_id($file->get_contextid()); if (has_capability('moodle/h5p:deploy', $context, $file->get_userid())) { return true;