Search moodle.org's
Developer Documentation

  • 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 37 and 311]

       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  }