Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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