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
26defined('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 */
35class 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}
332