From 13de79c66f5f5b7114a46476543c18de4c0159d2 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Thu, 10 Jun 2021 16:55:09 +0200 Subject: [PATCH 1/3] MDL-71885 core_h5p: Add new methods to API --- h5p/classes/api.php | 95 ++++++++++++ h5p/tests/api_test.php | 324 +++++++++++++++++++++++++++++++++++++++++ h5p/upgrade.txt | 3 + 3 files changed, 422 insertions(+) diff --git a/h5p/classes/api.php b/h5p/classes/api.php index 5eb3ecbef63..6aaf4adebff 100644 --- a/h5p/classes/api.php +++ b/h5p/classes/api.php @@ -217,6 +217,101 @@ class api { return [$file, $h5p]; } + /** + * Get the original file and H5P DB instance for a given H5P pluginfile URL. If it doesn't exist, it's not created. + * If the file has been added as a reference, this method will return the original linked file. + * + * @param string $url H5P pluginfile URL. + * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions. + * @param bool $skipcapcheck Whether capabilities should be checked or not to get the pluginfile URL because sometimes they + * might be controlled before calling this method. + * + * @return array of [\stored_file|false, \stdClass|false, \stored_file|false]: + * - \stored_file: original local file for the given url (if it has been added as a reference, this method + * will return the linked file) or false if there isn't any H5P file with this URL. + * - \stdClass: an H5P object or false if there isn't any H5P with this URL. + * - \stored_file: file associated to the given url (if it's different from original) or false when both files + * (original and file) are the same. + * @since Moodle 4.0 + */ + public static function get_original_content_from_pluginfile_url(string $url, bool $preventredirect = true, + bool $skipcapcheck = false): array { + + $file = false; + list($originalfile, $h5p) = self::get_content_from_pluginfile_url($url, $preventredirect, $skipcapcheck); + if ($originalfile) { + if ($reference = $originalfile->get_reference()) { + $file = $originalfile; + // If the file has been added as a reference to any other file, get it. + $fs = new \file_storage(); + $referenced = \file_storage::unpack_reference($reference); + $originalfile = $fs->get_file( + $referenced['contextid'], + $referenced['component'], + $referenced['filearea'], + $referenced['itemid'], + $referenced['filepath'], + $referenced['filename'] + ); + $h5p = self::get_content_from_pathnamehash($originalfile->get_pathnamehash()); + if (empty($h5p)) { + $h5p = false; + } + } + } + + return [$originalfile, $h5p, $file]; + } + + /** + * Check if the user can edit an H5P file. It will return true in the following situations: + * - The user is the author of the file. + * - The component is different from user (i.e. private files). + * - If the component is contentbank, the user can edit this file (calling the ContentBank API). + * - If the component is mod_h5pactivity, the user has the addinstance capability. + * + * @param \stored_file $file The H5P file to check. + * + * @return boolean Whether the user can edit or not the given file. + * @since Moodle 4.0 + */ + public static function can_edit_content(\stored_file $file): bool { + global $USER; + + // Private files. + $currentuserisauthor = $file->get_userid() == $USER->id; + $isuserfile = $file->get_component() === 'user'; + if ($currentuserisauthor && $isuserfile) { + // The user can edit the content because it's a private user file and she is the owner. + return true; + } + + // For mod_h5pactivity, check whether the user can add/edit them. + if ($file->get_component() === 'mod_h5pactivity') { + $context = \context::instance_by_id($file->get_contextid()); + if (has_capability("mod/h5pactivity:addinstance", $context)) { + // The user can edit the content because she has the capability for creating H5P activities where the file belongs. + return true; + } + } + + // For contentbank files, use the API to check if the user has access. + if ($file->get_component() == 'contentbank') { + $cb = new \core_contentbank\contentbank(); + $content = $cb->get_content_from_id($file->get_itemid()); + $contenttype = $content->get_content_type_instance(); + if ($contenttype instanceof \contenttype_h5p\contenttype) { + // Only H5P contenttypes should be considered here. + if ($contenttype->can_edit($content)) { + // The user has permissions to edit the H5P in the content bank. + return true; + } + } + } + + return false; + } + /** * Create, if it doesn't exist, the H5P DB instance id for a H5P pluginfile URL. If it exists: * - If the content is not the same, remove the existing content and re-deploy the H5P content again. diff --git a/h5p/tests/api_test.php b/h5p/tests/api_test.php index ec7f5e56ae9..0ab510e4c37 100644 --- a/h5p/tests/api_test.php +++ b/h5p/tests/api_test.php @@ -333,6 +333,330 @@ class api_test extends \advanced_testcase { $this->assertFalse($h5p); } + /** + * Test the behaviour of get_original_content_from_pluginfile_url(). + * + * @covers ::get_original_content_from_pluginfile_url + */ + public function test_get_original_content_from_pluginfile_url(): void { + $this->setRunTestInSeparateProcess(true); + $this->resetAfterTest(); + $this->setAdminUser(); + + $factory = new factory(); + $syscontext = \context_system::instance(); + + // Create the original file. + $filename = 'greeting-card-887.h5p'; + $path = __DIR__ . '/fixtures/' . $filename; + $originalfile = helper::create_fake_stored_file_from_path($path); + $originalfilerecord = [ + 'contextid' => $originalfile->get_contextid(), + 'component' => $originalfile->get_component(), + 'filearea' => $originalfile->get_filearea(), + 'itemid' => $originalfile->get_itemid(), + 'filepath' => $originalfile->get_filepath(), + 'filename' => $originalfile->get_filename(), + ]; + + $config = (object)[ + 'frame' => 1, + 'export' => 1, + 'embed' => 0, + 'copyright' => 0, + ]; + + $originalurl = \moodle_url::make_pluginfile_url( + $originalfile->get_contextid(), + $originalfile->get_component(), + $originalfile->get_filearea(), + $originalfile->get_itemid(), + $originalfile->get_filepath(), + $originalfile->get_filename() + ); + + // Create a reference to the original file. + $reffilerecord = [ + 'contextid' => $syscontext->id, + 'component' => 'core', + 'filearea' => 'phpunit', + 'itemid' => 0, + 'filepath' => '/', + 'filename' => $filename + ]; + + $fs = get_file_storage(); + $ref = $fs->pack_reference($originalfilerecord); + $repos = \repository::get_instances(['type' => 'user']); + $userrepository = reset($repos); + $referencedfile = $fs->create_file_from_reference($reffilerecord, $userrepository->id, $ref); + $this->assertEquals($referencedfile->get_contenthash(), $originalfile->get_contenthash()); + + $referencedurl = \moodle_url::make_pluginfile_url( + $syscontext->id, + 'core', + 'phpunit', + 0, + '/', + $filename + ); + + // Scenario 1: Original file (without any reference). + $originalh5pid = helper::save_h5p($factory, $originalfile, $config); + list($source, $h5p, $file) = api::get_original_content_from_pluginfile_url($originalurl->out()); + $this->assertEquals($originalfile->get_pathnamehash(), $source->get_pathnamehash()); + $this->assertEquals($originalfile->get_contenthash(), $source->get_contenthash()); + $this->assertEquals($originalh5pid, $h5p->id); + $this->assertFalse($file); + + // Scenario 2: Referenced file (alias to originalfile). + list($source, $h5p, $file) = api::get_original_content_from_pluginfile_url($referencedurl->out()); + $this->assertEquals($originalfile->get_pathnamehash(), $source->get_pathnamehash()); + $this->assertEquals($originalfile->get_contenthash(), $source->get_contenthash()); + $this->assertEquals($originalfile->get_contenthash(), $source->get_contenthash()); + $this->assertEquals($originalh5pid, $h5p->id); + $this->assertEquals($referencedfile->get_pathnamehash(), $file->get_pathnamehash()); + $this->assertEquals($referencedfile->get_contenthash(), $file->get_contenthash()); + $this->assertEquals($referencedfile->get_contenthash(), $file->get_contenthash()); + + // Scenario 3: Unexisting file. + $unexistingurl = \moodle_url::make_pluginfile_url( + $syscontext->id, + 'core', + 'phpunit', + 0, + '/', + 'unexisting.h5p' + ); + list($source, $h5p, $file) = api::get_original_content_from_pluginfile_url($unexistingurl->out()); + $this->assertFalse($source); + $this->assertFalse($h5p); + $this->assertFalse($file); + } + + /** + * Test the behaviour of can_edit_content(). + * + * @covers ::can_edit_content + * @dataProvider can_edit_content_provider + * + * @param string $currentuser User who will call the method. + * @param string $fileauthor Author of the file to check. + * @param string $filecomponent Component of the file to check. + * @param bool $expected Expected result after calling the can_edit_content method. + * + * @return void + */ + public function test_can_edit_content(string $currentuser, string $fileauthor, string $filecomponent, bool $expected): void { + global $USER; + + $this->setRunTestInSeparateProcess(true); + $this->resetAfterTest(); + + // Create course. + $course = $this->getDataGenerator()->create_course(); + $context = \context_course::instance($course->id); + + // Create some users. + $this->setAdminUser(); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $users = [ + 'admin' => $USER, + 'teacher' => $teacher, + 'student' => $student, + ]; + + // Set current user. + if ($currentuser !== 'admin') { + $this->setUser($users[$currentuser]); + } + + // Create the file. + $filename = 'greeting-card-887.h5p'; + $path = __DIR__ . '/fixtures/' . $filename; + if ($filecomponent === 'contentbank') { + $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); + $contents = $generator->generate_contentbank_data( + 'contenttype_h5p', + 1, + (int)$users[$fileauthor]->id, + $context, + true, + $path + ); + $content = array_shift($contents); + $file = $content->get_file(); + } else { + $filerecord = [ + 'contextid' => $context->id, + 'component' => $filecomponent, + 'filearea' => 'unittest', + 'itemid' => rand(), + 'filepath' => '/', + 'filename' => basename($path), + 'userid' => $users[$fileauthor]->id, + ]; + $fs = get_file_storage(); + $file = $fs->create_file_from_pathname($filerecord, $path); + } + + // Check if the currentuser can edit the file. + $result = api::can_edit_content($file); + $this->assertEquals($expected, $result); + } + + /** + * Data provider for test_can_edit_content(). + * + * @return array + */ + public function can_edit_content_provider(): array { + return [ + // Component = user. + 'user: Admin user is author' => [ + 'currentuser' => 'admin', + 'fileauthor' => 'admin', + 'filecomponent' => 'user', + 'expected' => true, + ], + 'user: Admin user, teacher is author' => [ + 'currentuser' => 'admin', + 'fileauthor' => 'teacher', + 'filecomponent' => 'user', + 'expected' => false, + ], + 'user: Teacher user, teacher is author' => [ + 'currentuser' => 'teacher', + 'fileauthor' => 'teacher', + 'filecomponent' => 'user', + 'expected' => true, + ], + 'user: Teacher user, admin is author' => [ + 'currentuser' => 'teacher', + 'fileauthor' => 'admin', + 'filecomponent' => 'user', + 'expected' => false, + ], + 'user: Student user, student is author' => [ + 'currentuser' => 'student', + 'fileauthor' => 'student', + 'filecomponent' => 'user', + 'expected' => true, + ], + 'user: Student user, teacher is author' => [ + 'currentuser' => 'student', + 'fileauthor' => 'teacher', + 'filecomponent' => 'user', + 'expected' => false, + ], + + // Component = mod_h5pactivity. + 'mod_h5pactivity: Admin user is author' => [ + 'currentuser' => 'admin', + 'fileauthor' => 'admin', + 'filecomponent' => 'mod_h5pactivity', + 'expected' => true, + ], + 'mod_h5pactivity: Admin user, teacher is author' => [ + 'currentuser' => 'admin', + 'fileauthor' => 'teacher', + 'filecomponent' => 'mod_h5pactivity', + 'expected' => true, + ], + 'mod_h5pactivity: Teacher user, teacher is author' => [ + 'currentuser' => 'teacher', + 'fileauthor' => 'teacher', + 'filecomponent' => 'mod_h5pactivity', + 'expected' => true, + ], + 'mod_h5pactivity: Teacher user, admin is author' => [ + 'currentuser' => 'teacher', + 'fileauthor' => 'admin', + 'filecomponent' => 'mod_h5pactivity', + 'expected' => true, + ], + 'mod_h5pactivity: Student user, student is author' => [ + 'currentuser' => 'student', + 'fileauthor' => 'student', + 'filecomponent' => 'mod_h5pactivity', + 'expected' => false, + ], + 'mod_h5pactivity: Student user, teacher is author' => [ + 'currentuser' => 'student', + 'fileauthor' => 'teacher', + 'filecomponent' => 'mod_h5pactivity', + 'expected' => false, + ], + + // Component = mod_forum. + 'mod_forum: Admin user is author' => [ + 'currentuser' => 'admin', + 'fileauthor' => 'admin', + 'filecomponent' => 'mod_forum', + 'expected' => false, + ], + 'mod_forum: Admin user, teacher is author' => [ + 'currentuser' => 'admin', + 'fileauthor' => 'teacher', + 'filecomponent' => 'mod_forum', + 'expected' => false, + ], + + // Component = contentbank. + 'contentbank: Admin user is author' => [ + 'currentuser' => 'admin', + 'fileauthor' => 'admin', + 'filecomponent' => 'contentbank', + 'expected' => true, + ], + 'contentbank: Admin user, teacher is author' => [ + 'currentuser' => 'admin', + 'fileauthor' => 'teacher', + 'filecomponent' => 'contentbank', + 'expected' => true, + ], + 'contentbank: Teacher user, teacher is author' => [ + 'currentuser' => 'teacher', + 'fileauthor' => 'teacher', + 'filecomponent' => 'contentbank', + 'expected' => true, + ], + 'contentbank: Teacher user, admin is author' => [ + 'currentuser' => 'teacher', + 'fileauthor' => 'admin', + 'filecomponent' => 'contentbank', + 'expected' => false, + ], + 'contentbank: Student user, student is author' => [ + 'currentuser' => 'student', + 'fileauthor' => 'student', + 'filecomponent' => 'contentbank', + 'expected' => false, + ], + 'contentbank: Student user, teacher is author' => [ + 'currentuser' => 'student', + 'fileauthor' => 'teacher', + 'filecomponent' => 'contentbank', + 'expected' => false, + ], + + // Unexisting components. + 'Unexisting component' => [ + 'currentuser' => 'admin', + 'fileauthor' => 'admin', + 'filecomponent' => 'unexisting_component', + 'expected' => false, + ], + 'Unexisting module activity' => [ + 'currentuser' => 'admin', + 'fileauthor' => 'admin', + 'filecomponent' => 'mod_unexisting', + 'expected' => false, + ], + ]; + } + /** * Test the behaviour of create_content_from_pluginfile_url(). */ diff --git a/h5p/upgrade.txt b/h5p/upgrade.txt index d3e1572eaa1..4e7fbf614de 100644 --- a/h5p/upgrade.txt +++ b/h5p/upgrade.txt @@ -1,6 +1,9 @@ This files describes API changes in core libraries and APIs, information provided here is intended especially for developers. +=== 4.0 === +* Added new methods to api: get_original_content_from_pluginfile_url and can_edit_content. + === 3.11 === * Added $skipcapcheck parameter to H5P constructor, api::create_content_from_pluginfile_url() and api::get_content_from_pluginfile_url() to let skip capabilities check to get the pluginfile URL. From 86b06a7b9a96e7515883ec1f333ba9f5137d7bc9 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Thu, 10 Jun 2021 17:07:30 +0200 Subject: [PATCH 2/3] MDL-71885 core_h5p: Add the form for editing content The editor form is based on the code that Victor Deniz prepared while he was working on the integration of the H5P editor into Moodle. The original version of this file can be found in MDL-67814. --- h5p/classes/form/editcontent_form.php | 83 +++++++++++++++++++ h5p/edit.php | 114 ++++++++++++++++++++++++++ h5p/upgrade.txt | 1 + lang/en/h5p.php | 3 + 4 files changed, 201 insertions(+) create mode 100644 h5p/classes/form/editcontent_form.php create mode 100644 h5p/edit.php diff --git a/h5p/classes/form/editcontent_form.php b/h5p/classes/form/editcontent_form.php new file mode 100644 index 00000000000..f9a1dca1993 --- /dev/null +++ b/h5p/classes/form/editcontent_form.php @@ -0,0 +1,83 @@ +. + +namespace core_h5p\form; + +use core_h5p\editor; + +/** + * Form to edit an existing H5P content. + * + * @package core_h5p + * @copyright 2020 Victor Deniz + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class editcontent_form extends \moodleform { + + /** @var editor H5P editor object */ + private $editor; + + /** + * The form definition. + */ + public function definition() { + + $mform = $this->_form; + $id = $this->_customdata['id'] ?? null; + $contenturl = $this->_customdata['contenturl'] ?? null; + $returnurl = $this->_customdata['returnurl'] ?? null; + + $editor = new editor(); + + if ($id) { + $mform->addElement('hidden', 'id', $id); + $mform->setType('id', PARAM_INT); + + $editor->set_content($id); + } + + if ($contenturl) { + $mform->addElement('hidden', 'url', $contenturl); + $mform->setType('url', PARAM_LOCALURL); + } + + if ($returnurl) { + $mform->addElement('hidden', 'returnurl', $returnurl); + $mform->setType('returnurl', PARAM_LOCALURL); + } + + $this->editor = $editor; + $mformid = 'h5peditor'; + $mform->setAttributes(array('id' => $mformid) + $mform->getAttributes()); + + $this->add_action_buttons(); + + $editor->add_editor_to_form($mform); + + $this->add_action_buttons(); + } + + /** + * Updates an H5P content. + * + * @param \stdClass $data Object with all the H5P data. + * + * @return void + */ + public function save_h5p(\stdClass $data): void { + $this->editor->save_content($data); + } +} diff --git a/h5p/edit.php b/h5p/edit.php new file mode 100644 index 00000000000..a524967736e --- /dev/null +++ b/h5p/edit.php @@ -0,0 +1,114 @@ +. + +/** + * Open the editor to modify an H5P content from a given H5P URL. + * + * @package core_h5p + * @copyright 2021 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../config.php'); +require_once("$CFG->libdir/formslib.php"); +require_once("$CFG->libdir/filestorage/file_storage.php"); + +require_login(null, false); + +$contenturl = required_param('url', PARAM_LOCALURL); +$returnurl = optional_param('returnurl', null, PARAM_LOCALURL); + +// If no returnurl is defined, use local_referer. +if (empty($returnurl)) { + $returnurl = get_local_referer(false); + if (empty($returnurl)) { + // If local referer is empty, returnurl will be set to default site page. + $returnurl = new \moodle_url('/'); + } +} + +$contentid = null; +$isreferenced = false; +$context = \context_system::instance(); +if (!empty($contenturl)) { + list($originalfile, $h5p, $file) = \core_h5p\api::get_original_content_from_pluginfile_url($contenturl); + $isreferenced = ($file !== false); + if ($originalfile) { + // Check if the user can edit the content behind the given URL. + if (\core_h5p\api::can_edit_content($originalfile)) { + if (!$h5p) { + // This H5P file hasn't been deployed yet, so it should be saved to create the entry into the H5P DB. + \core_h5p\local\library\autoloader::register(); + $factory = new \core_h5p\factory(); + $config = new \stdClass(); + $onlyupdatelibs = !\core_h5p\helper::can_update_library($originalfile); + $contentid = \core_h5p\helper::save_h5p($factory, $originalfile, $config, $onlyupdatelibs, false); + } else { + // The H5P content exists. Update the contentid value. + $contentid = $h5p->id; + } + } + if ($file) { + list($context, $course, $cm) = get_context_info_array($file->get_contextid()); + if ($course) { + $context = \context_course::instance($course->id); + } + } else { + list($context, $course, $cm) = get_context_info_array($originalfile->get_contextid()); + if ($course) { + $context = \context_course::instance($course->id); + } + } + } +} + +if (empty($contentid)) { + throw new \moodle_exception('error:emptycontentid', 'core_h5p', $returnurl); +} + +$pagetitle = get_string('h5peditor', 'core_h5p'); +$url = new \moodle_url("/h5p/edit.php"); + +$PAGE->set_context($context); +$PAGE->set_url($url); +$PAGE->set_title($pagetitle); +$PAGE->set_heading($pagetitle); + +$values = [ + 'id' => $contentid, + 'contenturl' => $contenturl, + 'returnurl' => $returnurl, +]; + +$form = new \core_h5p\form\editcontent_form(null, $values); +if ($form->is_cancelled()) { + redirect($returnurl); +} else if ($data = $form->get_data()) { + $form->save_h5p($data); + if (!empty($returnurl)) { + redirect($returnurl); + } +} + +echo $OUTPUT->header(); + +if ($isreferenced) { + echo $OUTPUT->notification(get_string('contentinuse', 'core_h5p'), 'info'); +} + +$form->display(); + +echo $OUTPUT->footer(); diff --git a/h5p/upgrade.txt b/h5p/upgrade.txt index 4e7fbf614de..03828711b38 100644 --- a/h5p/upgrade.txt +++ b/h5p/upgrade.txt @@ -3,6 +3,7 @@ information provided here is intended especially for developers. === 4.0 === * Added new methods to api: get_original_content_from_pluginfile_url and can_edit_content. +* Added edit.php and editcontent_form class, for modifying H5P content given an H5P identifier (from the h5p table). === 3.11 === * Added $skipcapcheck parameter to H5P constructor, api::create_content_from_pluginfile_url() and diff --git a/lang/en/h5p.php b/lang/en/h5p.php index 73c67156e05..84a0fefda11 100644 --- a/lang/en/h5p.php +++ b/lang/en/h5p.php @@ -59,6 +59,7 @@ $string['connectionLost'] = 'Connection lost. Results will be stored and sent wh $string['connectionReestablished'] = 'Connection reestablished.'; $string['contentCopied'] = 'Content is copied to the clipboard'; $string['contentchanged'] = 'This content has changed since you last used it.'; +$string['contentinuse'] = 'This content may be in use in other places.'; $string['contenttype'] = 'Content type'; $string['copyright'] = 'Rights of use'; $string['copyrightinfo'] = 'Copyright information'; @@ -78,6 +79,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['error:emptycontentid'] = 'The given URL is incorrect or you cannot edit this file.'; $string['eventh5pviewed'] = 'H5P content viewed'; $string['eventh5pdeleted'] = 'H5P deleted'; $string['feature'] = 'Feature'; @@ -88,6 +90,7 @@ $string['filter_displayh5p_description'] = 'The Display H5P filter converts URLs $string['fullscreen'] = 'Fullscreen'; $string['gpl'] = 'General Public License v3'; $string['h5p'] = 'H5P'; +$string['h5peditor'] = 'H5P Editor'; $string['h5ptitle'] = 'Visit h5p.org to check out more content.'; $string['h5pfilenotfound'] = 'H5P file not found'; $string['h5pinvalidurl'] = 'Invalid H5P content URL.'; From 692abf2c464c20d83753f0365cd14344607623e8 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Thu, 10 Jun 2021 17:58:49 +0200 Subject: [PATCH 3/3] MDL-71885 core_h5p: Display the edit content button A new parameter has been added to the display method, to define whether the edit button should be displayed or not. The H5P activity will display this button (if the user has the required permissions). However, it won't be displayed when previewing H5P in the content bank. --- h5p/classes/player.php | 16 +- h5p/templates/h5pembed.mustache | 5 + h5p/upgrade.txt | 2 + lang/en/h5p.php | 1 + .../behat/inline_editing_content.feature | 210 ++++++++++++++++++ mod/h5pactivity/view.php | 2 +- 6 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 mod/h5pactivity/tests/behat/inline_editing_content.feature diff --git a/h5p/classes/player.php b/h5p/classes/player.php index bbd523e7bc2..040246053a9 100644 --- a/h5p/classes/player.php +++ b/h5p/classes/player.php @@ -153,12 +153,14 @@ class player { * @param stdClass $config Configuration for H5P buttons. * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions * @param string $component optional moodle component to sent xAPI tracking + * @param bool $displayedit Whether the edit button should be displayed below the H5P content. * * @return string The embedable code to display a H5P file. */ public static function display(string $url, \stdClass $config, bool $preventredirect = true, - string $component = ''): string { - global $OUTPUT; + string $component = '', bool $displayedit = false): string { + global $OUTPUT, $CFG; + $params = [ 'url' => $url, 'preventredirect' => $preventredirect, @@ -176,6 +178,16 @@ class player { $template = new \stdClass(); $template->embedurl = $fileurl->out(false); + if ($displayedit) { + list($originalfile, $h5p) = api::get_original_content_from_pluginfile_url($url, $preventredirect, true); + if ($originalfile) { + // Check if the user can edit this content. + if (api::can_edit_content($originalfile)) { + $template->editurl = $CFG->wwwroot . '/h5p/edit.php?url=' . $url; + } + } + } + $result = $OUTPUT->render_from_template('core_h5p/h5pembed', $template); $result .= self::get_resize_code(); return $result; diff --git a/h5p/templates/h5pembed.mustache b/h5p/templates/h5pembed.mustache index 1a64f3cda1d..2d0983833e0 100644 --- a/h5p/templates/h5pembed.mustache +++ b/h5p/templates/h5pembed.mustache @@ -24,6 +24,7 @@ Variables required for this template: * embedurl - The URL with the H5P file to embed + * editurl - The URL for the edit button; if it's empty, the edit button won't be displayed. Example context (json): { @@ -35,3 +36,7 @@ allowfullscreen="allowfullscreen" class="h5p-player w-100 border-0" style="height: 0px;" id="{{uniqid}}-h5player"> + +{{#editurl}} + {{#str}}editcontent, core_h5p{{/str}} +{{/editurl}} diff --git a/h5p/upgrade.txt b/h5p/upgrade.txt index 03828711b38..8e811526904 100644 --- a/h5p/upgrade.txt +++ b/h5p/upgrade.txt @@ -4,6 +4,8 @@ information provided here is intended especially for developers. === 4.0 === * Added new methods to api: get_original_content_from_pluginfile_url and can_edit_content. * Added edit.php and editcontent_form class, for modifying H5P content given an H5P identifier (from the h5p table). +* Added a new parameter to the player::display method, to define whether the edit button should be displayed below the +H5P content or not. Default value for this parameter is false. === 3.11 === * Added $skipcapcheck parameter to H5P constructor, api::create_content_from_pluginfile_url() and diff --git a/lang/en/h5p.php b/lang/en/h5p.php index 84a0fefda11..80d2f4d948f 100644 --- a/lang/en/h5p.php +++ b/lang/en/h5p.php @@ -76,6 +76,7 @@ $string['description'] = 'Description'; $string['disablefullscreen'] = 'Disable fullscreen'; $string['download'] = 'Download'; $string['downloadtitle'] = 'Download this content as a H5P file.'; +$string['editcontent'] = 'Edit H5P content'; $string['editor'] = 'Editor'; $string['embed'] = 'Embed'; $string['embedtitle'] = 'View the embed code for this content.'; diff --git a/mod/h5pactivity/tests/behat/inline_editing_content.feature b/mod/h5pactivity/tests/behat/inline_editing_content.feature new file mode 100644 index 00000000000..7dd070d1bb1 --- /dev/null +++ b/mod/h5pactivity/tests/behat/inline_editing_content.feature @@ -0,0 +1,210 @@ +@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe +Feature: Inline editing H5P content + In order to edit an existing H5P activity file + As a teacher + I need to see the button and access to the H5P editor + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | teacher2 | Teacher | 2 | teacher2@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | teacher2 | C1 | editingteacher | + | student1 | C1 | student | + And the following "permission overrides" exist: + | capability | permission | role | contextlevel | reference | + | moodle/h5p:updatelibraries | Allow | editingteacher | System | | + + @javascript + Scenario: Add H5P activity using link to content bank file + Given the following "contentbank content" exist: + | contextlevel | reference | contenttype | user | contentname | filepath | + | Course | C1 | contenttype_h5p | teacher1 | Greeting card | /h5p/tests/fixtures/greeting-card-887.h5p | + And I log in as "admin" + # Add the navigation block. + And I am on "Course 1" course homepage with editing mode on + And I add the "Navigation" block if not present + # Create an H5P activity with a link to the content-bank file. + And I add a "H5P" to section "1" + And I set the following fields to these values: + | Name | H5P package added as link to content bank | + | Description | Description | + And I click on "Add..." "button" in the "Package file" "form_row" + And I select "Content bank" repository in file picker + And I click on "Greeting card" "file" in repository content area + And I click on "Link to the file" "radio" + And I click on "Select this file" "button" + And I click on "Save and display" "button" + And I switch to "h5p-player" class iframe + And I switch to "h5p-iframe" class iframe + And I should see "Hello world!" + And I switch to the main frame + # Modify the H5P content using the edit button (which opens the H5P editor). + And I follow "Edit H5P content" + And I should see "This content may be in use in other places." + And I switch to "h5p-editor-iframe" class iframe + And I set the field "Greeting text" to "It's a Wonderful Life!" + And I switch to the main frame + And I click on "Save changes" "button" + And I switch to "h5p-player" class iframe + And I switch to "h5p-iframe" class iframe + # Check the H5P content has changed. + And I should not see "Hello world!" + And I should see "It's a Wonderful Life!" + And I switch to the main frame + # Check the H5P has also changed into the content bank. + And I am on "Course 1" course homepage + And I click on "Site pages" "list_item" in the "Navigation" "block" + And I click on "Content bank" "link" in the "Navigation" "block" + And I click on "Greeting card" "link" + And I switch to "h5p-player" class iframe + And I switch to "h5p-iframe" class iframe + And I should not see "Hello world!" + And I should see "It's a Wonderful Life!" + And I switch to the main frame + And I log out + # Check teacher1 can see the Edit button (because she is the author of this file in the content bank). + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I follow "H5P package added as link to content bank" + And I should see "Edit H5P content" + And I log out + # Check teacher2 can't see the Edit button (because the file was created by the teacher1). + And I log in as "teacher2" + And I am on "Course 1" course homepage with editing mode on + When I follow "H5P package added as link to content bank" + Then I should not see "Edit H5P content" + And I log out + # Check student1 can't see the Edit button. + And I log in as "student1" + And I am on "Course 1" course homepage with editing mode on + And I follow "H5P package added as link to content bank" + And I should not see "Edit H5P content" + + @javascript + Scenario: Add H5P activity using copy to content bank file + Given the following "contentbank content" exist: + | contextlevel | reference | contenttype | user | contentname | filepath | + | Course | C1 | contenttype_h5p | admin | Greeting card | /h5p/tests/fixtures/greeting-card-887.h5p | + And I log in as "admin" + # Add the navigation block. + And I am on "Course 1" course homepage with editing mode on + And I add the "Navigation" block if not present + # Create an H5P activity with a copy to the content-bank file. + And I add a "H5P" to section "1" + And I set the following fields to these values: + | Name | H5P package added as copy to content bank | + | Description | Description | + And I click on "Add..." "button" in the "Package file" "form_row" + And I select "Content bank" repository in file picker + And I click on "Greeting card" "file" in repository content area + And I click on "Make a copy of the file" "radio" + And I click on "Select this file" "button" + And I click on "Save and display" "button" + And I switch to "h5p-player" class iframe + And I switch to "h5p-iframe" class iframe + And I should see "Hello world!" + And I switch to the main frame + # Modify the H5P content using the edit button (which opens the H5P editor). + And I follow "Edit H5P content" + And I should not see "This content may be in use in other places." + And I switch to "h5p-editor-iframe" class iframe + And I set the field "Greeting text" to "The nightmare before Christmas" + And I switch to the main frame + And I click on "Save changes" "button" + # Check the H5P content has changed. + And I switch to "h5p-player" class iframe + And I switch to "h5p-iframe" class iframe + And I should not see "Hello world!" + And I should see "The nightmare before Christmas" + And I switch to the main frame + # Check the H5P has also changed into the content bank. + And I am on "Course 1" course homepage + And I click on "Site pages" "list_item" in the "Navigation" "block" + And I click on "Content bank" "link" in the "Navigation" "block" + And I click on "Greeting card" "link" + And I switch to "h5p-player" class iframe + And I switch to "h5p-iframe" class iframe + And I should see "Hello world!" + And I should not see "The nightmare before Christmas" + And I switch to the main frame + And I log out + # Check teacher1 can see the Edit button (because the file is a copy). + And I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I follow "H5P package added as copy to content bank" + And I should see "Edit H5P content" + And I log out + # Check teacher2 can also see the Edit button (because the file is a copy). + And I log in as "teacher2" + And I am on "Course 1" course homepage with editing mode on + When I follow "H5P package added as copy to content bank" + Then I should see "Edit H5P content" + And I log out + # Check student1 can't see the Edit button. + And I log in as "student1" + And I am on "Course 1" course homepage with editing mode on + And I follow "H5P package added as copy to content bank" + And I should not see "Edit H5P content" + + @javascript + Scenario: Add H5P activity using private user file + Given I log in as "teacher1" + # Upload the H5P to private user files. + And I follow "Manage private files..." + And I upload "h5p/tests/fixtures/greeting-card-887.h5p" file to "Files" filemanager + And I click on "Save changes" "button" + # Create an H5P activity with a private user file. + And I am on "Course 1" course homepage with editing mode on + And I add a "H5P" to section "1" + And I set the following fields to these values: + | Name | H5P package added as private user file | + | Description | Description | + And I click on "Add..." "button" in the "Package file" "form_row" + And I select "Private files" repository in file picker + And I click on "greeting-card-887.h5p" "file" in repository content area + And I click on "Link to the file" "radio" + And I click on "Select this file" "button" + And I click on "Save and display" "button" + And I switch to "h5p-player" class iframe + And I switch to "h5p-iframe" class iframe + And I should see "Hello world!" + And I switch to the main frame + # Modify the H5P content using the edit button (which opens the H5P editor). + And I follow "Edit H5P content" + And I should see "This content may be in use in other places." + And I switch to "h5p-editor-iframe" class iframe + And I set the field "Greeting text" to "Little women" + And I switch to the main frame + And I click on "Save changes" "button" + # Check the H5P content has changed. + And I switch to "h5p-player" class iframe + And I switch to "h5p-iframe" class iframe + And I should not see "Hello world!" + And I should see "Little women" + And I switch to the main frame + And I log out + # Check admin can't see the Edit button (because the file belongs to teacher1). + And I log in as "admin" + And I am on "Course 1" course homepage with editing mode on + And I follow "H5P package added as private user file" + And I should not see "Edit H5P content" + And I log out + # Check teacher2 can't see the Edit button (because the file belongs to teacher1). + And I log in as "teacher2" + And I am on "Course 1" course homepage with editing mode on + When I follow "H5P package added as private user file" + Then I should not see "Edit H5P content" + And I log out + # Check student1 can't see the Edit button. + And I log in as "student1" + And I am on "Course 1" course homepage with editing mode on + And I follow "H5P package added as private user file" + And I should not see "Edit H5P content" diff --git a/mod/h5pactivity/view.php b/mod/h5pactivity/view.php index 549d3f59351..21c440ba462 100644 --- a/mod/h5pactivity/view.php +++ b/mod/h5pactivity/view.php @@ -98,6 +98,6 @@ if (!$manager->is_tracking_enabled()) { echo $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING); } -echo player::display($fileurl, $config, true, 'mod_h5pactivity'); +echo player::display($fileurl, $config, true, 'mod_h5pactivity', true); echo $OUTPUT->footer();