Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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

   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|stdClass $users The user(s) 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          if (is_array($users)) {
 134              $user = $users[0];
 135          } else {
 136              $user = $users;
 137          }
 138  
 139          if ($plugin->get_subtype() == 'assignsubmission') {
 140              if ($assignment->get_instance()->teamsubmission) {
 141                  $sg = $assignment->get_group_submission($user->id, 0, false);
 142              } else {
 143                  $sg = $assignment->get_user_submission($user->id, false);
 144              }
 145          } else if ($plugin->get_subtype() == 'assignfeedback') {
 146              $sg = $assignment->get_user_grade($user->id, false);
 147          } else {
 148              return false;
 149          }
 150  
 151          if (!$sg) {
 152              return true;
 153          }
 154          foreach ($plugin->get_files($sg, $user) as $pluginfilename => $file) {
 155              if ($pluginfilename == $filename) {
 156                  // Extract the file and compare hashes.
 157                  $contenthash = '';
 158                  if (is_array($file)) {
 159                      $content = reset($file);
 160                      $contenthash = file_storage::hash_from_string($content);
 161                  } else {
 162                      $contenthash = $file->get_contenthash();
 163                  }
 164                  if ($contenthash != $fileinfo->get_contenthash()) {
 165                      return true;
 166                  } else {
 167                      return false;
 168                  }
 169              }
 170          }
 171          return true;
 172      }
 173  
 174      /**
 175       * Delete all temp files used when importing a zip
 176       *
 177       * @param int $contextid - The context id of this assignment instance
 178       * @return bool true if all files were deleted
 179       */
 180      public function delete_import_files($contextid) {
 181          global $USER;
 182  
 183          $fs = get_file_storage();
 184  
 185          return $fs->delete_area_files($contextid,
 186                                        'assignfeedback_file',
 187                                        ASSIGNFEEDBACK_FILE_IMPORT_FILEAREA,
 188                                        $USER->id);
 189      }
 190  
 191      /**
 192       * Extract the uploaded zip to a temporary import area for this user
 193       *
 194       * @param stored_file $zipfile The uploaded file
 195       * @param int $contextid The context for this assignment
 196       * @return bool - True if the files were unpacked
 197       */
 198      public function extract_files_from_zip($zipfile, $contextid) {
 199          global $USER;
 200  
 201          $feedbackfilesupdated = 0;
 202          $feedbackfilesadded = 0;
 203          $userswithnewfeedback = array();
 204  
 205          // Unzipping a large zip file is memory intensive.
 206          raise_memory_limit(MEMORY_EXTRA);
 207  
 208          $packer = get_file_packer('application/zip');
 209          core_php_time_limit::raise(ASSIGNFEEDBACK_FILE_MAXFILEUNZIPTIME);
 210  
 211          return $packer->extract_to_storage($zipfile,
 212                                      $contextid,
 213                                      'assignfeedback_file',
 214                                      ASSIGNFEEDBACK_FILE_IMPORT_FILEAREA,
 215                                      $USER->id,
 216                                      'import');
 217  
 218      }
 219  
 220      /**
 221       * Get the list of files extracted from the uploaded zip
 222       *
 223       * @param int $contextid
 224       * @return array of stored_files
 225       */
 226      public function get_import_files($contextid) {
 227          global $USER;
 228  
 229          $fs = get_file_storage();
 230          $files = $fs->get_directory_files($contextid,
 231                                            'assignfeedback_file',
 232                                            ASSIGNFEEDBACK_FILE_IMPORT_FILEAREA,
 233                                            $USER->id,
 234                                            '/import/', true); // Get files recursive (all levels).
 235  
 236          $keys = array_keys($files);
 237  
 238          return $files;
 239      }
 240  
 241      /**
 242       * Returns a mapping from unique user / group ids in folder names to array of moodle users.
 243       *
 244       * @param assign $assignment  - The assignment instance
 245       * @return array the mapping.
 246       */
 247      public function get_participant_mapping(assign $assignment): array {
 248          $currentgroup = groups_get_activity_group($assignment->get_course_module(), true);
 249          $allusers = $assignment->list_participants($currentgroup, false);
 250          $participants = array();
 251          foreach ($allusers as $user) {
 252              if ($assignment->get_instance()->teamsubmission) {
 253                  $group = $assignment->get_submission_group($user->id);
 254                  if (!$group) {
 255                      continue;
 256                  }
 257                  if (!isset($participants[$group->id])) {
 258                      $participants[$group->id] = [];
 259                  }
 260                  $participants[$group->id][] = $user;
 261              } else {
 262                  $participants[$assignment->get_uniqueid_for_user($user->id)] = [$user];
 263              }
 264          }
 265          return $participants;
 266      }
 267  
 268      /**
 269       * Process an uploaded zip file
 270       *
 271       * @param assign $assignment - The assignment instance
 272       * @param assign_feedback_file $fileplugin - The file feedback plugin
 273       * @return string - The html response
 274       */
 275      public function import_zip_files($assignment, $fileplugin) {
 276          global $CFG, $PAGE, $DB;
 277  
 278          core_php_time_limit::raise(ASSIGNFEEDBACK_FILE_MAXFILEUNZIPTIME);
 279          $packer = get_file_packer('application/zip');
 280  
 281          $feedbackfilesupdated = 0;
 282          $feedbackfilesadded = 0;
 283          $userswithnewfeedback = array();
 284          $contextid = $assignment->get_context()->id;
 285  
 286          $fs = get_file_storage();
 287          $files = $this->get_import_files($contextid);
 288  
 289          $participants = $this->get_participant_mapping($assignment);
 290  
 291          foreach ($files as $unzippedfile) {
 292              $users = null;
 293              $plugin = null;
 294              $filename = '';
 295  
 296              if ($this->is_valid_filename_for_import($assignment, $unzippedfile, $participants, $users, $plugin, $filename)) {
 297                  if ($this->is_file_modified($assignment, $users, $plugin, $filename, $unzippedfile)) {
 298                      foreach ($users as $user) {
 299                          $grade = $assignment->get_user_grade($user->id, true);
 300  
 301                          // In 3.1 the default download structure of the submission files changed so that each student had their own
 302                          // separate folder, the files were not renamed and the folder structure was kept. It is possible that
 303                          // a user downloaded the submission files in 3.0 (or earlier) and edited the zip to add feedback or
 304                          // changed the behavior back to the previous format, the following code means that we will still support the
 305                          // old file structure. For more information please see - MDL-52489 / MDL-56022.
 306                          $path = pathinfo($filename);
 307                          if ($path['dirname'] == '.') { // Student submissions are not in separate folders.
 308                              $basename = $filename;
 309                              $dirname = "/";
 310                              $dirnamewslash = "/";
 311                          } else {
 312                              $basename = $path['basename'];
 313                              $dirname = $path['dirname'];
 314                              $dirnamewslash = $dirname . "/";
 315                          }
 316  
 317                          if ($oldfile = $fs->get_file($contextid,
 318                                                       'assignfeedback_file',
 319                                                       ASSIGNFEEDBACK_FILE_FILEAREA,
 320                                                       $grade->id,
 321                                                       $dirname,
 322                                                       $basename)) {
 323                              // Update existing feedback file.
 324                              $oldfile->replace_file_with($unzippedfile);
 325                              $feedbackfilesupdated++;
 326                          } else {
 327                              // Create a new feedback file.
 328                              $newfilerecord = new stdClass();
 329                              $newfilerecord->contextid = $contextid;
 330                              $newfilerecord->component = 'assignfeedback_file';
 331                              $newfilerecord->filearea = ASSIGNFEEDBACK_FILE_FILEAREA;
 332                              $newfilerecord->filename = $basename;
 333                              $newfilerecord->filepath = $dirnamewslash;
 334                              $newfilerecord->itemid = $grade->id;
 335                              $fs->create_file_from_storedfile($newfilerecord, $unzippedfile);
 336                              $feedbackfilesadded++;
 337                          }
 338                          $userswithnewfeedback[$user->id] = 1;
 339  
 340                          // Update the number of feedback files for this user.
 341                          $fileplugin->update_file_count($grade);
 342  
 343                          // Update the last modified time on the grade which will trigger student notifications.
 344                          $assignment->notify_grade_modified($grade);
 345                      }
 346                  }
 347              }
 348          }
 349  
 350          require_once($CFG->dirroot . '/mod/assign/feedback/file/renderable.php');
 351          $importsummary = new assignfeedback_file_import_summary($assignment->get_course_module()->id,
 352                                                              count($userswithnewfeedback),
 353                                                              $feedbackfilesadded,
 354                                                              $feedbackfilesupdated);
 355  
 356          $assignrenderer = $assignment->get_renderer();
 357          $renderer = $PAGE->get_renderer('assignfeedback_file');
 358  
 359          $o = '';
 360  
 361          $o .= $assignrenderer->render(new assign_header($assignment->get_instance(),
 362                                                          $assignment->get_context(),
 363                                                          false,
 364                                                          $assignment->get_course_module()->id,
 365                                                          get_string('uploadzipsummary', 'assignfeedback_file')));
 366  
 367          $o .= $renderer->render($importsummary);
 368  
 369          $o .= $assignrenderer->render_footer();
 370          return $o;
 371      }
 372  
 373  }