MDL-71885 core_h5p: Add new methods to API
authorSara Arjona <sara@moodle.com>
Thu, 10 Jun 2021 14:55:09 +0000 (16:55 +0200)
committerSara Arjona <sara@moodle.com>
Mon, 5 Jul 2021 08:56:13 +0000 (10:56 +0200)
h5p/classes/api.php
h5p/tests/api_test.php
h5p/upgrade.txt

index 5eb3ecb..6aaf4ad 100644 (file)
@@ -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.
index ec7f5e5..0ab510e 100644 (file)
@@ -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().
      */
index d3e1572..4e7fbf6 100644 (file)
@@ -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.