Merge branch 'MDL-67057-master' of git://github.com/andrewnicols/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Thu, 7 Nov 2019 01:20:27 +0000 (09:20 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Thu, 7 Nov 2019 01:20:27 +0000 (09:20 +0800)
1  2 
h5p/classes/player.php

diff --combined h5p/classes/player.php
@@@ -76,10 -76,15 +76,15 @@@ class player 
      private $context;
  
      /**
-      * @var context The \core_h5p\factory object.
+      * @var factory The \core_h5p\factory object.
       */
      private $factory;
  
+     /**
+      * @var stdClass The error, exception and info messages, raised while preparing and running the player.
+      */
+     private $messages;
      /**
       * Inits the H5P player for rendering the content.
       *
@@@ -94,6 -99,8 +99,8 @@@
  
          $this->factory = new \core_h5p\factory();
  
+         $this->messages = new \stdClass();
          // Create \core_h5p\core instance.
          $this->core = $this->factory->get_core();
  
       * @return stdClass with framework error messages.
       */
      public function get_messages() : \stdClass {
-         $messages = new \stdClass();
-         $messages->error = $this->core->h5pF->getMessages('error');
+         // 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($messages->error)) {
-             $messages->error = false;
+         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 $messages;
+         return $this->messages;
      }
  
      /**
       * @return int|false H5P DB identifier.
       */
      private function get_h5p_id(string $url, \stdClass $config) {
-         global $DB;
+         global $DB, $USER;
  
          $fs = get_file_storage();
  
  
              // Check if the user uploading the H5P content is "trustable". If the file hasn't been uploaded by a user with this
              // capability, the content won't be deployed and an error message will be displayed.
-             if (!has_capability('moodle/h5p:deploy', $this->context, $file->get_userid())) {
+             if (!helper::can_deploy_package($file)) {
                  $this->core->h5pF->setErrorMessage(get_string('nopermissiontodeploy', 'core_h5p'));
                  return false;
              }
  
+             // The H5P content can be only deployed if the author of the .h5p file can update libraries or if all the
+             // content-type libraries exist, to avoid users without the h5p:updatelibraries capability upload malicious content.
+             $onlyupdatelibs = !helper::can_update_library($file);
+             // Set the .h5p file, in order to check later the permissions to update libraries.
+             $this->core->h5pF->set_file($file);
              // Validate and store the H5P content before displaying it.
-             return $this->save_h5p($file, $config);
+             $h5pid = helper::save_h5p($this->factory, $file, $config, $onlyupdatelibs, false);
+             if (!$h5pid && $file->get_userid() != $USER->id && has_capability('moodle/h5p:updatelibraries', $this->context)) {
+                 // The user has permission to update libraries but the package has been uploaded by a different
+                 // user without this permission. Check if there is some missing required library error.
+                 $missingliberror = false;
+                 $messages = $this->get_messages();
+                 if (!empty($messages->error)) {
+                     foreach ($messages->error as $error) {
+                         if ($error->code == 'missing-required-library') {
+                             $missingliberror = true;
+                             break;
+                         }
+                     }
+                 }
+                 if ($missingliberror) {
+                     // The message about the permissions to upload libraries should be removed.
+                     $infomsg = "Note that the libraries may exist in the file you uploaded, but you're not allowed to upload " .
+                         "new libraries. Contact the site administrator about this.";
+                     if (($key = array_search($infomsg, $messages->info)) !== false) {
+                         unset($messages->info[$key]);
+                     }
+                     // No library will be installed and an error will be displayed, because this content is not trustable.
+                     $this->core->h5pF->setInfoMessage(get_string('notrustablefile', 'core_h5p'));
+                 }
+                 return false;
+             }
+             return $h5pid;
          }
      }
  
       * @return string|false pathnamehash for the file in the internal URL.
       */
      private function get_pluginfile_hash(string $url) {
 -        global $USER;
 +        global $USER, $CFG;
  
          // Decode the URL before start processing it.
          $url = new \moodle_url(urldecode($url));
              throw new \moodle_exception('h5pprivatefile', 'core_h5p');
          }
  
 -        // For CONTEXT_MODULE, check if the user is enrolled in the course and has permissions view this .h5p file.
 -        if ($this->context->contextlevel == CONTEXT_MODULE) {
 +        // 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, false, true);
  
              // Now check if module is available OR it is restricted but the intro is shown on the course page.
 -            $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, false, true);
 +            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, false, true);
 +                    }
                  }
              }
          }
          // 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 = component_callback($component, 'get_path_from_pluginfile', [$filearea, $parts], null);
 +        $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 || ($component === 'mod' && $filearea === 'intro');
 +            $hasnullitemid = $hasnullitemid || (substr($component, 0, 4) === 'mod_' && $filearea === 'intro');
              $hasnullitemid = $hasnullitemid || ($component === 'course' &&
                      ($filearea === 'summary' || $filearea === 'overviewfiles'));
              $hasnullitemid = $hasnullitemid || ($component === 'coursecat' && $filearea === 'description');
          return $fs->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
      }
  
-     /**
-      * Store an H5P file
-      *
-      * @param stored_file $file Moodle file instance
-      * @param stdClass $config Button options config.
-      *
-      * @return int|false The H5P identifier or false if it's not a valid H5P package.
-      */
-     private function save_h5p($file, \stdClass $config) : int {
-         // This may take a long time.
-         \core_php_time_limit::raise();
-         $path = $this->core->fs->getTmpPath();
-         $this->core->h5pF->getUploadedH5pFolderPath($path);
-         // Add manually the extension to the file to avoid the validation fails.
-         $path .= '.h5p';
-         $this->core->h5pF->getUploadedH5pPath($path);
-         // Copy the .h5p file to the temporary folder.
-         $file->copy_content_to($path);
-         // Check if the h5p file is valid before saving it.
-         $h5pvalidator = $this->factory->get_validator();
-         if ($h5pvalidator->isValidPackage(false, false)) {
-             $h5pstorage = $this->factory->get_storage();
-             $options = ['disable' => $this->get_display_options($config)];
-             $content = [
-                 'pathnamehash' => $file->get_pathnamehash(),
-                 'contenthash' => $file->get_contenthash(),
-             ];
-             $h5pstorage->savePackage($content, null, false, $options);
-             return $h5pstorage->contentId;
-         }
-         return false;
-     }
      /**
       * Get the representation of display options as int.
       * @param stdClass $config Button options config.