Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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