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