Merge branch 'MDL-69112-master' of git://github.com/merrill-oakland/moodle
[moodle.git] / mod / assign / feedback / file / importziplib.php
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/>.
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  */
26 defined('MOODLE_INTERNAL') || die();
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 {
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         }
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         }
62         // Break the full path-name into path parts.
63         $pathparts = explode('/', $fileinfo->get_filepath() . $fileinfo->get_filename());
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);
70             if (count($info) < 5) {
71                 continue;
72             }
74             // Check the participant id.
75             $participantid = $info[1];
77             if (!is_numeric($participantid)) {
78                 continue;
79             }
81             // Convert to int.
82             $participantid += 0;
84             if (empty($participants[$participantid])) {
85                 continue;
86             }
88             // Set user, which is by reference, so is used by the calling script.
89             $user = $participants[$participantid];
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]);
94             if (!$plugin) {
95                 continue;
96             }
98             // Take any remaining text in this part and put it back in the path parts array.
99             array_unshift($pathparts, $info[4]);
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);
105             return true;
106         }
108         return false;
109     }
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;
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         }
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     }
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;
164         $fs = get_file_storage();
166         return $fs->delete_area_files($contextid,
167                                       'assignfeedback_file',
168                                       ASSIGNFEEDBACK_FILE_IMPORT_FILEAREA,
169                                       $USER->id);
170     }
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;
182         $feedbackfilesupdated = 0;
183         $feedbackfilesadded = 0;
184         $userswithnewfeedback = array();
186         // Unzipping a large zip file is memory intensive.
187         raise_memory_limit(MEMORY_EXTRA);
189         $packer = get_file_packer('application/zip');
190         core_php_time_limit::raise(ASSIGNFEEDBACK_FILE_MAXFILEUNZIPTIME);
192         return $packer->extract_to_storage($zipfile,
193                                     $contextid,
194                                     'assignfeedback_file',
195                                     ASSIGNFEEDBACK_FILE_IMPORT_FILEAREA,
196                                     $USER->id,
197                                     'import');
199     }
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;
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).
217         $keys = array_keys($files);
219         return $files;
220     }
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;
232         core_php_time_limit::raise(ASSIGNFEEDBACK_FILE_MAXFILEUNZIPTIME);
233         $packer = get_file_packer('application/zip');
235         $feedbackfilesupdated = 0;
236         $feedbackfilesadded = 0;
237         $userswithnewfeedback = array();
238         $contextid = $assignment->get_context()->id;
240         $fs = get_file_storage();
241         $files = $this->get_import_files($contextid);
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         }
250         foreach ($files as $unzippedfile) {
251             // Set the timeout for unzipping each file.
252             $user = null;
253             $plugin = null;
254             $filename = '';
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);
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                     }
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;
299                     // Update the number of feedback files for this user.
300                     $fileplugin->update_file_count($grade);
302                     // Update the last modified time on the grade which will trigger student notifications.
303                     $assignment->notify_grade_modified($grade);
304                 }
305             }
306         }
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);
314         $assignrenderer = $assignment->get_renderer();
315         $renderer = $PAGE->get_renderer('assignfeedback_file');
317         $o = '';
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')));
325         $o .= $renderer->render($importsummary);
327         $o .= $assignrenderer->render_footer();
328         return $o;
329     }