MDL-67795 h5p: move methods from player to helper
authorSara Arjona <sara@moodle.com>
Fri, 21 Feb 2020 08:16:00 +0000 (09:16 +0100)
committerSara Arjona <sara@moodle.com>
Thu, 23 Apr 2020 11:09:40 +0000 (13:09 +0200)
h5p/classes/api.php
h5p/classes/helper.php
h5p/classes/player.php
h5p/tests/api_test.php
h5p/tests/helper_test.php

index 983619d..691731c 100644 (file)
@@ -26,6 +26,8 @@ namespace core_h5p;
 
 defined('MOODLE_INTERNAL') || die();
 
+use core\lock\lock_config;
+
 /**
  * Contains API class for the H5P area.
  *
@@ -176,4 +178,311 @@ class api {
 
         return $libraries;
     }
+
+    /**
+     * Get the H5P DB instance id for a H5P pluginfile URL. If it doesn't exist, it's not created.
+     *
+     * @param string $url H5P pluginfile URL.
+     * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions
+     *
+     * @return array of [file, stdClass|false]:
+     *             - file local file for this $url.
+     *             - stdClass is an H5P object or false if there isn't any H5P with this URL.
+     */
+    public static function get_content_from_pluginfile_url(string $url, bool $preventredirect = true): array {
+        global $DB;
+
+        // Deconstruct the URL and get the pathname associated.
+        $pathnamehash = self::get_pluginfile_hash($url, $preventredirect);
+        if (!$pathnamehash) {
+            return [false, false];
+        }
+
+        // Get the file.
+        $fs = get_file_storage();
+        $file = $fs->get_file_by_hash($pathnamehash);
+        if (!$file) {
+            return [false, false];
+        }
+
+        $h5p = $DB->get_record('h5p', ['pathnamehash' => $pathnamehash]);
+        return [$file, $h5p];
+    }
+
+    /**
+     * 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.
+     * - If the content is the same, returns the H5P identifier.
+     *
+     * @param string $url H5P pluginfile URL.
+     * @param stdClass $config Configuration for H5P buttons.
+     * @param factory $factory The \core_h5p\factory object
+     * @param stdClass $messages The error, exception and info messages, raised while preparing and running an H5P content.
+     * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions
+     *
+     * @return array of [file, h5pid]:
+     *             - file local file for this $url.
+     *             - h5pid is the H5P identifier or false if there isn't any H5P with this URL.
+     */
+    public static function create_content_from_pluginfile_url(string $url, \stdClass $config, factory $factory,
+        \stdClass &$messages, bool $preventredirect = true): array {
+        global $USER;
+
+        $core = $factory->get_core();
+        list($file, $h5p) = self::get_content_from_pluginfile_url($url, $preventredirect);
+
+        if (!$file) {
+            $core->h5pF->setErrorMessage(get_string('h5pfilenotfound', 'core_h5p'));
+            return [false, false];
+        }
+
+        $contenthash = $file->get_contenthash();
+        if ($h5p && $h5p->contenthash != $contenthash) {
+            // The content exists and it is different from the one deployed previously. The existing one should be removed before
+            // deploying the new version.
+            self::delete_content($h5p, $factory);
+            $h5p = false;
+        }
+
+        $context = \context::instance_by_id($file->get_contextid());
+        if ($h5p) {
+            // The H5P content has been deployed previously.
+            $displayoptions = helper::get_display_options($core, $config);
+            // Check if the user can set the displayoptions.
+            if ($displayoptions != $h5p->displayoptions && has_capability('moodle/h5p:setdisplayoptions', $context)) {
+                // If the displayoptions has changed and the user has permission to modify it, update this information in the DB.
+                $core->h5pF->updateContentFields($h5p->id, ['displayoptions' => $displayoptions]);
+            }
+            return [$file, $h5p->id];
+        } else {
+            // The H5P content hasn't been deployed previously.
+
+            // Check if the user uploading the H5P content is "trustable". If the file hasn't been uploaded by a user with this
+            // capability, the content won't be deployed and an error message will be displayed.
+            if (!helper::can_deploy_package($file)) {
+                $core->h5pF->setErrorMessage(get_string('nopermissiontodeploy', 'core_h5p'));
+                return [$file, false];
+            }
+
+            // The H5P content can be only deployed if the author of the .h5p file can update libraries or if all the
+            // content-type libraries exist, to avoid users without the h5p:updatelibraries capability upload malicious content.
+            $onlyupdatelibs = !helper::can_update_library($file);
+
+            // Start lock to prevent synchronous access to save the same H5P.
+            $lockfactory = lock_config::get_lock_factory('core_h5p');
+            $lockkey = 'core_h5p_' . $file->get_pathnamehash();
+            if ($lock = $lockfactory->get_lock($lockkey, 10)) {
+                try {
+                    // Validate and store the H5P content before displaying it.
+                    $h5pid = helper::save_h5p($factory, $file, $config, $onlyupdatelibs, false);
+                } finally {
+                    $lock->release();
+                }
+            } else {
+                $core->h5pF->setErrorMessage(get_string('lockh5pdeploy', 'core_h5p'));
+                return [$file, false];
+            };
+
+            if (!$h5pid && $file->get_userid() != $USER->id && has_capability('moodle/h5p:updatelibraries', $context)) {
+                // The user has permission to update libraries but the package has been uploaded by a different
+                // user without this permission. Check if there is some missing required library error.
+                $missingliberror = false;
+                $messages = helper::get_messages($messages, $factory);
+                if (!empty($messages->error)) {
+                    foreach ($messages->error as $error) {
+                        if ($error->code == 'missing-required-library') {
+                            $missingliberror = true;
+                            break;
+                        }
+                    }
+                }
+                if ($missingliberror) {
+                    // The message about the permissions to upload libraries should be removed.
+                    $infomsg = "Note that the libraries may exist in the file you uploaded, but you're not allowed to upload " .
+                        "new libraries. Contact the site administrator about this.";
+                    if (($key = array_search($infomsg, $messages->info)) !== false) {
+                        unset($messages->info[$key]);
+                    }
+
+                    // No library will be installed and an error will be displayed, because this content is not trustable.
+                    $core->h5pF->setInfoMessage(get_string('notrustablefile', 'core_h5p'));
+                }
+                return [$file, false];
+
+            }
+            return [$file, $h5pid];
+        }
+    }
+
+    /**
+     * Delete an H5P package.
+     *
+     * @param stdClass $content The H5P package to delete with, at least content['id].
+     * @param factory $factory The \core_h5p\factory object
+     */
+    public static function delete_content(\stdClass $content, factory $factory): void {
+        $h5pstorage = $factory->get_storage();
+
+        // Add an empty slug to the content if it's not defined, because the H5P library requires this field exists.
+        // It's not used when deleting a package, so the real slug value is not required at this point.
+        $content->slug = $content->slug ?? '';
+        $h5pstorage->deletePackage( (array) $content);
+    }
+
+    /**
+     * Delete an H5P package deployed from the defined $url.
+     *
+     * @param string $url pluginfile URL of the H5P package to delete.
+     * @param factory $factory The \core_h5p\factory object
+     */
+    public static function delete_content_from_pluginfile_url(string $url, factory $factory): void {
+        // Get the H5P to delete.
+        list($file, $h5p) = self::get_content_from_pluginfile_url($url);
+        if ($h5p) {
+            self::delete_content($h5p, $factory);
+        }
+    }
+
+    /**
+     * Get the pathnamehash from an H5P internal URL.
+     *
+     * @param  string $url H5P pluginfile URL poiting to an H5P file.
+     * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions
+     *
+     * @return string|false pathnamehash for the file in the internal URL.
+     */
+    protected static function get_pluginfile_hash(string $url, bool $preventredirect = true) {
+        global $USER, $CFG;
+
+        // Decode the URL before start processing it.
+        $url = new \moodle_url(urldecode($url));
+
+        // Remove params from the URL (such as the 'forcedownload=1'), to avoid errors.
+        $url->remove_params(array_keys($url->params()));
+        $path = $url->out_as_local_url();
+
+        // We only need the slasharguments.
+        $path = substr($path, strpos($path, '.php/') + 5);
+        $parts = explode('/', $path);
+        $filename = array_pop($parts);
+
+        // If the request is made by tokenpluginfile.php we need to avoid userprivateaccesskey.
+        if (strpos($url, '/tokenpluginfile.php')) {
+            array_shift($parts);
+        }
+
+        // Get the contextid, component and filearea.
+        $contextid = array_shift($parts);
+        $component = array_shift($parts);
+        $filearea = array_shift($parts);
+
+        // Ignore draft files, because they are considered temporary files, so shouldn't be displayed.
+        if ($filearea == 'draft') {
+            return false;
+        }
+
+        // Get the context.
+        try {
+            list($context, $course, $cm) = get_context_info_array($contextid);
+        } catch (\moodle_exception $e) {
+            throw new \moodle_exception('invalidcontextid', 'core_h5p');
+        }
+
+        // For CONTEXT_USER, such as the private files, raise an exception if the owner of the file is not the current user.
+        if ($context->contextlevel == CONTEXT_USER && $USER->id !== $context->instanceid) {
+            throw new \moodle_exception('h5pprivatefile', 'core_h5p');
+        }
+
+        // For CONTEXT_COURSECAT No login necessary - unless login forced everywhere.
+        if ($context->contextlevel == CONTEXT_COURSECAT) {
+            if ($CFG->forcelogin) {
+                require_login(null, true, null, false, true);
+            }
+        }
+
+        // For CONTEXT_BLOCK.
+        if ($context->contextlevel == CONTEXT_BLOCK) {
+            if ($context->get_course_context(false)) {
+                // If block is in course context, then check if user has capability to access course.
+                require_course_login($course, true, null, false, true);
+            } else if ($CFG->forcelogin) {
+                // No login necessary - unless login forced everywhere.
+                require_login(null, true, null, false, true);
+            } else {
+                // Get parent context and see if user have proper permission.
+                $parentcontext = $context->get_parent_context();
+                if ($parentcontext->contextlevel === CONTEXT_COURSECAT) {
+                    // Check if category is visible and user can view this category.
+                    if (!core_course_category::get($parentcontext->instanceid, IGNORE_MISSING)) {
+                        send_file_not_found();
+                    }
+                } else if ($parentcontext->contextlevel === CONTEXT_USER && $parentcontext->instanceid != $USER->id) {
+                    // The block is in the context of a user, it is only visible to the user who it belongs to.
+                    send_file_not_found();
+                }
+                if ($filearea !== 'content') {
+                    send_file_not_found();
+                }
+            }
+        }
+
+        // For CONTEXT_MODULE and CONTEXT_COURSE check if the user is enrolled in the course.
+        // And for CONTEXT_MODULE has permissions view this .h5p file.
+        if ($context->contextlevel == CONTEXT_MODULE ||
+                $context->contextlevel == CONTEXT_COURSE) {
+            // Require login to the course first (without login to the module).
+            require_course_login($course, true, null, !$preventredirect, $preventredirect);
+
+            // Now check if module is available OR it is restricted but the intro is shown on the course page.
+            if ($context->contextlevel == CONTEXT_MODULE) {
+                $cminfo = \cm_info::create($cm);
+                if (!$cminfo->uservisible) {
+                    if (!$cm->showdescription || !$cminfo->is_visible_on_course_page()) {
+                        // Module intro is not visible on the course page and module is not available, show access error.
+                        require_course_login($course, true, $cminfo, !$preventredirect, $preventredirect);
+                    }
+                }
+            }
+        }
+
+        // Some components, such as mod_page or mod_resource, add the revision to the URL to prevent caching problems.
+        // So the URL contains this revision number as itemid but a 0 is always stored in the files table.
+        // In order to get the proper hash, a callback should be done (looking for those exceptions).
+        $pathdata = null;
+        if ($context->contextlevel == CONTEXT_MODULE || $context->contextlevel == CONTEXT_BLOCK) {
+            $pathdata = component_callback($component, 'get_path_from_pluginfile', [$filearea, $parts], null);
+        }
+        if (null === $pathdata) {
+            // Look for the components and fileareas which have empty itemid defined in xxx_pluginfile.
+            $hasnullitemid = false;
+            $hasnullitemid = $hasnullitemid || ($component === 'user' && ($filearea === 'private' || $filearea === 'profile'));
+            $hasnullitemid = $hasnullitemid || (substr($component, 0, 4) === 'mod_' && $filearea === 'intro');
+            $hasnullitemid = $hasnullitemid || ($component === 'course' &&
+                    ($filearea === 'summary' || $filearea === 'overviewfiles'));
+            $hasnullitemid = $hasnullitemid || ($component === 'coursecat' && $filearea === 'description');
+            $hasnullitemid = $hasnullitemid || ($component === 'backup' &&
+                    ($filearea === 'course' || $filearea === 'activity' || $filearea === 'automated'));
+            if ($hasnullitemid) {
+                $itemid = 0;
+            } else {
+                $itemid = array_shift($parts);
+            }
+
+            if (empty($parts)) {
+                $filepath = '/';
+            } else {
+                $filepath = '/' . implode('/', $parts) . '/';
+            }
+        } else {
+            // The itemid and filepath have been returned by the component callback.
+            [
+                'itemid' => $itemid,
+                'filepath' => $filepath,
+            ] = $pathdata;
+        }
+
+        $fs = get_file_storage();
+        $pathnamehash = $fs->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
+        return $pathnamehash;
+    }
 }
index b561b5e..f34236e 100644 (file)
@@ -83,6 +83,33 @@ class helper {
     }
 
 
+    /**
+     * Get the error messages stored in our H5P framework.
+     *
+     * @param stdClass $messages The error, exception and info messages, raised while preparing and running an H5P content.
+     * @param factory $factory The \core_h5p\factory object
+     *
+     * @return stdClass with framework error messages.
+     */
+    public static function get_messages(\stdClass $messages, factory $factory): \stdClass {
+        $core = $factory->get_core();
+
+        // Check if there are some errors and store them in $messages.
+        if (empty($messages->error)) {
+            $messages->error = $core->h5pF->getMessages('error') ?: false;
+        } else {
+            $messages->error = array_merge($messages->error, $core->h5pF->getMessages('error'));
+        }
+
+        if (empty($messages->info)) {
+            $messages->info = $core->h5pF->getMessages('info') ?: false;
+        } else {
+            $messages->info = array_merge($messages->info, $core->h5pF->getMessages('info'));
+        }
+
+        return $messages;
+    }
+
     /**
      * Get the representation of display options as int.
      *
index e8c68a6..23c5420 100644 (file)
@@ -28,7 +28,6 @@ defined('MOODLE_INTERNAL') || die();
 
 use core_h5p\local\library\autoloader;
 use core_xapi\local\statement\item_activity;
-use core\lock\lock_config;
 
 /**
  * H5P player class, for displaying any local H5P content.
@@ -124,12 +123,21 @@ class player {
         $this->core = $this->factory->get_core();
 
         // Get the H5P identifier linked to this URL.
-        if ($this->h5pid = $this->get_h5p_id($url, $config)) {
-            // Load the content of the H5P content associated to this $url.
-            $this->content = $this->core->loadContent($this->h5pid);
-
-            // Get the embedtype to use for displaying the H5P content.
-            $this->embedtype = core::determineEmbedType($this->content['embedType'], $this->content['library']['embedTypes']);
+        list($file, $this->h5pid) = api::create_content_from_pluginfile_url(
+            $url,
+            $config,
+            $this->factory,
+            $this->messages
+        );
+        if ($file) {
+            $this->context = \context::instance_by_id($file->get_contextid());
+            if ($this->h5pid) {
+                // Load the content of the H5P content associated to this $url.
+                $this->content = $this->core->loadContent($this->h5pid);
+
+                // Get the embedtype to use for displaying the H5P content.
+                $this->embedtype = core::determineEmbedType($this->content['embedType'], $this->content['library']['embedTypes']);
+            }
         }
     }
 
@@ -174,20 +182,7 @@ class player {
      * @return stdClass with framework error messages.
      */
     public function get_messages(): \stdClass {
-        // Check if there are some errors and store them in $messages.
-        if (empty($this->messages->error)) {
-            $this->messages->error = $this->core->h5pF->getMessages('error') ?: false;
-        } else {
-            $this->messages->error = array_merge($this->messages->error, $this->core->h5pF->getMessages('error'));
-        }
-
-        if (empty($this->messages->info)) {
-            $this->messages->info = $this->core->h5pF->getMessages('info') ?: false;
-        } else {
-            $this->messages->info = array_merge($this->messages->info, $this->core->h5pF->getMessages('info'));
-        }
-
-        return $this->messages;
+        return helper::get_messages($this->messages, $this->factory);
     }
 
     /**
@@ -249,7 +244,7 @@ class player {
         \core_h5p\event\h5p_viewed::create([
             'objectid' => $this->h5pid,
             'userid' => $USER->id,
-            'context' => $this->context,
+            'context' => $this->get_context(),
             'other' => [
                 'url' => $this->url->out(),
                 'time' => time()
@@ -277,277 +272,6 @@ class player {
         return $this->context;
     }
 
-    /**
-     * Get the H5P DB instance id for a H5P pluginfile URL. The H5P file will be saved if it doesn't exist previously or
-     * if its content has changed. Besides, the displayoptions in the $config will be also updated when they have changed and
-     * the user has the right permissions.
-     *
-     * @param string $url H5P pluginfile URL.
-     * @param stdClass $config Configuration for H5P buttons.
-     *
-     * @return int|false H5P DB identifier.
-     */
-    private function get_h5p_id(string $url, \stdClass $config) {
-        global $DB, $USER;
-
-        $fs = get_file_storage();
-
-        // Deconstruct the URL and get the pathname associated.
-        $pathnamehash = $this->get_pluginfile_hash($url);
-        if (!$pathnamehash) {
-            $this->core->h5pF->setErrorMessage(get_string('h5pfilenotfound', 'core_h5p'));
-            return false;
-        }
-
-        // Get the file.
-        $file = $fs->get_file_by_hash($pathnamehash);
-        if (!$file) {
-            $this->core->h5pF->setErrorMessage(get_string('h5pfilenotfound', 'core_h5p'));
-            return false;
-        }
-
-        $h5p = $DB->get_record('h5p', ['pathnamehash' => $pathnamehash]);
-        $contenthash = $file->get_contenthash();
-        if ($h5p && $h5p->contenthash != $contenthash) {
-            // The content exists and it is different from the one deployed previously. The existing one should be removed before
-            // deploying the new version.
-            $this->delete_h5p($h5p);
-            $h5p = false;
-        }
-
-        if ($h5p) {
-            // The H5P content has been deployed previously.
-            $displayoptions = $this->get_display_options($config);
-            // Check if the user can set the displayoptions.
-            if ($displayoptions != $h5p->displayoptions && has_capability('moodle/h5p:setdisplayoptions', $this->context)) {
-                // If the displayoptions has changed and the user has permission to modify it, update this information in the DB.
-                $this->core->h5pF->updateContentFields($h5p->id, ['displayoptions' => $displayoptions]);
-            }
-            return $h5p->id;
-        } else {
-            // The H5P content hasn't been deployed previously.
-
-            // Check if the user uploading the H5P content is "trustable". If the file hasn't been uploaded by a user with this
-            // capability, the content won't be deployed and an error message will be displayed.
-            if (!helper::can_deploy_package($file)) {
-                $this->core->h5pF->setErrorMessage(get_string('nopermissiontodeploy', 'core_h5p'));
-                return false;
-            }
-
-            // The H5P content can be only deployed if the author of the .h5p file can update libraries or if all the
-            // content-type libraries exist, to avoid users without the h5p:updatelibraries capability upload malicious content.
-            $onlyupdatelibs = !helper::can_update_library($file);
-
-            // Start lock to prevent synchronous access to save the same h5p.
-            $lockfactory = lock_config::get_lock_factory('core_h5p');
-            $lockkey = 'core_h5p_' . $pathnamehash;
-            if ($lock = $lockfactory->get_lock($lockkey, 10)) {
-                try {
-                    // Validate and store the H5P content before displaying it.
-                    $h5pid = helper::save_h5p($this->factory, $file, $config, $onlyupdatelibs, false);
-                } finally {
-                    $lock->release();
-                }
-            } else {
-                $this->core->h5pF->setErrorMessage(get_string('lockh5pdeploy', 'core_h5p'));
-                return false;
-            };
-            if (!$h5pid && $file->get_userid() != $USER->id && has_capability('moodle/h5p:updatelibraries', $this->context)) {
-                // The user has permission to update libraries but the package has been uploaded by a different
-                // user without this permission. Check if there is some missing required library error.
-                $missingliberror = false;
-                $messages = $this->get_messages();
-                if (!empty($messages->error)) {
-                    foreach ($messages->error as $error) {
-                        if ($error->code == 'missing-required-library') {
-                            $missingliberror = true;
-                            break;
-                        }
-                    }
-                }
-                if ($missingliberror) {
-                    // The message about the permissions to upload libraries should be removed.
-                    $infomsg = "Note that the libraries may exist in the file you uploaded, but you're not allowed to upload " .
-                        "new libraries. Contact the site administrator about this.";
-                    if (($key = array_search($infomsg, $messages->info)) !== false) {
-                        unset($messages->info[$key]);
-                    }
-
-                    // No library will be installed and an error will be displayed, because this content is not trustable.
-                    $this->core->h5pF->setInfoMessage(get_string('notrustablefile', 'core_h5p'));
-                }
-                return false;
-
-            }
-            return $h5pid;
-        }
-    }
-
-    /**
-     * Get the pathnamehash from an H5P internal URL.
-     *
-     * @param  string $url H5P pluginfile URL poiting to an H5P file.
-     *
-     * @return string|false pathnamehash for the file in the internal URL.
-     */
-    private function get_pluginfile_hash(string $url) {
-        global $USER, $CFG;
-
-        // Decode the URL before start processing it.
-        $url = new \moodle_url(urldecode($url));
-
-        // Remove params from the URL (such as the 'forcedownload=1'), to avoid errors.
-        $url->remove_params(array_keys($url->params()));
-        $path = $url->out_as_local_url();
-
-        // We only need the slasharguments.
-        $path = substr($path, strpos($path, '.php/') + 5);
-        $parts = explode('/', $path);
-        $filename = array_pop($parts);
-
-        // If the request is made by tokenpluginfile.php we need to avoid userprivateaccesskey.
-        if (strpos($this->url, '/tokenpluginfile.php')) {
-            array_shift($parts);
-        }
-        // Get the contextid, component and filearea.
-        $contextid = array_shift($parts);
-        $component = array_shift($parts);
-        $filearea = array_shift($parts);
-
-        // Ignore draft files, because they are considered temporary files, so shouldn't be displayed.
-        if ($filearea == 'draft') {
-            return false;
-        }
-
-        // Get the context.
-        try {
-            list($this->context, $course, $cm) = get_context_info_array($contextid);
-        } catch (\moodle_exception $e) {
-            throw new \moodle_exception('invalidcontextid', 'core_h5p');
-        }
-
-        // For CONTEXT_USER, such as the private files, raise an exception if the owner of the file is not the current user.
-        if ($this->context->contextlevel == CONTEXT_USER && $USER->id !== $this->context->instanceid) {
-            throw new \moodle_exception('h5pprivatefile', 'core_h5p');
-        }
-
-        // For CONTEXT_COURSECAT No login necessary - unless login forced everywhere.
-        if ($this->context->contextlevel == CONTEXT_COURSECAT) {
-            if ($CFG->forcelogin) {
-                require_login(null, true, null, false, true);
-            }
-        }
-
-        // For CONTEXT_BLOCK.
-        if ($this->context->contextlevel == CONTEXT_BLOCK) {
-            if ($this->context->get_course_context(false)) {
-                // If block is in course context, then check if user has capability to access course.
-                require_course_login($course, true, null, false, true);
-            } else if ($CFG->forcelogin) {
-                // No login necessary - unless login forced everywhere.
-                require_login(null, true, null, false, true);
-            } else {
-                // Get parent context and see if user have proper permission.
-                $parentcontext = $this->context->get_parent_context();
-                if ($parentcontext->contextlevel === CONTEXT_COURSECAT) {
-                    // Check if category is visible and user can view this category.
-                    if (!core_course_category::get($parentcontext->instanceid, IGNORE_MISSING)) {
-                        send_file_not_found();
-                    }
-                } else if ($parentcontext->contextlevel === CONTEXT_USER && $parentcontext->instanceid != $USER->id) {
-                    // The block is in the context of a user, it is only visible to the user who it belongs to.
-                    send_file_not_found();
-                }
-                if ($filearea !== 'content') {
-                    send_file_not_found();
-                }
-            }
-        }
-
-        // For CONTEXT_MODULE and CONTEXT_COURSE check if the user is enrolled in the course.
-        // And for CONTEXT_MODULE has permissions view this .h5p file.
-        if ($this->context->contextlevel == CONTEXT_MODULE ||
-                $this->context->contextlevel == CONTEXT_COURSE) {
-            // Require login to the course first (without login to the module).
-            require_course_login($course, true, null, !$this->preventredirect, $this->preventredirect);
-
-            // Now check if module is available OR it is restricted but the intro is shown on the course page.
-            if ($this->context->contextlevel == CONTEXT_MODULE) {
-                $cminfo = \cm_info::create($cm);
-                if (!$cminfo->uservisible) {
-                    if (!$cm->showdescription || !$cminfo->is_visible_on_course_page()) {
-                        // Module intro is not visible on the course page and module is not available, show access error.
-                        require_course_login($course, true, $cminfo, !$this->preventredirect, $this->preventredirect);
-                    }
-                }
-            }
-        }
-
-        // Some components, such as mod_page or mod_resource, add the revision to the URL to prevent caching problems.
-        // So the URL contains this revision number as itemid but a 0 is always stored in the files table.
-        // In order to get the proper hash, a callback should be done (looking for those exceptions).
-        $pathdata = null;
-        if ($this->context->contextlevel == CONTEXT_MODULE || $this->context->contextlevel == CONTEXT_BLOCK) {
-            $pathdata = component_callback($component, 'get_path_from_pluginfile', [$filearea, $parts], null);
-        }
-        if (null === $pathdata) {
-            // Look for the components and fileareas which have empty itemid defined in xxx_pluginfile.
-            $hasnullitemid = false;
-            $hasnullitemid = $hasnullitemid || ($component === 'user' && ($filearea === 'private' || $filearea === 'profile'));
-            $hasnullitemid = $hasnullitemid || (substr($component, 0, 4) === 'mod_' && $filearea === 'intro');
-            $hasnullitemid = $hasnullitemid || ($component === 'course' &&
-                    ($filearea === 'summary' || $filearea === 'overviewfiles'));
-            $hasnullitemid = $hasnullitemid || ($component === 'coursecat' && $filearea === 'description');
-            $hasnullitemid = $hasnullitemid || ($component === 'backup' &&
-                    ($filearea === 'course' || $filearea === 'activity' || $filearea === 'automated'));
-            if ($hasnullitemid) {
-                $itemid = 0;
-            } else {
-                $itemid = array_shift($parts);
-            }
-
-            if (empty($parts)) {
-                $filepath = '/';
-            } else {
-                $filepath = '/' . implode('/', $parts) . '/';
-            }
-        } else {
-            // The itemid and filepath have been returned by the component callback.
-            [
-                'itemid' => $itemid,
-                'filepath' => $filepath,
-            ] = $pathdata;
-        }
-
-        $fs = get_file_storage();
-        return $fs->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
-    }
-
-    /**
-     * Get the representation of display options as int.
-     * @param stdClass $config Button options config.
-     *
-     * @return int The representation of display options as int.
-     */
-    private function get_display_options(\stdClass $config): int {
-        $export = isset($config->export) ? $config->export : 0;
-        $embed = isset($config->embed) ? $config->embed : 0;
-        $copyright = isset($config->copyright) ? $config->copyright : 0;
-        $frame = ($export || $embed || $copyright);
-        if (!$frame) {
-            $frame = isset($config->frame) ? $config->frame : 0;
-        }
-
-        $disableoptions = [
-            core::DISPLAY_OPTION_FRAME     => $frame,
-            core::DISPLAY_OPTION_DOWNLOAD  => $export,
-            core::DISPLAY_OPTION_EMBED     => $embed,
-            core::DISPLAY_OPTION_COPYRIGHT => $copyright,
-        ];
-
-        return $this->core->getStorableDisplayOptions($disableoptions, 0);
-    }
-
     /**
      * Delete an H5P package.
      *
index d64e1eb..f7f01b8 100644 (file)
@@ -273,4 +273,182 @@ class api_testcase extends \advanced_testcase {
             ],
         ];
     }
+
+    /**
+     * Test the behaviour of get_content_from_pluginfile_url().
+     */
+    public function test_get_content_from_pluginfile_url(): void {
+        $this->setRunTestInSeparateProcess(true);
+        $this->resetAfterTest();
+        $factory = new factory();
+
+        // Create the H5P data.
+        $filename = 'find-the-words.h5p';
+        $path = __DIR__ . '/fixtures/' . $filename;
+        $fakefile = helper::create_fake_stored_file_from_path($path);
+        $config = (object)[
+            'frame' => 1,
+            'export' => 1,
+            'embed' => 0,
+            'copyright' => 0,
+        ];
+
+        // Get URL for this H5P content file.
+        $syscontext = \context_system::instance();
+        $url = \moodle_url::make_pluginfile_url(
+            $syscontext->id,
+            \core_h5p\file_storage::COMPONENT,
+            'unittest',
+            $fakefile->get_itemid(),
+            '/',
+            $filename
+        );
+
+        // Scenario 1: Get the H5P for this URL and check there isn't any existing H5P (because it hasn't been saved).
+        list($newfile, $h5p) = api::get_content_from_pluginfile_url($url->out());
+        $this->assertEquals($fakefile->get_pathnamehash(), $newfile->get_pathnamehash());
+        $this->assertEquals($fakefile->get_contenthash(), $newfile->get_contenthash());
+        $this->assertFalse($h5p);
+
+        // Scenario 2: Save the H5P and check now the H5P is exactly the same as the original one.
+        $h5pid = helper::save_h5p($factory, $fakefile, $config);
+        list($newfile, $h5p) = api::get_content_from_pluginfile_url($url->out());
+
+        $this->assertEquals($h5pid, $h5p->id);
+        $this->assertEquals($fakefile->get_pathnamehash(), $h5p->pathnamehash);
+        $this->assertEquals($fakefile->get_contenthash(), $h5p->contenthash);
+
+        // Scenario 3: Get the H5P for an unexisting H5P file.
+        $url = \moodle_url::make_pluginfile_url(
+            $syscontext->id,
+            \core_h5p\file_storage::COMPONENT,
+            'unittest',
+            $fakefile->get_itemid(),
+            '/',
+            'unexisting.h5p'
+        );
+        list($newfile, $h5p) = api::get_content_from_pluginfile_url($url->out());
+        $this->assertFalse($newfile);
+        $this->assertFalse($h5p);
+    }
+
+    /**
+     * Test the behaviour of create_content_from_pluginfile_url().
+     */
+    public function test_create_content_from_pluginfile_url(): void {
+        global $DB;
+
+        $this->setRunTestInSeparateProcess(true);
+        $this->resetAfterTest();
+        $factory = new factory();
+
+        // Create the H5P data.
+        $filename = 'find-the-words.h5p';
+        $path = __DIR__ . '/fixtures/' . $filename;
+        $fakefile = helper::create_fake_stored_file_from_path($path);
+        $config = (object)[
+            'frame' => 1,
+            'export' => 1,
+            'embed' => 0,
+            'copyright' => 0,
+        ];
+
+        // Get URL for this H5P content file.
+        $syscontext = \context_system::instance();
+        $url = \moodle_url::make_pluginfile_url(
+            $syscontext->id,
+            \core_h5p\file_storage::COMPONENT,
+            'unittest',
+            $fakefile->get_itemid(),
+            '/',
+            $filename
+        );
+
+        // Scenario 1: Create the H5P from this URL and check the content is exactly the same as the fake file.
+        $messages = new \stdClass();
+        list($newfile, $h5pid) = api::create_content_from_pluginfile_url($url->out(), $config, $factory, $messages);
+        $this->assertNotFalse($h5pid);
+        $h5p = $DB->get_record('h5p', ['id' => $h5pid]);
+        $this->assertEquals($fakefile->get_pathnamehash(), $h5p->pathnamehash);
+        $this->assertEquals($fakefile->get_contenthash(), $h5p->contenthash);
+        $this->assertTrue(empty($messages->error));
+        $this->assertTrue(empty($messages->info));
+
+        // Scenario 2: Create the H5P for an unexisting H5P file.
+        $url = \moodle_url::make_pluginfile_url(
+            $syscontext->id,
+            \core_h5p\file_storage::COMPONENT,
+            'unittest',
+            $fakefile->get_itemid(),
+            '/',
+            'unexisting.h5p'
+        );
+        list($newfile, $h5p) = api::create_content_from_pluginfile_url($url->out(), $config, $factory, $messages);
+        $this->assertFalse($newfile);
+        $this->assertFalse($h5p);
+        $this->assertTrue(empty($messages->error));
+        $this->assertTrue(empty($messages->info));
+    }
+
+    /**
+     * Test the behaviour of delete_content_from_pluginfile_url().
+     */
+    public function test_delete_content_from_pluginfile_url(): void {
+        global $DB;
+
+        $this->setRunTestInSeparateProcess(true);
+        $this->resetAfterTest();
+        $factory = new factory();
+
+        // Create the H5P data.
+        $filename = 'find-the-words.h5p';
+        $path = __DIR__ . '/fixtures/' . $filename;
+        $fakefile = helper::create_fake_stored_file_from_path($path);
+        $config = (object)[
+            'frame' => 1,
+            'export' => 1,
+            'embed' => 0,
+            'copyright' => 0,
+        ];
+
+        // Get URL for this H5P content file.
+        $syscontext = \context_system::instance();
+        $url = \moodle_url::make_pluginfile_url(
+            $syscontext->id,
+            \core_h5p\file_storage::COMPONENT,
+            'unittest',
+            $fakefile->get_itemid(),
+            '/',
+            $filename
+        );
+
+        // Scenario 1: Try to remove the H5P content for an undeployed file.
+        list($newfile, $h5p) = api::get_content_from_pluginfile_url($url->out());
+        $this->assertEquals(0, $DB->count_records('h5p'));
+        api::delete_content_from_pluginfile_url($url->out(), $factory);
+        $this->assertEquals(0, $DB->count_records('h5p'));
+
+        // Scenario 2: Deploy an H5P from this URL, check it's created, remove it and check it has been removed as expected.
+        $this->assertEquals(0, $DB->count_records('h5p'));
+
+        $messages = new \stdClass();
+        list($newfile, $h5pid) = api::create_content_from_pluginfile_url($url->out(), $config, $factory, $messages);
+        $this->assertEquals(1, $DB->count_records('h5p'));
+
+        api::delete_content_from_pluginfile_url($url->out(), $factory);
+        $this->assertEquals(0, $DB->count_records('h5p'));
+
+        // Scenario 3: Try to remove the H5P for an unexisting H5P URL.
+        $url = \moodle_url::make_pluginfile_url(
+            $syscontext->id,
+            \core_h5p\file_storage::COMPONENT,
+            'unittest',
+            $fakefile->get_itemid(),
+            '/',
+            'unexisting.h5p'
+        );
+        $this->assertEquals(0, $DB->count_records('h5p'));
+        api::delete_content_from_pluginfile_url($url->out(), $factory);
+        $this->assertEquals(0, $DB->count_records('h5p'));
+    }
 }
index c216a2c..734bd08 100644 (file)
@@ -290,4 +290,42 @@ class helper_testcase extends \advanced_testcase {
         $candeploy = helper::can_update_library($file);
         $this->assertTrue($candeploy);
     }
+
+    /**
+     * Test the behaviour of get_messages().
+     */
+    public function test_get_messages(): void {
+        $this->resetAfterTest();
+
+        $factory = new \core_h5p\factory();
+        $messages = new \stdClass();
+
+        helper::get_messages($messages, $factory);
+        $this->assertTrue(empty($messages->error));
+        $this->assertTrue(empty($messages->info));
+
+        // Add an some messages manually and check they are still there.
+        $messages->error['error1'] = 'Testing ERROR message';
+        $messages->info['info1'] = 'Testing INFO message';
+        $messages->info['info2'] = 'Testing INFO message';
+        helper::get_messages($messages, $factory);
+        $this->assertCount(1, $messages->error);
+        $this->assertCount(2, $messages->info);
+
+        // When saving an invalid .h5p file, 6 errors should be raised.
+        $path = __DIR__ . '/fixtures/h5ptest.zip';
+        $file = helper::create_fake_stored_file_from_path($path);
+        $factory->get_framework()->set_file($file);
+        $config = (object)[
+            'frame' => 1,
+            'export' => 1,
+            'embed' => 0,
+            'copyright' => 0,
+        ];
+        $h5pid = helper::save_h5p($factory, $file, $config);
+        $this->assertFalse($h5pid);
+        helper::get_messages($messages, $factory);
+        $this->assertCount(7, $messages->error);
+        $this->assertCount(2, $messages->info);
+    }
 }