diff --git a/h5p/classes/api.php b/h5p/classes/api.php index 81c76256512..6cd3484fc61 100644 --- a/h5p/classes/api.php +++ b/h5p/classes/api.php @@ -502,4 +502,43 @@ class api { return ($h5p) ? $h5p : null; } + + /** + * Return the H5P export information file when the file has been deployed. + * Otherwise, return null if H5P file: + * i) has not been deployed. + * ii) has changed the content. + * + * The information returned will be: + * - filename, filepath, mimetype, filesize, timemodified and fileurl. + * + * @param int $contextid ContextId of the H5P activity. + * @param factory $factory The \core_h5p\factory object. + * @param string $component component + * @param string $filearea file area + * @return array|null Return file info otherwise null. + */ + public static function get_export_info_from_context_id(int $contextid, + factory $factory, + string $component, + string $filearea): ?array { + + $core = $factory->get_core(); + $fs = get_file_storage(); + $files = $fs->get_area_files($contextid, $component, $filearea, 0, 'id', false); + $file = reset($files); + + if ($h5p = self::get_content_from_pathnamehash($file->get_pathnamehash())) { + if ($h5p->contenthash == $file->get_contenthash()) { + $content = $core->loadContent($h5p->id); + $slug = $content['slug'] ? $content['slug'] . '-' : ''; + $filename = "{$slug}{$content['id']}.h5p"; + $deployedfile = helper::get_export_info($filename, null, $factory); + + return $deployedfile; + } + } + + return null; + } } diff --git a/h5p/classes/helper.php b/h5p/classes/helper.php index 93780fa8665..8e85ffc99eb 100644 --- a/h5p/classes/helper.php +++ b/h5p/classes/helper.php @@ -429,4 +429,50 @@ class helper { return $strings; } + + /** + * Get the information related to the H5P export file. + * The information returned will be: + * - filename, filepath, mimetype, filesize, timemodified and fileurl. + * + * @param string $exportfilename The H5P export filename (with slug). + * @param \moodle_url $url The URL of the exported file. + * @param factory $factory The \core_h5p\factory object + * @return array|null The information export file otherwise null. + */ + public static function get_export_info(string $exportfilename, \moodle_url $url = null, ?factory $factory = null): ?array { + + if (!$factory) { + $factory = new factory(); + } + $core = $factory->get_core(); + + // Get export file. + if (!$fileh5p = $core->fs->get_export_file($exportfilename)) { + return null; + } + + // Build the export info array. + $file = []; + $file['filename'] = $fileh5p->get_filename(); + $file['filepath'] = $fileh5p->get_filepath(); + $file['mimetype'] = $fileh5p->get_mimetype(); + $file['filesize'] = $fileh5p->get_filesize(); + $file['timemodified'] = $fileh5p->get_timemodified(); + + if (!$url) { + $url = \moodle_url::make_webservice_pluginfile_url( + $fileh5p->get_contextid(), + $fileh5p->get_component(), + $fileh5p->get_filearea(), + '', + '', + $fileh5p->get_filename() + ); + } + + $file['fileurl'] = $url->out(false); + + return $file; + } } diff --git a/h5p/classes/player.php b/h5p/classes/player.php index 23c542010a6..9cbc12107bf 100644 --- a/h5p/classes/player.php +++ b/h5p/classes/player.php @@ -456,7 +456,7 @@ class player { } /** - * Return the export file for Mobile App. + * Return the info export file for Mobile App. * * @return array */ @@ -467,23 +467,8 @@ class player { $path = $exporturl->out_as_local_url(); $parts = explode('/', $path); $filename = array_pop($parts); - // Get the the export file. - $systemcontext = \context_system::instance(); - $fs = get_file_storage(); - $fileh5p = $fs->get_file($systemcontext->id, - \core_h5p\file_storage::COMPONENT, - \core_h5p\file_storage::EXPORT_FILEAREA, - 0, - '/', - $filename); - // Get the options that the Mobile App needs. - $file = []; - $file['filename'] = $fileh5p->get_filename(); - $file['filepath'] = $fileh5p->get_filepath(); - $file['mimetype'] = $fileh5p->get_mimetype(); - $file['filesize'] = $fileh5p->get_filesize(); - $file['timemodified'] = $fileh5p->get_timemodified(); - $file['fileurl'] = $exporturl->out(false); + // Get the required info from the export file to be able to get the export file by third apps. + $file = helper::get_export_info($filename, $exporturl); return $file; } diff --git a/h5p/tests/api_test.php b/h5p/tests/api_test.php index f7f01b88a03..e1ec76e8350 100644 --- a/h5p/tests/api_test.php +++ b/h5p/tests/api_test.php @@ -451,4 +451,56 @@ class api_testcase extends \advanced_testcase { api::delete_content_from_pluginfile_url($url->out(), $factory); $this->assertEquals(0, $DB->count_records('h5p')); } + + /** + * Test the behaviour of get_export_info_from_context_id(). + */ + public function test_get_export_info_from_context_id(): void { + global $DB; + + $this->setRunTestInSeparateProcess(true); + $this->resetAfterTest(); + $factory = new factory(); + + // Create the H5P data. + $filename = 'find-the-words.h5p'; + $syscontext = \context_system::instance(); + + // Test scenario 1: H5P exists and deployed. + $generator = $this->getDataGenerator()->get_plugin_generator('core_h5p'); + $fakeexportfile = $generator->create_export_file($filename, + $syscontext->id, + \core_h5p\file_storage::COMPONENT, + \core_h5p\file_storage::EXPORT_FILEAREA); + + $exportfile = api::get_export_info_from_context_id($syscontext->id, + $factory, + \core_h5p\file_storage::COMPONENT, + \core_h5p\file_storage::EXPORT_FILEAREA); + $this->assertEquals($fakeexportfile['filename'], $exportfile['filename']); + $this->assertEquals($fakeexportfile['filepath'], $exportfile['filepath']); + $this->assertEquals($fakeexportfile['filesize'], $exportfile['filesize']); + $this->assertEquals($fakeexportfile['timemodified'], $exportfile['timemodified']); + $this->assertEquals($fakeexportfile['fileurl'], $exportfile['fileurl']); + + // Test scenario 2: H5P exist, deployed but the content has changed. + // We need to change the contenthash to simulate the H5P file was changed. + $h5pfile = $DB->get_record('h5p', []); + $h5pfile->contenthash = sha1('testedit'); + $DB->update_record('h5p', $h5pfile); + $exportfile = api::get_export_info_from_context_id($syscontext->id, + $factory, + \core_h5p\file_storage::COMPONENT, + \core_h5p\file_storage::EXPORT_FILEAREA); + $this->assertNull($exportfile); + + // Tests scenario 3: H5P is not deployed. + // We need to delete the H5P record to simulate the H5P was not deployed. + $DB->delete_records('h5p', ['id' => $h5pfile->id]); + $exportfile = api::get_export_info_from_context_id($syscontext->id, + $factory, + \core_h5p\file_storage::COMPONENT, + \core_h5p\file_storage::EXPORT_FILEAREA); + $this->assertNull($exportfile); + } } diff --git a/h5p/tests/external_test.php b/h5p/tests/external_test.php index 737c7cc638f..42f1aea2d34 100644 --- a/h5p/tests/external_test.php +++ b/h5p/tests/external_test.php @@ -55,56 +55,45 @@ class core_h5p_external_testcase extends externallib_advanced_testcase { * test_get_trusted_h5p_file description */ public function test_get_trusted_h5p_file() { - global $DB; $this->resetAfterTest(true); $this->setAdminUser(); // This is a valid .H5P file. $filename = 'find-the-words.h5p'; - $path = __DIR__ . '/fixtures/'.$filename; $syscontext = \context_system::instance(); - $filerecord = [ - 'contextid' => $syscontext->id, - 'component' => \core_h5p\file_storage::COMPONENT, - 'filearea' => 'unittest', - 'itemid' => 0, - 'filepath' => '/', - 'filename' => $filename, - ]; - // Load the h5p file into DB. - $fs = get_file_storage(); - $file = $fs->create_file_from_pathname($filerecord, $path); + + // Create a fake export H5P file with normal pluginfile call. + $generator = $this->getDataGenerator()->get_plugin_generator('core_h5p'); + $deployedfile = $generator->create_export_file($filename, + $syscontext->id, + \core_h5p\file_storage::COMPONENT, + \core_h5p\file_storage::EXPORT_FILEAREA, + $generator::PLUGINFILE); + // Make the URL to pass to the WS. $url = \moodle_url::make_pluginfile_url( $syscontext->id, \core_h5p\file_storage::COMPONENT, - 'unittest', + \core_h5p\file_storage::EXPORT_FILEAREA, 0, '/', $filename ); + // Call the WS. - $result = external::get_trusted_h5p_file($url->out(), 0, 0, 0, 0); + $result = external::get_trusted_h5p_file($url->out(false), 0, 0, 0, 0); $result = external_api::clean_returnvalue(external::get_trusted_h5p_file_returns(), $result); // Expected result: Just 1 record on files and none on warnings. $this->assertCount(1, $result['files']); $this->assertCount(0, $result['warnings']); - // Get the export file in the DB to compare with the ws's results. - $fileh5p = $this->get_export_file($filename, $file->get_pathnamehash()); - $fileh5purl = \moodle_url::make_pluginfile_url( - $syscontext->id, - \core_h5p\file_storage::COMPONENT, - \core_h5p\file_storage::EXPORT_FILEAREA, - '', - '', - $fileh5p->get_filename() - ); - $this->assertEquals($fileh5p->get_filepath(), $result['files'][0]['filepath']); - $this->assertEquals($fileh5p->get_mimetype(), $result['files'][0]['mimetype']); - $this->assertEquals($fileh5p->get_filesize(), $result['files'][0]['filesize']); - $this->assertEquals($fileh5p->get_timemodified(), $result['files'][0]['timemodified']); - $this->assertEquals($fileh5p->get_filename(), $result['files'][0]['filename']); - $this->assertEquals($fileh5purl->out(), $result['files'][0]['fileurl']); + + // Check info export file to compare with the ws's results. + $this->assertEquals($deployedfile['filepath'], $result['files'][0]['filepath']); + $this->assertEquals($deployedfile['mimetype'], $result['files'][0]['mimetype']); + $this->assertEquals($deployedfile['filesize'], $result['files'][0]['filesize']); + $this->assertEquals($deployedfile['timemodified'], $result['files'][0]['timemodified']); + $this->assertEquals($deployedfile['filename'], $result['files'][0]['filename']); + $this->assertEquals($deployedfile['fileurl'], $result['files'][0]['fileurl']); } /** @@ -170,56 +159,41 @@ class core_h5p_external_testcase extends externallib_advanced_testcase { * using webservice/pluginfile.php as url param. */ public function test_allow_webservice_pluginfile_in_url_param() { - global $DB; $this->resetAfterTest(true); $this->setAdminUser(); // This is a valid .H5P file. $filename = 'find-the-words.h5p'; - $path = __DIR__ . '/fixtures/'.$filename; $syscontext = \context_system::instance(); - $filerecord = [ - 'contextid' => $syscontext->id, - 'component' => \core_h5p\file_storage::COMPONENT, - 'filearea' => 'unittest', - 'itemid' => 0, - 'filepath' => '/', - 'filename' => $filename, - ]; - // Load the h5p file into DB. - $fs = get_file_storage(); - $file = $fs->create_file_from_pathname($filerecord, $path); + + // Create a fake export H5P file with webservice call. + $generator = $this->getDataGenerator()->get_plugin_generator('core_h5p'); + $deployedfile = $generator->create_export_file($filename, + $syscontext->id, + \core_h5p\file_storage::COMPONENT, + \core_h5p\file_storage::EXPORT_FILEAREA); + // Make the URL to pass to the WS. $url = \moodle_url::make_webservice_pluginfile_url( $syscontext->id, \core_h5p\file_storage::COMPONENT, - 'unittest', + \core_h5p\file_storage::EXPORT_FILEAREA, 0, '/', $filename ); + // Call the WS. $result = external::get_trusted_h5p_file($url->out(), 0, 0, 0, 0); $result = external_api::clean_returnvalue(external::get_trusted_h5p_file_returns(), $result); - // Expected result: Just 1 record on files and none on warnings. - $this->assertCount(1, $result['files']); - $this->assertCount(0, $result['warnings']); - // Get the export file in the DB to compare with the ws's results. - $fileh5p = $this->get_export_file($filename, $file->get_pathnamehash()); - $fileh5purl = \moodle_url::make_webservice_pluginfile_url( - $syscontext->id, - \core_h5p\file_storage::COMPONENT, - \core_h5p\file_storage::EXPORT_FILEAREA, - '', - '', - $fileh5p->get_filename() - ); - $this->assertEquals($fileh5p->get_filepath(), $result['files'][0]['filepath']); - $this->assertEquals($fileh5p->get_mimetype(), $result['files'][0]['mimetype']); - $this->assertEquals($fileh5p->get_filesize(), $result['files'][0]['filesize']); - $this->assertEquals($fileh5p->get_timemodified(), $result['files'][0]['timemodified']); - $this->assertEquals($fileh5p->get_filename(), $result['files'][0]['filename']); - $this->assertEquals($fileh5purl->out(), $result['files'][0]['fileurl']); + + // Check info export file to compare with the ws's results. + $this->assertEquals($deployedfile['filepath'], $result['files'][0]['filepath']); + $this->assertEquals($deployedfile['mimetype'], $result['files'][0]['mimetype']); + $this->assertEquals($deployedfile['filesize'], $result['files'][0]['filesize']); + $this->assertEquals($deployedfile['timemodified'], $result['files'][0]['timemodified']); + $this->assertEquals($deployedfile['filename'], $result['files'][0]['filename']); + $this->assertEquals($deployedfile['fileurl'], $result['files'][0]['fileurl']); } /** @@ -227,83 +201,46 @@ class core_h5p_external_testcase extends externallib_advanced_testcase { * using tokenpluginfile.php as url param. */ public function test_allow_tokenluginfile_in_url_param() { - global $DB; $this->resetAfterTest(true); $this->setAdminUser(); // This is a valid .H5P file. $filename = 'find-the-words.h5p'; - $path = __DIR__ . '/fixtures/'.$filename; $syscontext = \context_system::instance(); - $filerecord = [ - 'contextid' => $syscontext->id, - 'component' => \core_h5p\file_storage::COMPONENT, - 'filearea' => 'unittest', - 'itemid' => 0, - 'filepath' => '/', - 'filename' => $filename, - ]; - // Load the h5p file into DB. - $fs = get_file_storage(); - $file = $fs->create_file_from_pathname($filerecord, $path); + + // Create a fake export H5P file with tokenfile call. + $generator = $this->getDataGenerator()->get_plugin_generator('core_h5p'); + $deployedfile = $generator->create_export_file($filename, + $syscontext->id, + \core_h5p\file_storage::COMPONENT, + \core_h5p\file_storage::EXPORT_FILEAREA, + $generator::TOKENPLUGINFILE); + // Make the URL to pass to the WS. $url = \moodle_url::make_pluginfile_url( $syscontext->id, \core_h5p\file_storage::COMPONENT, - 'unittest', + \core_h5p\file_storage::EXPORT_FILEAREA, 0, '/', $filename, false, true ); + // Call the WS. - $result = external::get_trusted_h5p_file($url->out(), 0, 0, 0, 0); + $result = external::get_trusted_h5p_file($url->out(false), 0, 0, 0, 0); $result = external_api::clean_returnvalue(external::get_trusted_h5p_file_returns(), $result); // Expected result: Just 1 record on files and none on warnings. $this->assertCount(1, $result['files']); $this->assertCount(0, $result['warnings']); - // Get the export file in the DB to compare with the ws's results. - $fileh5p = $this->get_export_file($filename, $file->get_pathnamehash()); - $fileh5purl = \moodle_url::make_pluginfile_url( - $syscontext->id, - \core_h5p\file_storage::COMPONENT, - \core_h5p\file_storage::EXPORT_FILEAREA, - '', - '', - $fileh5p->get_filename(), - false, - true - ); - $this->assertEquals($fileh5p->get_filepath(), $result['files'][0]['filepath']); - $this->assertEquals($fileh5p->get_mimetype(), $result['files'][0]['mimetype']); - $this->assertEquals($fileh5p->get_filesize(), $result['files'][0]['filesize']); - $this->assertEquals($fileh5p->get_timemodified(), $result['files'][0]['timemodified']); - $this->assertEquals($fileh5p->get_filename(), $result['files'][0]['filename']); - $this->assertEquals($fileh5purl->out(), $result['files'][0]['fileurl']); - } - /** - * Get the H5P export file. - * - * @param string $filename - * @param string $pathnamehash - * @return stored_file - */ - protected function get_export_file($filename, $pathnamehash) { - global $DB; - - // Simulate the filenameexport using slug as H5P does. - $id = $DB->get_field('h5p', 'id', ['pathnamehash' => $pathnamehash]); - $filenameexport = basename($filename, '.h5p').'-'.$id.'-'.$id.'.h5p'; - $syscontext = \context_system::instance(); - $fs = get_file_storage(); - $fileh5p = $fs->get_file($syscontext->id, - \core_h5p\file_storage::COMPONENT, - \core_h5p\file_storage::EXPORT_FILEAREA, - 0, - '/', - $filenameexport); - return $fileh5p; + // Check info export file to compare with the ws's results. + $this->assertEquals($deployedfile['filepath'], $result['files'][0]['filepath']); + $this->assertEquals($deployedfile['mimetype'], $result['files'][0]['mimetype']); + $this->assertEquals($deployedfile['filesize'], $result['files'][0]['filesize']); + $this->assertEquals($deployedfile['timemodified'], $result['files'][0]['timemodified']); + $this->assertEquals($deployedfile['filename'], $result['files'][0]['filename']); + $this->assertEquals($deployedfile['fileurl'], $result['files'][0]['fileurl']); } } diff --git a/h5p/tests/generator/lib.php b/h5p/tests/generator/lib.php index 9cf4313c6f2..88c229d25e5 100644 --- a/h5p/tests/generator/lib.php +++ b/h5p/tests/generator/lib.php @@ -25,6 +25,8 @@ use core_h5p\local\library\autoloader; use core_h5p\core; +use core_h5p\player; +use core_h5p\factory; defined('MOODLE_INTERNAL') || die(); @@ -38,6 +40,13 @@ defined('MOODLE_INTERNAL') || die(); */ class core_h5p_generator extends \component_generator_base { + /** Url pointing to webservice plugin file. */ + public const WSPLUGINFILE = 0; + /** Url pointing to token plugin file. */ + public const TOKENPLUGINFILE = 1; + /** Url pointing to plugin file. */ + public const PLUGINFILE = 2; + /** * Convenience function to create a file. * @@ -428,4 +437,121 @@ class core_h5p_generator extends \component_generator_base { $fs = new file_storage(); return $fs->create_file_from_string($filerecord, $content); } + + /** + * Create a fake export H5P deployed file. + * + * @param string $filename Name of the H5P file to deploy. + * @param int $contextid Context id of the H5P activity. + * @param string $component component. + * @param string $filearea file area. + * @param int $typeurl Type of url to create the export url plugin file. + * @return array return deployed file information. + */ + public function create_export_file(string $filename, int $contextid, + string $component, + string $filearea, + int $typeurl = self::WSPLUGINFILE): array { + global $CFG; + + // We need the autoloader for H5P player. + autoloader::register(); + + $path = $CFG->dirroot.'/h5p/tests/fixtures/'.$filename; + $filerecord = [ + 'contextid' => $contextid, + 'component' => $component, + 'filearea' => $filearea, + 'itemid' => 0, + 'filepath' => '/', + 'filename' => $filename, + ]; + // Load the h5p file into DB. + $fs = get_file_storage(); + if (!$fs->get_file($contextid, $component, $filearea, $filerecord['itemid'], $filerecord['filepath'], $filename)) { + $fs->create_file_from_pathname($filerecord, $path); + } + + // Make the URL to pass to the player. + if ($typeurl == self::WSPLUGINFILE) { + $url = \moodle_url::make_webservice_pluginfile_url( + $filerecord['contextid'], + $filerecord['component'], + $filerecord['filearea'], + $filerecord['itemid'], + $filerecord['filepath'], + $filerecord['filename'] + ); + } else { + $includetoken = false; + if ($typeurl == self::TOKENPLUGINFILE) { + $includetoken = true; + } + $url = \moodle_url::make_pluginfile_url( + $filerecord['contextid'], + $filerecord['component'], + $filerecord['filearea'], + $filerecord['itemid'], + $filerecord['filepath'], + $filerecord['filename'], + false, + $includetoken + ); + } + + $config = new stdClass(); + $h5pplayer = new player($url->out(false), $config); + // We need to add assets to page to create the export file. + $h5pplayer->add_assets_to_page(); + + // Call the method. We need the id of the new H5P content. + $rc = new \ReflectionClass(player::class); + $rcp = $rc->getProperty('h5pid'); + $rcp->setAccessible(true); + $h5pid = $rcp->getValue($h5pplayer); + + // Get the info export file. + $factory = new factory(); + $core = $factory->get_core(); + $content = $core->loadContent($h5pid); + $slug = $content['slug'] ? $content['slug'] . '-' : ''; + $exportfilename = "{$slug}{$h5pid}.h5p"; + $fileh5p = $core->fs->get_export_file($exportfilename); + $deployedfile = []; + $deployedfile['filename'] = $fileh5p->get_filename(); + $deployedfile['filepath'] = $fileh5p->get_filepath(); + $deployedfile['mimetype'] = $fileh5p->get_mimetype(); + $deployedfile['filesize'] = $fileh5p->get_filesize(); + $deployedfile['timemodified'] = $fileh5p->get_timemodified(); + + // Create the url depending the request was made through typeurl. + if ($typeurl == self::WSPLUGINFILE) { + $url = \moodle_url::make_webservice_pluginfile_url( + $fileh5p->get_contextid(), + $fileh5p->get_component(), + $fileh5p->get_filearea(), + '', + '', + $fileh5p->get_filename() + ); + } else { + $includetoken = false; + if ($typeurl == self::TOKENPLUGINFILE) { + $includetoken = true; + } + $url = \moodle_url::make_pluginfile_url( + $fileh5p->get_contextid(), + $fileh5p->get_component(), + $fileh5p->get_filearea(), + '', + '', + $fileh5p->get_filename(), + false, + $includetoken + ); + } + $deployedfile['fileurl'] = $url->out(false); + + return $deployedfile; + } } diff --git a/h5p/tests/helper_test.php b/h5p/tests/helper_test.php index 734bd08e431..a58f61a0a36 100644 --- a/h5p/tests/helper_test.php +++ b/h5p/tests/helper_test.php @@ -328,4 +328,55 @@ class helper_testcase extends \advanced_testcase { $this->assertCount(7, $messages->error); $this->assertCount(2, $messages->info); } + + /** + * Test the behaviour of get_export_info(). + */ + public function test_get_export_info(): void { + $this->resetAfterTest(); + + $filename = 'guess-the-answer.h5p'; + $syscontext = \context_system::instance(); + + $generator = $this->getDataGenerator()->get_plugin_generator('core_h5p'); + $deployedfile = $generator->create_export_file($filename, + $syscontext->id, + file_storage::COMPONENT, + file_storage::EXPORT_FILEAREA); + + // Test scenario 1: Get export information from correct filename. + $helperfile = helper::get_export_info($deployedfile['filename']); + $this->assertEquals($deployedfile['filename'], $helperfile['filename']); + $this->assertEquals($deployedfile['filepath'], $helperfile['filepath']); + $this->assertEquals($deployedfile['filesize'], $helperfile['filesize']); + $this->assertEquals($deployedfile['timemodified'], $helperfile['timemodified']); + $this->assertEquals($deployedfile['fileurl'], $helperfile['fileurl']); + + // Test scenario 2: Get export information from correct filename and url. + $url = \moodle_url::make_pluginfile_url( + $syscontext->id, + file_storage::COMPONENT, + 'unittest', + 0, + '/', + $deployedfile['filename'], + false, + true + ); + $helperfile = helper::get_export_info($deployedfile['filename'], $url); + $this->assertEquals($url, $helperfile['fileurl']); + + // Test scenario 3: Get export information from correct filename and factory. + $factory = new \core_h5p\factory(); + $helperfile = helper::get_export_info($deployedfile['filename'], null, $factory); + $this->assertEquals($deployedfile['filename'], $helperfile['filename']); + $this->assertEquals($deployedfile['filepath'], $helperfile['filepath']); + $this->assertEquals($deployedfile['filesize'], $helperfile['filesize']); + $this->assertEquals($deployedfile['timemodified'], $helperfile['timemodified']); + $this->assertEquals($deployedfile['fileurl'], $helperfile['fileurl']); + + // Test scenario 4: Get export information from wrong filename. + $helperfile = helper::get_export_info('nofileexist.h5p', $url); + $this->assertNull($helperfile); + } } diff --git a/mod/h5pactivity/classes/external/get_h5pactivities_by_courses.php b/mod/h5pactivity/classes/external/get_h5pactivities_by_courses.php new file mode 100644 index 00000000000..e1546dd1029 --- /dev/null +++ b/mod/h5pactivity/classes/external/get_h5pactivities_by_courses.php @@ -0,0 +1,136 @@ +. + +/** + * This is the external method for returning a list of h5p activities. + * + * @package mod_h5pactivity + * @since Moodle 3.9 + * @copyright 2020 Carlos Escobedo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_h5pactivity\external; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/externallib.php'); + +use external_api; +use external_function_parameters; +use external_value; +use external_single_structure; +use external_multiple_structure; +use external_util; +use external_warnings; +use context_module; +use core_h5p\factory; + +/** + * This is the external method for returning a list of h5p activities. + * + * @copyright 2020 Carlos Escobedo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_h5pactivities_by_courses extends external_api { + /** + * Parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters ( + [ + 'courseids' => new external_multiple_structure( + new external_value(PARAM_INT, 'Course id'), 'Array of course ids', VALUE_DEFAULT, [] + ), + ] + ); + } + + /** + * Returns a list of h5p activities in a provided list of courses. + * If no list is provided all h5p activities that the user can view will be returned. + * + * @param array $courseids course ids + * @return array of h5p activities and warnings + * @since Moodle 3.9 + */ + public static function execute(array $courseids): array { + global $PAGE; + + $warnings = []; + $returnedh5pactivities = []; + + $params = external_api::validate_parameters(self::execute_parameters(), [ + 'courseids' => $courseids + ]); + + $mycourses = []; + if (empty($params['courseids'])) { + $mycourses = enrol_get_my_courses(); + $params['courseids'] = array_keys($mycourses); + } + + // Ensure there are courseids to loop through. + if (!empty($params['courseids'])) { + + $factory = new factory(); + + list($courses, $warnings) = external_util::validate_courses($params['courseids'], $mycourses); + $output = $PAGE->get_renderer('core'); + + // Get the h5p activities in this course, this function checks users visibility permissions. + // We can avoid then additional validate_context calls. + $h5pactivities = get_all_instances_in_courses('h5pactivity', $courses); + foreach ($h5pactivities as $h5pactivity) { + $context = context_module::instance($h5pactivity->coursemodule); + // Remove fields that are not from the h5p activity (added by get_all_instances_in_courses). + unset($h5pactivity->coursemodule, $h5pactivity->context, + $h5pactivity->visible, $h5pactivity->section, + $h5pactivity->groupmode, $h5pactivity->groupingid); + + $exporter = new h5pactivity_summary_exporter($h5pactivity, + ['context' => $context, 'factory' => $factory]); + $summary = $exporter->export($output); + $returnedh5pactivities[] = $summary; + } + } + + $result = [ + 'h5pactivities' => $returnedh5pactivities, + 'warnings' => $warnings + ]; + return $result; + } + + /** + * Describes the get_h5pactivities_by_courses return value. + * + * @return external_single_structure + * @since Moodle 3.9 + */ + public static function execute_returns() { + return new external_single_structure( + [ + 'h5pactivities' => new external_multiple_structure( + h5pactivity_summary_exporter::get_read_structure() + ), + 'warnings' => new external_warnings(), + ] + ); + } +} \ No newline at end of file diff --git a/mod/h5pactivity/classes/external/h5pactivity_summary_exporter.php b/mod/h5pactivity/classes/external/h5pactivity_summary_exporter.php new file mode 100644 index 00000000000..0088715be4b --- /dev/null +++ b/mod/h5pactivity/classes/external/h5pactivity_summary_exporter.php @@ -0,0 +1,241 @@ +. + +/** + * Class for exporting h5p activity data. + * + * @package mod_h5pactivity + * @since Moodle 3.9 + * @copyright 2020 Carlos Escobedo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_h5pactivity\external; + +use core\external\exporter; +use renderer_base; +use external_util; +use external_files; +use core_h5p\factory; +use core_h5p\api; + +/** + * Class for exporting h5p activity data. + * + * @copyright 2020 Carlos Escobedo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class h5pactivity_summary_exporter extends exporter { + + /** + * Properties definition. + * + * @return array + */ + protected static function define_properties() { + + return [ + 'id' => [ + 'type' => PARAM_INT, + 'description' => 'The primary key of the record.', + ], + 'course' => [ + 'type' => PARAM_INT, + 'description' => 'Course id this h5p activity is part of.', + ], + 'name' => [ + 'type' => PARAM_TEXT, + 'description' => 'The name of the activity module instance.', + ], + 'timecreated' => [ + 'type' => PARAM_INT, + 'description' => 'Timestamp of when the instance was added to the course.', + 'optional' => true, + ], + 'timemodified' => [ + 'type' => PARAM_INT, + 'description' => 'Timestamp of when the instance was last modified.', + 'optional' => true, + ], + 'intro' => [ + 'default' => '', + 'type' => PARAM_RAW, + 'description' => 'H5P activity description.', + 'null' => NULL_ALLOWED, + ], + 'introformat' => [ + 'choices' => [FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN], + 'type' => PARAM_INT, + 'default' => FORMAT_MOODLE, + 'description' => 'The format of the intro field.', + ], + 'grade' => [ + 'type' => PARAM_INT, + 'default' => 0, + 'description' => 'The maximum grade for submission.', + 'optional' => true, + ], + 'displayoptions' => [ + 'type' => PARAM_INT, + 'default' => 0, + 'description' => 'H5P Button display options.', + ], + 'enabletracking' => [ + 'type' => PARAM_INT, + 'default' => 1, + 'description' => 'Enable xAPI tracking.', + ], + 'grademethod' => [ + 'type' => PARAM_INT, + 'default' => 1, + 'description' => 'Which H5P attempt is used for grading.', + ], + 'contenthash' => [ + 'type' => PARAM_ALPHANUM, + 'description' => 'Sha1 hash of file content.', + 'optional' => true, + ], + ]; + } + + /** + * Related objects definition. + * + * @return array + */ + protected static function define_related() { + return [ + 'context' => 'context', + 'factory' => 'core_h5p\\factory' + ]; + } + + /** + * Other properties definition. + * + * @return array + */ + protected static function define_other_properties() { + return [ + 'coursemodule' => [ + 'type' => PARAM_INT + ], + 'introfiles' => [ + 'type' => external_files::get_properties_for_exporter(), + 'multiple' => true + ], + 'package' => [ + 'type' => external_files::get_properties_for_exporter(), + 'multiple' => true + ], + 'deployedfile' => [ + 'optional' => true, + 'description' => 'H5P file deployed.', + 'type' => [ + 'filename' => array( + 'type' => PARAM_FILE, + 'description' => 'File name.', + 'optional' => true, + 'null' => NULL_NOT_ALLOWED, + ), + 'filepath' => array( + 'type' => PARAM_PATH, + 'description' => 'File path.', + 'optional' => true, + 'null' => NULL_NOT_ALLOWED, + ), + 'filesize' => array( + 'type' => PARAM_INT, + 'description' => 'File size.', + 'optional' => true, + 'null' => NULL_NOT_ALLOWED, + ), + 'fileurl' => array( + 'type' => PARAM_URL, + 'description' => 'Downloadable file url.', + 'optional' => true, + 'null' => NULL_NOT_ALLOWED, + ), + 'timemodified' => array( + 'type' => PARAM_INT, + 'description' => 'Time modified.', + 'optional' => true, + 'null' => NULL_NOT_ALLOWED, + ), + 'mimetype' => array( + 'type' => PARAM_RAW, + 'description' => 'File mime type.', + 'optional' => true, + 'null' => NULL_NOT_ALLOWED, + ) + ] + ], + ]; + } + + /** + * Assign values to the defined other properties. + * + * @param renderer_base $output The output renderer object. + * @return array + */ + protected function get_other_values(renderer_base $output) { + $context = $this->related['context']; + $factory = $this->related['factory']; + + $values = [ + 'coursemodule' => $context->instanceid, + ]; + + $values['introfiles'] = external_util::get_area_files($context->id, 'mod_h5pactivity', 'intro', false, false); + + $values['package'] = external_util::get_area_files($context->id, 'mod_h5pactivity', 'package', false, false); + + // Only if this H5P activity has been deployed, return the exported file. + $fileh5p = api::get_export_info_from_context_id($context->id, $factory, 'mod_h5pactivity', 'package'); + if ($fileh5p) { + $values['deployedfile'] = $fileh5p; + } + + return $values; + } + + /** + * Get the formatting parameters for the intro. + * + * @return array with the formatting parameters + */ + protected function get_format_parameters_for_intro() { + return [ + 'component' => 'mod_h5pactivity', + 'filearea' => 'intro', + 'options' => ['noclean' => true], + ]; + } + + /** + * Get the formatting parameters for the package. + * + * @return array with the formatting parameters + */ + protected function get_format_parameters_for_package() { + return [ + 'component' => 'mod_h5pactivity', + 'filearea' => 'package', + 'itemid' => 0, + 'options' => ['noclean' => true], + ]; + } +} diff --git a/mod/h5pactivity/db/services.php b/mod/h5pactivity/db/services.php index 904df636692..b27e6b86ce5 100644 --- a/mod/h5pactivity/db/services.php +++ b/mod/h5pactivity/db/services.php @@ -62,4 +62,15 @@ $functions = [ 'capabilities' => 'mod/h5pactivity:view', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], + 'mod_h5pactivity_get_h5pactivities_by_courses' => [ + 'classname' => 'mod_h5pactivity\external\get_h5pactivities_by_courses', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Returns a list of h5p activities in a list of + provided courses, if no list is provided all h5p activities + that the user can view will be returned.', + 'type' => 'read', + 'capabilities' => 'mod/h5pactivity:view', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], ]; diff --git a/mod/h5pactivity/tests/external/get_h5pactivities_by_courses_test.php b/mod/h5pactivity/tests/external/get_h5pactivities_by_courses_test.php new file mode 100644 index 00000000000..62050d2ce1b --- /dev/null +++ b/mod/h5pactivity/tests/external/get_h5pactivities_by_courses_test.php @@ -0,0 +1,182 @@ +. + +/** + * External function test for get_h5pactivities_by_courses. + * + * @package mod_h5pactivity + * @category external + * @since Moodle 3.9 + * @copyright 2020 Carlos Escobedo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_h5pactivity\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +use external_api; +use externallib_advanced_testcase; +use stdClass; +use context_module; + +/** + * External function test for get_h5pactivities_by_courses. + * + * @package mod_h5pactivity + * @copyright 2020 Carlos Escobedo + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_h5pactivities_by_courses_testcase extends externallib_advanced_testcase { + + /** + * Test test_get_h5pactivities_by_courses user student. + */ + public function test_get_h5pactivities_by_courses() { + global $CFG, $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + + // Create 2 courses. + // Course 1 -> 2 activities with H5P files package without deploy. + // Course 2 -> 1 activity with H5P file package deployed. + $course1 = $this->getDataGenerator()->create_course(); + $params = [ + 'course' => $course1->id, + 'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/filltheblanks.h5p', + 'introformat' => 1 + ]; + $activities[] = $this->getDataGenerator()->create_module('h5pactivity', $params); + // Add filename to make easier the asserts. + $activities[0]->filename = 'filltheblanks.h5p'; + $params = [ + 'course' => $course1->id, + 'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/greeting-card-887.h5p', + 'introformat' => 1 + ]; + $activities[] = $this->getDataGenerator()->create_module('h5pactivity', $params); + // Add filename to make easier the asserts. + $activities[1]->filename = 'greeting-card-887.h5p'; + + $course2 = $this->getDataGenerator()->create_course(); + $params = [ + 'course' => $course2->id, + 'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/guess-the-answer.h5p', + 'introformat' => 1 + ]; + $activities[] = $this->getDataGenerator()->create_module('h5pactivity', $params); + $activities[2]->filename = 'guess-the-answer.h5p'; + + $context = context_module::instance($activities[2]->cmid); + // Create a fake deploy H5P file. + $generator = $this->getDataGenerator()->get_plugin_generator('core_h5p'); + $deployedfile = $generator->create_export_file($activities[2]->filename, $context->id, 'mod_h5pactivity', 'package'); + + // Create a user and enrol as student in both courses. + $user = $this->getDataGenerator()->create_user(); + $studentrole = $DB->get_record('role', ['shortname' => 'student']); + $maninstance1 = $DB->get_record('enrol', ['courseid' => $course1->id, 'enrol' => 'manual'], '*', MUST_EXIST); + $maninstance2 = $DB->get_record('enrol', ['courseid' => $course2->id, 'enrol' => 'manual'], '*', MUST_EXIST); + $manual = enrol_get_plugin('manual'); + $manual->enrol_user($maninstance1, $user->id, $studentrole->id); + $manual->enrol_user($maninstance2, $user->id, $studentrole->id); + + // Check the activities returned by the first course. + $this->setUser($user); + $courseids = [$course1->id]; + $result = get_h5pactivities_by_courses::execute($courseids); + $result = external_api::clean_returnvalue(get_h5pactivities_by_courses::execute_returns(), $result); + $this->assertCount(0, $result['warnings']); + $this->assertCount(2, $result['h5pactivities']); + $this->assert_activities($activities, $result); + $this->assertNotContains('deployedfile', $result['h5pactivities'][0]); + $this->assertNotContains('deployedfile', $result['h5pactivities'][1]); + + // Call the external function without passing course id. + // Expected result, all the courses, course1 and course2. + $result = get_h5pactivities_by_courses::execute([]); + $result = external_api::clean_returnvalue(get_h5pactivities_by_courses::execute_returns(), $result); + $this->assertCount(0, $result['warnings']); + $this->assertCount(3, $result['h5pactivities']); + // We need to sort the $result by id. + // Because we are not sure how it is ordered with more than one course. + array_multisort(array_map(function($element) { + return $element['id']; + }, $result['h5pactivities']), SORT_ASC, $result['h5pactivities']); + $this->assert_activities($activities, $result); + $this->assertNotContains('deployedfile', $result['h5pactivities'][0]); + $this->assertNotContains('deployedfile', $result['h5pactivities'][1]); + // Only the activity from the second course has been deployed. + $this->assertEquals($deployedfile['filename'], $result['h5pactivities'][2]['deployedfile']['filename']); + $this->assertEquals($deployedfile['filepath'], $result['h5pactivities'][2]['deployedfile']['filepath']); + $this->assertEquals($deployedfile['filesize'], $result['h5pactivities'][2]['deployedfile']['filesize']); + $this->assertEquals($deployedfile['timemodified'], $result['h5pactivities'][2]['deployedfile']['timemodified']); + $this->assertEquals($deployedfile['mimetype'], $result['h5pactivities'][2]['deployedfile']['mimetype']); + $this->assertEquals($deployedfile['fileurl'], $result['h5pactivities'][2]['deployedfile']['fileurl']); + + // Unenrol user from second course. + $manual->unenrol_user($maninstance2, $user->id); + // Remove the last activity from the array. + array_pop($activities); + + // Call the external function without passing course id. + $result = get_h5pactivities_by_courses::execute([]); + $result = external_api::clean_returnvalue(get_h5pactivities_by_courses::execute_returns(), $result); + $this->assertCount(0, $result['warnings']); + $this->assertCount(2, $result['h5pactivities']); + $this->assert_activities($activities, $result); + + // Call for the second course we unenrolled the user from, expected warning. + $result = get_h5pactivities_by_courses::execute([$course2->id]); + $result = external_api::clean_returnvalue(get_h5pactivities_by_courses::execute_returns(), $result); + $this->assertCount(1, $result['warnings']); + $this->assertEquals('1', $result['warnings'][0]['warningcode']); + $this->assertEquals($course2->id, $result['warnings'][0]['itemid']); + } + + /** + * Create a scenario to use into the tests. + * + * @param array $activities list of H5P activities. + * @param array $result list of H5P activities by WS. + * @return void + */ + protected function assert_activities(array $activities, array $result): void { + + $total = count($result); + for ($i = 0; $i < $total; $i++) { + $this->assertEquals($activities[$i]->id, $result['h5pactivities'][$i]['id']); + $this->assertEquals($activities[$i]->course, $result['h5pactivities'][$i]['course']); + $this->assertEquals($activities[$i]->name, $result['h5pactivities'][$i]['name']); + $this->assertEquals($activities[$i]->timecreated, $result['h5pactivities'][$i]['timecreated']); + $this->assertEquals($activities[$i]->timemodified, $result['h5pactivities'][$i]['timemodified']); + $this->assertEquals($activities[$i]->intro, $result['h5pactivities'][$i]['intro']); + $this->assertEquals($activities[$i]->introformat, $result['h5pactivities'][$i]['introformat']); + $this->assertEquals([], $result['h5pactivities'][$i]['introfiles']); + $this->assertEquals($activities[$i]->grade, $result['h5pactivities'][$i]['grade']); + $this->assertEquals($activities[$i]->displayoptions, $result['h5pactivities'][$i]['displayoptions']); + $this->assertEquals($activities[$i]->enabletracking, $result['h5pactivities'][$i]['enabletracking']); + $this->assertEquals($activities[$i]->grademethod, $result['h5pactivities'][$i]['grademethod']); + $this->assertEquals($activities[$i]->cmid, $result['h5pactivities'][$i]['coursemodule']); + $this->assertEquals($activities[$i]->filename, $result['h5pactivities'][$i]['package'][0]['filename']); + } + } +}