Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402] [Versions 402 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * This file contains the definition for the library class for file feedback plugin
  19   *
  20   *
  21   * @package   assignfeedback_file
  22   * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
  23   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  use mod_assign\output\assign_header;
  29  
  30  /**
  31   * library class for importing feedback files from a zip
  32   *
  33   * @package   assignfeedback_file
  34   * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
  35   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  class assignfeedback_file_zip_importer {
  38  
  39      /**
  40       * Is this filename valid (contains a unique participant ID) for import?
  41       *
  42       * @param assign $assignment - The assignment instance
  43       * @param stored_file $fileinfo - The fileinfo
  44       * @param array $participants - A list of valid participants for this module indexed by unique_id or group id.
  45       * @param array $users - Set to array with the user(s) that matches by participant id
  46       * @param assign_plugin $plugin - Set to the plugin that exported the file
  47       * @param string $filename - Set to truncated filename (prefix stripped)
  48       * @return bool If the participant Id can be extracted and this is a valid user
  49       */
  50      public function is_valid_filename_for_import($assignment, $fileinfo, $participants, & $users, & $plugin, & $filename) {
  51          if ($fileinfo->is_directory()) {
  52              return false;
  53          }
  54  
  55          // Ignore hidden files.
  56          if (strpos($fileinfo->get_filename(), '.') === 0) {
  57              return false;
  58          }
  59          // Ignore hidden files.
  60          if (strpos($fileinfo->get_filename(), '~') === 0) {
  61              return false;
  62          }
  63  
  64          // Break the full path-name into path parts.
  65          $pathparts = explode('/', $fileinfo->get_filepath() . $fileinfo->get_filename());
  66  
  67          while (!empty($pathparts)) {
  68              // Get the next path part and break it up by underscores.
  69              $pathpart = array_shift($pathparts);
  70              $info = explode('_', $pathpart, 5);
  71  
  72              // Expected format for the directory names in $pathpart is fullname_userid_plugintype_pluginname (as created by zip
  73              // export in Moodle >= 4.1) resp. fullname_userid_plugintype_pluginname_ (as created by earlier versions). We ensure
  74              // compatibility with both ways here.
  75              if (count($info) < 4) {
  76                  continue;
  77              }
  78  
  79              // Check the participant id.
  80              $participantid = $info[1];
  81  
  82              if (!is_numeric($participantid)) {
  83                  continue;
  84              }
  85  
  86              // Convert to int.
  87              $participantid += 0;
  88  
  89              if (empty($participants[$participantid])) {
  90                  continue;
  91              }
  92  
  93              // Set user, which is by reference, so is used by the calling script.
  94              $users = $participants[$participantid];
  95  
  96              // Set the plugin. This by reference, and is used by the calling script.
  97              $plugin = $assignment->get_plugin_by_type($info[2], $info[3]);
  98  
  99              if (!$plugin) {
 100                  continue;
 101              }
 102  
 103              // To get clean path names, we need to have at least an empty entry for $info[4].
 104              if (count($info) == 4) {
 105                  $info[4] = '';
 106              }
 107              // Take any remaining text in this part and put it back in the path parts array.
 108              array_unshift($pathparts, $info[4]);
 109  
 110              // Combine the remaining parts and set it as the filename.
 111              // Note that filename is a 'by reference' variable, so we need to set it before returning.
 112              $filename = implode('/', $pathparts);
 113  
 114              return true;
 115          }
 116  
 117          return false;
 118      }
 119  
 120      /**
 121       * Does this file exist in any of the current files supported by this plugin for this user?
 122       *
 123       * @param assign $assignment - The assignment instance
 124       * @param array $users The user matching this uploaded file
 125       * @param assign_plugin $plugin The matching plugin from the filename
 126       * @param string $filename The parsed filename from the zip
 127       * @param stored_file $fileinfo The info about the extracted file from the zip
 128       * @return bool - True if the file has been modified or is new
 129       */
 130      public function is_file_modified($assignment, $users, $plugin, $filename, $fileinfo) {
 131          $sg = null;
 132  
 133          $user = $users[0];
 134  
 135          if ($plugin->get_subtype() == 'assignsubmission') {
 136              if ($assignment->get_instance()->teamsubmission) {
 137                  $sg = $assignment->get_group_submission($user->id, 0, false);
 138              } else {
 139                  $sg = $assignment->get_user_submission($user->id, false);
 140              }
 141          } else if ($plugin->get_subtype() == 'assignfeedback') {
 142              $sg = $assignment->get_user_grade($user->id, false);
 143          } else {
 144              return false;
 145          }
 146  
 147          if (!$sg) {
 148              return true;
 149          }
 150          foreach ($plugin->get_files($sg, $user) as $pluginfilename => $file) {
 151              if ($pluginfilename == $filename) {
 152                  // Extract the file and compare hashes.
 153                  $contenthash = '';
 154                  if (is_array($file)) {
 155                      $content = reset($file);
 156                      $contenthash = file_storage::hash_from_string($content);
 157                  } else {
 158                      $contenthash = $file->get_contenthash();
 159                  }
 160                  if ($contenthash != $fileinfo->get_contenthash()) {
 161                      return true;
 162                  } else {
 163                      return false;
 164                  }
 165              }
 166          }
 167          return true;
 168      }
 169  
 170      /**
 171       * Delete all temp files used when importing a zip
 172       *
 173       * @param int $contextid - The context id of this assignment instance
 174       * @return bool true if all files were deleted
 175       */
 176      public function delete_import_files($contextid) {
 177          global $USER;
 178  
 179          $fs = get_file_storage();
 180  
 181          return $fs->delete_area_files($contextid,
 182                                        'assignfeedback_file',
 183                                        ASSIGNFEEDBACK_FILE_IMPORT_FILEAREA,
 184                                        $USER->id);
 185      }
 186  
 187      /**
 188       * Extract the uploaded zip to a temporary import area for this user
 189       *
 190       * @param stored_file $zipfile The uploaded file
 191       * @param int $contextid The context for this assignment
 192       * @return bool - True if the files were unpacked
 193       */
 194      public function extract_files_from_zip($zipfile, $contextid) {
 195          global $USER;
 196  
 197          $feedbackfilesupdated = 0;
 198          $feedbackfilesadded = 0;
 199          $userswithnewfeedback = array();
 200  
 201          // Unzipping a large zip file is memory intensive.
 202          raise_memory_limit(MEMORY_EXTRA);
 203  
 204          $packer = get_file_packer('application/zip');
 205          core_php_time_limit::raise(ASSIGNFEEDBACK_FILE_MAXFILEUNZIPTIME);
 206  
 207          return $packer->extract_to_storage($zipfile,
 208                                      $contextid,
 209                                      'assignfeedback_file',
 210                                      ASSIGNFEEDBACK_FILE_IMPORT_FILEAREA,
 211                                      $USER->id,
 212                                      'import');
 213  
 214      }
 215  
 216      /**
 217       * Get the list of files extracted from the uploaded zip
 218       *
 219       * @param int $contextid
 220       * @return array of stored_files
 221       */
 222      public function get_import_files($contextid) {
 223          global $USER;
 224  
 225          $fs = get_file_storage();
 226          $files = $fs->get_directory_files($contextid,
 227                                            'assignfeedback_file',
 228                                            ASSIGNFEEDBACK_FILE_IMPORT_FILEAREA,
 229                                            $USER->id,
 230                                            '/import/', true); // Get files recursive (all levels).
 231  
 232          $keys = array_keys($files);
 233  
 234          return $files;
 235      }
 236  
 237      /**
 238       * Returns a mapping from unique user / group ids in folder names to array of moodle users.
 239       *
 240       * @param assign $assignment  - The assignment instance
 241       * @return array the mapping.
 242       */
 243      public function get_participant_mapping(assign $assignment): array {
 244          $currentgroup = groups_get_activity_group($assignment->get_course_module(), true);
 245          $allusers = $assignment->list_participants($currentgroup, false);
 246          $participants = array();
 247          foreach ($allusers as $user) {
 248              if ($assignment->get_instance()->teamsubmission) {
 249                  $group = $assignment->get_submission_group($user->id);
 250                  if (!$group) {
 251                      continue;
 252                  }
 253                  if (!isset($participants[$group->id])) {
 254                      $participants[$group->id] = [];
 255                  }
 256                  $participants[$group->id][] = $user;
 257              } else {
 258                  $participants[$assignment->get_uniqueid_for_user($user->id)] = [$user];
 259              }
 260          }
 261          return $participants;
 262      }
 263  
 264      /**
 265       * Process an uploaded zip file
 266       *
 267       * @param assign $assignment - The assignment instance
 268       * @param assign_feedback_file $fileplugin - The file feedback plugin
 269       * @return string - The html response
 270       */
 271      public function import_zip_files($assignment, $fileplugin) {
 272          global $CFG, $PAGE, $DB;
 273  
 274          core_php_time_limit::raise(ASSIGNFEEDBACK_FILE_MAXFILEUNZIPTIME);
 275          $packer = get_file_packer('application/zip');
 276  
 277          $feedbackfilesupdated = 0;
 278          $feedbackfilesadded = 0;
 279          $userswithnewfeedback = array();
 280          $contextid = $assignment->get_context()->id;
 281  
 282          $fs = get_file_storage();
 283          $files = $this->get_import_files($contextid);
 284  
 285          $participants = $this->get_participant_mapping($assignment);
 286  
 287          foreach ($files as $unzippedfile) {
 288              $users = null;
 289              $plugin = null;
 290              $filename = '';
 291  
 292              if ($this->is_valid_filename_for_import($assignment, $unzippedfile, $participants, $users, $plugin, $filename)) {
 293                  if ($this->is_file_modified($assignment, $users, $plugin, $filename, $unzippedfile)) {
 294                      foreach ($users as $user) {
 295                          $grade = $assignment->get_user_grade($user->id, true);
 296  
 297                          // In 3.1 the default download structure of the submission files changed so that each student had their own
 298                          // separate folder, the files were not renamed and the folder structure was kept. It is possible that
 299                          // a user downloaded the submission files in 3.0 (or earlier) and edited the zip to add feedback or
 300                          // changed the behavior back to the previous format, the following code means that we will still support the
 301                          // old file structure. For more information please see - MDL-52489 / MDL-56022.
 302                          $path = pathinfo($filename);
 303                          if ($path['dirname'] == '.') { // Student submissions are not in separate folders.
 304                              $basename = $filename;
 305                              $dirname = "/";
 306                              $dirnamewslash = "/";
 307                          } else {
 308                              $basename = $path['basename'];
 309                              $dirname = $path['dirname'];
 310                              $dirnamewslash = $dirname . "/";
 311                          }
 312  
 313                          if ($oldfile = $fs->get_file($contextid,
 314                                                       'assignfeedback_file',
 315                                                       ASSIGNFEEDBACK_FILE_FILEAREA,
 316                                                       $grade->id,
 317                                                       $dirname,
 318                                                       $basename)) {
 319                              // Update existing feedback file.
 320                              $oldfile->replace_file_with($unzippedfile);
 321                              $feedbackfilesupdated++;
 322                          } else {
 323                              // Create a new feedback file.
 324                              $newfilerecord = new stdClass();
 325                              $newfilerecord->contextid = $contextid;
 326                              $newfilerecord->component = 'assignfeedback_file';
 327                              $newfilerecord->filearea = ASSIGNFEEDBACK_FILE_FILEAREA;
 328                              $newfilerecord->filename = $basename;
 329                              $newfilerecord->filepath = $dirnamewslash;
 330                              $newfilerecord->itemid = $grade->id;
 331                              $fs->create_file_from_storedfile($newfilerecord, $unzippedfile);
 332                              $feedbackfilesadded++;
 333                          }
 334                          $userswithnewfeedback[$user->id] = 1;
 335  
 336                          // Update the number of feedback files for this user.
 337                          $fileplugin->update_file_count($grade);
 338  
 339                          // Update the last modified time on the grade which will trigger student notifications.
 340                          $assignment->notify_grade_modified($grade);
 341                      }
 342                  }
 343              }
 344          }
 345  
 346          require_once($CFG->dirroot . '/mod/assign/feedback/file/renderable.php');
 347          $importsummary = new assignfeedback_file_import_summary($assignment->get_course_module()->id,
 348                                                              count($userswithnewfeedback),
 349                                                              $feedbackfilesadded,
 350                                                              $feedbackfilesupdated);
 351  
 352          $assignrenderer = $assignment->get_renderer();
 353          $renderer = $PAGE->get_renderer('assignfeedback_file');
 354  
 355          $o = '';
 356  
 357          $o .= $assignrenderer->render(new assign_header($assignment->get_instance(),
 358                                                          $assignment->get_context(),
 359                                                          false,
 360                                                          $assignment->get_course_module()->id,
 361                                                          get_string('uploadzipsummary', 'assignfeedback_file')));
 362  
 363          $o .= $renderer->render($importsummary);
 364  
 365          $o .= $assignrenderer->render_footer();
 366          return $o;
 367      }
 368  
 369  }