18b8fbfa |
1 | <?php |
c9b5ebf5 |
2 | |
72fb21b6 |
3 | // This file is part of Moodle - http://moodle.org/ |
4 | // |
5 | // Moodle is free software: you can redistribute it and/or modify |
6 | // it under the terms of the GNU General Public License as published by |
7 | // the Free Software Foundation, either version 3 of the License, or |
8 | // (at your option) any later version. |
9 | // |
10 | // Moodle is distributed in the hope that it will be useful, |
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | // GNU General Public License for more details. |
14 | // |
15 | // You should have received a copy of the GNU General Public License |
16 | // along with Moodle. If not, see <http://www.gnu.org/licenses/>. |
17 | |
c9b5ebf5 |
18 | /** |
19 | * uploadlib.php - This class handles all aspects of fileuploading |
20 | * |
72fb21b6 |
21 | * @package moodlecore |
22 | * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} |
23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later |
c9b5ebf5 |
24 | */ |
25 | |
72fb21b6 |
26 | /** {@see eventslib.php} */ |
3b120e46 |
27 | require_once($CFG->libdir.'/eventslib.php'); |
28 | |
4fb072d9 |
29 | //error_reporting(E_ALL ^ E_NOTICE); |
18b8fbfa |
30 | /** |
c9b5ebf5 |
31 | * This class handles all aspects of fileuploading |
72fb21b6 |
32 | * |
33 | * @package moodlecore |
34 | * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} |
35 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later |
18b8fbfa |
36 | */ |
37 | class upload_manager { |
38 | |
c9b5ebf5 |
39 | /** |
40 | * Array to hold local copies of stuff in $_FILES |
41 | * @var array $files |
42 | */ |
43 | var $files; |
44 | /** |
45 | * Holds all configuration stuff |
46 | * @var array $config |
47 | */ |
48 | var $config; |
49 | /** |
50 | * Keep track of if we're ok |
51 | * (errors for each file are kept in $files['whatever']['uploadlog'] |
52 | * @var boolean $status |
53 | */ |
54 | var $status; |
55 | /** |
56 | * The course this file has been uploaded for. {@link $COURSE} |
57 | * (for logging and virus notifications) |
58 | * @var course $course |
59 | */ |
60 | var $course; |
61 | /** |
62 | * If we're only getting one file. |
63 | * (for logging and virus notifications) |
64 | * @var string $inputname |
65 | */ |
66 | var $inputname; |
67 | /** |
68 | * If we're given silent=true in the constructor, this gets built |
69 | * up to hold info about the process. |
70 | * @var string $notify |
71 | */ |
72 | var $notify; |
18b8fbfa |
73 | |
74 | /** |
75 | * Constructor, sets up configuration stuff so we know how to act. |
c9b5ebf5 |
76 | * |
18b8fbfa |
77 | * Note: destination not taken as parameter as some modules want to use the insertid in the path and we need to check the other stuff first. |
c9b5ebf5 |
78 | * |
79 | * @uses $CFG |
80 | * @param string $inputname If this is given the upload manager will only process the file in $_FILES with this name. |
81 | * @param boolean $deleteothers Whether to delete other files in the destination directory (optional, defaults to false) |
82 | * @param boolean $handlecollisions Whether to use {@link handle_filename_collision()} or not. (optional, defaults to false) |
83 | * @param course $course The course the files are being uploaded for (for logging and virus notifications) {@link $COURSE} |
84 | * @param boolean $recoverifmultiple If we come across a virus, or if a file doesn't validate or whatever, do we continue? optional, defaults to true. |
85 | * @param int $modbytes Max bytes for this module - this and $course->maxbytes are used to get the maxbytes from {@link get_max_upload_file_size()}. |
86 | * @param boolean $silent Whether to notify errors or not. |
87 | * @param boolean $allownull Whether we care if there's no file when we've set the input name. |
aff2944e |
88 | * @param boolean $allownullmultiple Whether we care if there's no files AT ALL when we've got multiples. This won't complain if we have file 1 and file 3 but not file 2, only for NO FILES AT ALL. |
18b8fbfa |
89 | */ |
aff2944e |
90 | function upload_manager($inputname='', $deleteothers=false, $handlecollisions=false, $course=null, $recoverifmultiple=false, $modbytes=0, $silent=false, $allownull=false, $allownullmultiple=true) { |
18b8fbfa |
91 | |
4fb072d9 |
92 | global $CFG, $SITE; |
93 | |
94 | if (empty($course->id)) { |
95 | $course = $SITE; |
96 | } |
18b8fbfa |
97 | |
98 | $this->config->deleteothers = $deleteothers; |
99 | $this->config->handlecollisions = $handlecollisions; |
100 | $this->config->recoverifmultiple = $recoverifmultiple; |
c9b5ebf5 |
101 | $this->config->maxbytes = get_max_upload_file_size($CFG->maxbytes, $course->maxbytes, $modbytes); |
81d425b4 |
102 | $this->config->silent = $silent; |
96038147 |
103 | $this->config->allownull = $allownull; |
18b8fbfa |
104 | $this->files = array(); |
105 | $this->status = false; |
106 | $this->course = $course; |
107 | $this->inputname = $inputname; |
96038147 |
108 | if (empty($this->inputname)) { |
aff2944e |
109 | $this->config->allownull = $allownullmultiple; |
96038147 |
110 | } |
18b8fbfa |
111 | } |
112 | |
113 | /** |
c9b5ebf5 |
114 | * Gets all entries out of $_FILES and stores them locally in $files and then |
115 | * checks each one against {@link get_max_upload_file_size()} and calls {@link cleanfilename()} |
116 | * and scans them for viruses etc. |
117 | * @uses $CFG |
118 | * @uses $_FILES |
119 | * @return boolean |
18b8fbfa |
120 | */ |
121 | function preprocess_files() { |
aa9a6867 |
122 | global $CFG, $OUTPUT; |
9056cec0 |
123 | |
18b8fbfa |
124 | foreach ($_FILES as $name => $file) { |
125 | $this->status = true; // only set it to true here so that we can check if this function has been called. |
126 | if (empty($this->inputname) || $name == $this->inputname) { // if we have input name, only process if it matches. |
127 | $file['originalname'] = $file['name']; // do this first for the log. |
128 | $this->files[$name] = $file; // put it in first so we can get uploadlog out in print_upload_log. |
4fb072d9 |
129 | $this->files[$name]['uploadlog'] = ''; // initialize error log |
96038147 |
130 | $this->status = $this->validate_file($this->files[$name]); // default to only allowing empty on multiple uploads. |
aff2944e |
131 | if (!$this->status && ($this->files[$name]['error'] == 0 || $this->files[$name]['error'] == 4) && ($this->config->allownull || empty($this->inputname))) { |
18b8fbfa |
132 | // this shouldn't cause everything to stop.. modules should be responsible for knowing which if any are compulsory. |
133 | continue; |
134 | } |
a2520070 |
135 | if ($this->status && !empty($CFG->runclamonupload)) { |
34941e07 |
136 | $this->status = clam_scan_moodle_file($this->files[$name],$this->course); |
18b8fbfa |
137 | } |
138 | if (!$this->status) { |
139 | if (!$this->config->recoverifmultiple && count($this->files) > 1) { |
bdd3e83a |
140 | $a = new object(); |
141 | $a->name = $this->files[$name]['originalname']; |
18b8fbfa |
142 | $a->problem = $this->files[$name]['uploadlog']; |
81d425b4 |
143 | if (!$this->config->silent) { |
aa9a6867 |
144 | echo $OUTPUT->notification(get_string('uploadfailednotrecovering','moodle',$a)); |
81d425b4 |
145 | } |
146 | else { |
c9b5ebf5 |
147 | $this->notify .= '<br />'. get_string('uploadfailednotrecovering','moodle',$a); |
81d425b4 |
148 | } |
18b8fbfa |
149 | $this->status = false; |
150 | return false; |
9056cec0 |
151 | |
152 | } else if (count($this->files) == 1) { |
153 | |
154 | if (!$this->config->silent and !$this->config->allownull) { |
aa9a6867 |
155 | echo $OUTPUT->notification($this->files[$name]['uploadlog']); |
9056cec0 |
156 | } else { |
c9b5ebf5 |
157 | $this->notify .= '<br />'. $this->files[$name]['uploadlog']; |
81d425b4 |
158 | } |
18b8fbfa |
159 | $this->status = false; |
160 | return false; |
161 | } |
162 | } |
163 | else { |
164 | $newname = clean_filename($this->files[$name]['name']); |
165 | if ($newname != $this->files[$name]['name']) { |
bdd3e83a |
166 | $a = new object(); |
18b8fbfa |
167 | $a->oldname = $this->files[$name]['name']; |
168 | $a->newname = $newname; |
c9b5ebf5 |
169 | $this->files[$name]['uploadlog'] .= get_string('uploadrenamedchars','moodle', $a); |
18b8fbfa |
170 | } |
171 | $this->files[$name]['name'] = $newname; |
172 | $this->files[$name]['clear'] = true; // ok to save. |
3abd1a55 |
173 | $this->config->somethingtosave = true; |
18b8fbfa |
174 | } |
175 | } |
176 | } |
46c5446e |
177 | if (!is_array($_FILES) || count($_FILES) == 0) { |
96038147 |
178 | return $this->config->allownull; |
46c5446e |
179 | } |
18b8fbfa |
180 | $this->status = true; |
181 | return true; // if we've got this far it means that we're recovering so we want status to be ok. |
182 | } |
183 | |
184 | /** |
185 | * Validates a single file entry from _FILES |
c9b5ebf5 |
186 | * |
187 | * @param object $file The entry from _FILES to validate |
188 | * @return boolean True if ok. |
18b8fbfa |
189 | */ |
96038147 |
190 | function validate_file(&$file) { |
18b8fbfa |
191 | if (empty($file)) { |
96038147 |
192 | return false; |
18b8fbfa |
193 | } |
194 | if (!is_uploaded_file($file['tmp_name']) || $file['size'] == 0) { |
195 | $file['uploadlog'] .= "\n".$this->get_file_upload_error($file); |
18b8fbfa |
196 | return false; |
197 | } |
198 | if ($file['size'] > $this->config->maxbytes) { |
c9b5ebf5 |
199 | $file['uploadlog'] .= "\n". get_string('uploadedfiletoobig', 'moodle', $this->config->maxbytes); |
18b8fbfa |
200 | return false; |
201 | } |
202 | return true; |
203 | } |
204 | |
205 | /** |
206 | * Moves all the files to the destination directory. |
c9b5ebf5 |
207 | * |
208 | * @uses $CFG |
209 | * @uses $USER |
210 | * @param string $destination The destination directory. |
211 | * @return boolean status; |
18b8fbfa |
212 | */ |
213 | function save_files($destination) { |
aa9a6867 |
214 | global $CFG, $USER, $OUTPUT; |
18b8fbfa |
215 | |
216 | if (!$this->status) { // preprocess_files hasn't been run |
217 | $this->preprocess_files(); |
218 | } |
3abd1a55 |
219 | |
220 | // if there are no files, bail before we create an empty directory. |
221 | if (empty($this->config->somethingtosave)) { |
222 | return true; |
223 | } |
224 | |
e78d624f |
225 | $savedsomething = false; |
226 | |
18b8fbfa |
227 | if ($this->status) { |
c9b5ebf5 |
228 | if (!(strpos($destination, $CFG->dataroot) === false)) { |
18b8fbfa |
229 | // take it out for giving to make_upload_directory |
c9b5ebf5 |
230 | $destination = substr($destination, strlen($CFG->dataroot)+1); |
18b8fbfa |
231 | } |
232 | |
c9b5ebf5 |
233 | if ($destination{strlen($destination)-1} == '/') { // strip off a trailing / if we have one |
234 | $destination = substr($destination, 0, -1); |
18b8fbfa |
235 | } |
236 | |
c9b5ebf5 |
237 | if (!make_upload_directory($destination, true)) { //TODO maybe put this function here instead of moodlelib.php now. |
18b8fbfa |
238 | $this->status = false; |
239 | return false; |
240 | } |
241 | |
c9b5ebf5 |
242 | $destination = $CFG->dataroot .'/'. $destination; // now add it back in so we have a full path |
18b8fbfa |
243 | |
244 | $exceptions = array(); //need this later if we're deleting other files. |
245 | |
246 | foreach (array_keys($this->files) as $i) { |
247 | |
248 | if (!$this->files[$i]['clear']) { |
249 | // not ok to save |
250 | continue; |
251 | } |
252 | |
253 | if ($this->config->handlecollisions) { |
c9b5ebf5 |
254 | $this->handle_filename_collision($destination, $this->files[$i]); |
18b8fbfa |
255 | } |
256 | if (move_uploaded_file($this->files[$i]['tmp_name'], $destination.'/'.$this->files[$i]['name'])) { |
c9b5ebf5 |
257 | chmod($destination .'/'. $this->files[$i]['name'], $CFG->directorypermissions); |
18b8fbfa |
258 | $this->files[$i]['fullpath'] = $destination.'/'.$this->files[$i]['name']; |
259 | $this->files[$i]['uploadlog'] .= "\n".get_string('uploadedfile'); |
260 | $this->files[$i]['saved'] = true; |
261 | $exceptions[] = $this->files[$i]['name']; |
262 | // now add it to the log (this is important so we know who to notify if a virus is found later on) |
c9b5ebf5 |
263 | clam_log_upload($this->files[$i]['fullpath'], $this->course); |
18b8fbfa |
264 | $savedsomething=true; |
265 | } |
266 | } |
267 | if ($savedsomething && $this->config->deleteothers) { |
c9b5ebf5 |
268 | $this->delete_other_files($destination, $exceptions); |
18b8fbfa |
269 | } |
270 | } |
aff2944e |
271 | if (empty($savedsomething)) { |
18b8fbfa |
272 | $this->status = false; |
aba94550 |
273 | if ((empty($this->config->allownull) && !empty($this->inputname)) || (empty($this->inputname) && empty($this->config->allownullmultiple))) { |
aa9a6867 |
274 | echo $OUTPUT->notification(get_string('uploadnofilefound')); |
aff2944e |
275 | } |
18b8fbfa |
276 | return false; |
277 | } |
278 | return $this->status; |
279 | } |
280 | |
281 | /** |
c9b5ebf5 |
282 | * Wrapper function that calls {@link preprocess_files()} and {@link viruscheck_files()} and then {@link save_files()} |
18b8fbfa |
283 | * Modules that require the insert id in the filepath should not use this and call these functions seperately in the required order. |
c9b5ebf5 |
284 | * @parameter string $destination Where to save the uploaded files to. |
285 | * @return boolean |
18b8fbfa |
286 | */ |
287 | function process_file_uploads($destination) { |
288 | if ($this->preprocess_files()) { |
289 | return $this->save_files($destination); |
290 | } |
291 | return false; |
292 | } |
293 | |
294 | /** |
295 | * Deletes all the files in a given directory except for the files in $exceptions (full paths) |
c9b5ebf5 |
296 | * |
297 | * @param string $destination The directory to clean up. |
298 | * @param array $exceptions Full paths of files to KEEP. |
18b8fbfa |
299 | */ |
c9b5ebf5 |
300 | function delete_other_files($destination, $exceptions=null) { |
aa9a6867 |
301 | global $OUTPUT; |
4fb072d9 |
302 | $deletedsomething = false; |
18b8fbfa |
303 | if ($filestodel = get_directory_list($destination)) { |
304 | foreach ($filestodel as $file) { |
c9b5ebf5 |
305 | if (!is_array($exceptions) || !in_array($file, $exceptions)) { |
306 | unlink($destination .'/'. $file); |
18b8fbfa |
307 | $deletedsomething = true; |
308 | } |
309 | } |
310 | } |
311 | if ($deletedsomething) { |
81d425b4 |
312 | if (!$this->config->silent) { |
aa9a6867 |
313 | echo $OUTPUT->notification(get_string('uploadoldfilesdeleted')); |
81d425b4 |
314 | } |
315 | else { |
c9b5ebf5 |
316 | $this->notify .= '<br />'. get_string('uploadoldfilesdeleted'); |
81d425b4 |
317 | } |
18b8fbfa |
318 | } |
319 | } |
320 | |
321 | /** |
322 | * Handles filename collisions - if the desired filename exists it will rename it according to the pattern in $format |
c9b5ebf5 |
323 | * @param string $destination Destination directory (to check existing files against) |
324 | * @param object $file Passed in by reference. The current file from $files we're processing. |
94ba6c90 |
325 | * @return void - modifies &$file parameter. |
18b8fbfa |
326 | */ |
94ba6c90 |
327 | function handle_filename_collision($destination, &$file) { |
328 | if (!file_exists($destination .'/'. $file['name'])) { |
329 | return; |
330 | } |
331 | |
332 | $parts = explode('.', $file['name']); |
333 | if (count($parts) > 1) { |
334 | $extension = '.'.array_pop($parts); |
335 | $name = implode('.', $parts); |
336 | } else { |
337 | $extension = ''; |
338 | $name = $file['name']; |
339 | } |
340 | |
341 | $current = 0; |
342 | if (preg_match('/^(.*)_(\d*)$/s', $name, $matches)) { |
343 | $name = $matches[1]; |
344 | $current = (int)$matches[2]; |
345 | } |
346 | $i = $current + 1; |
347 | |
348 | while (!$this->check_before_renaming($destination, $name.'_'.$i.$extension, $file)) { |
349 | $i++; |
18b8fbfa |
350 | } |
94ba6c90 |
351 | $a = new object(); |
352 | $a->oldname = $file['name']; |
353 | $file['name'] = $name.'_'.$i.$extension; |
354 | $a->newname = $file['name']; |
355 | $file['uploadlog'] .= "\n". get_string('uploadrenamedcollision','moodle', $a); |
18b8fbfa |
356 | } |
357 | |
358 | /** |
359 | * This function checks a potential filename against what's on the filesystem already and what's been saved already. |
c9b5ebf5 |
360 | * @param string $destination Destination directory (to check existing files against) |
361 | * @param string $nametocheck The filename to be compared. |
362 | * @param object $file The current file from $files we're processing. |
363 | * return boolean |
18b8fbfa |
364 | */ |
c9b5ebf5 |
365 | function check_before_renaming($destination, $nametocheck, $file) { |
366 | if (!file_exists($destination .'/'. $nametocheck)) { |
18b8fbfa |
367 | return true; |
368 | } |
369 | if ($this->config->deleteothers) { |
370 | foreach ($this->files as $tocheck) { |
371 | // if we're deleting files anyway, it's not THIS file and we care about it and it has the same name and has already been saved.. |
372 | if ($file['tmp_name'] != $tocheck['tmp_name'] && $tocheck['clear'] && $nametocheck == $tocheck['name'] && $tocheck['saved']) { |
373 | $collision = true; |
374 | } |
375 | } |
376 | if (!$collision) { |
377 | return true; |
378 | } |
379 | } |
380 | return false; |
381 | } |
382 | |
c9b5ebf5 |
383 | /** |
384 | * ? |
385 | * |
386 | * @param object $file Passed in by reference. The current file from $files we're processing. |
387 | * @return string |
388 | * @todo Finish documenting this function |
389 | */ |
18b8fbfa |
390 | function get_file_upload_error(&$file) { |
391 | |
392 | switch ($file['error']) { |
393 | case 0: // UPLOAD_ERR_OK |
394 | if ($file['size'] > 0) { |
395 | $errmessage = get_string('uploadproblem', $file['name']); |
396 | } else { |
397 | $errmessage = get_string('uploadnofilefound'); /// probably a dud file name |
398 | } |
399 | break; |
400 | |
401 | case 1: // UPLOAD_ERR_INI_SIZE |
402 | $errmessage = get_string('uploadserverlimit'); |
403 | break; |
404 | |
405 | case 2: // UPLOAD_ERR_FORM_SIZE |
406 | $errmessage = get_string('uploadformlimit'); |
407 | break; |
408 | |
409 | case 3: // UPLOAD_ERR_PARTIAL |
410 | $errmessage = get_string('uploadpartialfile'); |
411 | break; |
412 | |
413 | case 4: // UPLOAD_ERR_NO_FILE |
414 | $errmessage = get_string('uploadnofilefound'); |
415 | break; |
416 | |
a8e352c5 |
417 | // Note: there is no error with a value of 5 |
418 | |
419 | case 6: // UPLOAD_ERR_NO_TMP_DIR |
420 | $errmessage = get_string('uploadnotempdir'); |
421 | break; |
422 | |
423 | case 7: // UPLOAD_ERR_CANT_WRITE |
424 | $errmessage = get_string('uploadcantwrite'); |
425 | break; |
426 | |
427 | case 8: // UPLOAD_ERR_EXTENSION |
428 | $errmessage = get_string('uploadextension'); |
429 | break; |
430 | |
18b8fbfa |
431 | default: |
432 | $errmessage = get_string('uploadproblem', $file['name']); |
433 | } |
434 | return $errmessage; |
435 | } |
436 | |
437 | /** |
438 | * prints a log of everything that happened (of interest) to each file in _FILES |
439 | * @param $return - optional, defaults to false (log is echoed) |
440 | */ |
aff2944e |
441 | function print_upload_log($return=false,$skipemptyifmultiple=false) { |
7bd43426 |
442 | $str = ''; |
18b8fbfa |
443 | foreach (array_keys($this->files) as $i => $key) { |
aff2944e |
444 | if (count($this->files) > 1 && !empty($skipemptyifmultiple) && $this->files[$key]['error'] == 4) { |
445 | continue; |
446 | } |
c9b5ebf5 |
447 | $str .= '<strong>'. get_string('uploadfilelog', 'moodle', $i+1) .' ' |
18b8fbfa |
448 | .((!empty($this->files[$key]['originalname'])) ? '('.$this->files[$key]['originalname'].')' : '') |
c9b5ebf5 |
449 | .'</strong> :'. nl2br($this->files[$key]['uploadlog']) .'<br />'; |
18b8fbfa |
450 | } |
451 | if ($return) { |
452 | return $str; |
453 | } |
454 | echo $str; |
455 | } |
456 | |
457 | /** |
458 | * If we're only handling one file (if inputname was given in the constructor) this will return the (possibly changed) filename of the file. |
c9b5ebf5 |
459 | @return boolean |
18b8fbfa |
460 | */ |
461 | function get_new_filename() { |
4fb072d9 |
462 | if (!empty($this->inputname) and count($this->files) == 1 and $this->files[$this->inputname]['error'] != 4) { |
18b8fbfa |
463 | return $this->files[$this->inputname]['name']; |
464 | } |
465 | return false; |
466 | } |
467 | |
81d425b4 |
468 | /** |
469 | * If we're only handling one file (if input name was given in the constructor) this will return the full path to the saved file. |
c9b5ebf5 |
470 | * @return boolean |
81d425b4 |
471 | */ |
472 | function get_new_filepath() { |
4fb072d9 |
473 | if (!empty($this->inputname) and count($this->files) == 1 and $this->files[$this->inputname]['error'] != 4) { |
81d425b4 |
474 | return $this->files[$this->inputname]['fullpath']; |
475 | } |
476 | return false; |
477 | } |
478 | |
18b8fbfa |
479 | /** |
480 | * If we're only handling one file (if inputname was given in the constructor) this will return the ORIGINAL filename of the file. |
c9b5ebf5 |
481 | * @return boolean |
18b8fbfa |
482 | */ |
483 | function get_original_filename() { |
4fb072d9 |
484 | if (!empty($this->inputname) and count($this->files) == 1 and $this->files[$this->inputname]['error'] != 4) { |
18b8fbfa |
485 | return $this->files[$this->inputname]['originalname']; |
486 | } |
487 | return false; |
488 | } |
96038147 |
489 | |
490 | /** |
c9b5ebf5 |
491 | * This function returns any errors wrapped up in red. |
492 | * @return string |
96038147 |
493 | */ |
494 | function get_errors() { |
902d5cc0 |
495 | if (!empty($this->notify)) { |
496 | return '<p class="notifyproblem">'. $this->notify .'</p>'; |
497 | } else { |
498 | return null; |
499 | } |
96038147 |
500 | } |
18b8fbfa |
501 | } |
502 | |
503 | /************************************************************************************** |
504 | THESE FUNCTIONS ARE OUTSIDE THE CLASS BECAUSE THEY NEED TO BE CALLED FROM OTHER PLACES. |
505 | FOR EXAMPLE CLAM_HANDLE_INFECTED_FILE AND CLAM_REPLACE_INFECTED_FILE USED FROM CRON |
506 | UPLOAD_PRINT_FORM_FRAGMENT DOESN'T REALLY BELONG IN THE CLASS BUT CERTAINLY IN THIS FILE |
507 | ***************************************************************************************/ |
508 | |
509 | |
510 | /** |
c9b5ebf5 |
511 | * This function prints out a number of upload form elements. |
512 | * |
72fb21b6 |
513 | * @global object |
c9b5ebf5 |
514 | * @param int $numfiles The number of elements required (optional, defaults to 1) |
515 | * @param array $names Array of element names to use (optional, defaults to FILE_n) |
516 | * @param array $descriptions Array of strings to be printed out before each file bit. |
517 | * @param boolean $uselabels -Whether to output text fields for file descriptions or not (optional, defaults to false) |
518 | * @param array $labelnames Array of element names to use for labels (optional, defaults to LABEL_n) |
519 | * @param int $coursebytes $coursebytes and $maxbytes are used to calculate upload max size ( using {@link get_max_upload_file_size}) |
520 | * @param int $modbytes $coursebytes and $maxbytes are used to calculate upload max size ( using {@link get_max_upload_file_size}) |
521 | * @param boolean $return -Whether to return the string (defaults to false - string is echoed) |
522 | * @return string Form returned as string if $return is true |
18b8fbfa |
523 | */ |
c9b5ebf5 |
524 | function upload_print_form_fragment($numfiles=1, $names=null, $descriptions=null, $uselabels=false, $labelnames=null, $coursebytes=0, $modbytes=0, $return=false) { |
18b8fbfa |
525 | global $CFG; |
c9b5ebf5 |
526 | $maxbytes = get_max_upload_file_size($CFG->maxbytes, $coursebytes, $modbytes); |
527 | $str = '<input type="hidden" name="MAX_FILE_SIZE" value="'. $maxbytes .'" />'."\n"; |
18b8fbfa |
528 | for ($i = 0; $i < $numfiles; $i++) { |
529 | if (is_array($descriptions) && !empty($descriptions[$i])) { |
c9b5ebf5 |
530 | $str .= '<strong>'. $descriptions[$i] .'</strong><br />'; |
18b8fbfa |
531 | } |
09e8a2f8 |
532 | $name = ((is_array($names) && !empty($names[$i])) ? $names[$i] : 'FILE_'.$i); |
c9b5ebf5 |
533 | $str .= '<input type="file" size="50" name="'. $name .'" alt="'. $name .'" /><br />'."\n"; |
18b8fbfa |
534 | if ($uselabels) { |
09e8a2f8 |
535 | $lname = ((is_array($labelnames) && !empty($labelnames[$i])) ? $labelnames[$i] : 'LABEL_'.$i); |
c9b5ebf5 |
536 | $str .= get_string('uploadlabel').' <input type="text" size="50" name="'. $lname .'" alt="'. $lname |
18b8fbfa |
537 | .'" /><br /><br />'."\n"; |
538 | } |
539 | } |
540 | if ($return) { |
541 | return $str; |
542 | } |
543 | else { |
544 | echo $str; |
545 | } |
546 | } |
547 | |
548 | |
549 | /** |
550 | * Deals with an infected file - either moves it to a quarantinedir |
551 | * (specified in CFG->quarantinedir) or deletes it. |
c9b5ebf5 |
552 | * |
18b8fbfa |
553 | * If moving it fails, it deletes it. |
c9b5ebf5 |
554 | * |
72fb21b6 |
555 | * @global object |
556 | * @global object |
c9b5ebf5 |
557 | * @param string $file Full path to the file |
558 | * @param int $userid If not used, defaults to $USER->id (there in case called from cron) |
559 | * @param boolean $basiconly Admin level reporting or user level reporting. |
560 | * @return string Details of what the function did. |
18b8fbfa |
561 | */ |
c9b5ebf5 |
562 | function clam_handle_infected_file($file, $userid=0, $basiconly=false) { |
18b8fbfa |
563 | |
c9b5ebf5 |
564 | global $CFG, $USER; |
18b8fbfa |
565 | if ($USER && !$userid) { |
566 | $userid = $USER->id; |
567 | } |
568 | $delete = true; |
569 | if (file_exists($CFG->quarantinedir) && is_dir($CFG->quarantinedir) && is_writable($CFG->quarantinedir)) { |
570 | $now = date('YmdHis'); |
c9b5ebf5 |
571 | if (rename($file, $CFG->quarantinedir .'/'. $now .'-user-'. $userid .'-infected')) { |
18b8fbfa |
572 | $delete = false; |
c9b5ebf5 |
573 | clam_log_infected($file, $CFG->quarantinedir.'/'. $now .'-user-'. $userid .'-infected', $userid); |
18b8fbfa |
574 | if ($basiconly) { |
c9b5ebf5 |
575 | $notice .= "\n". get_string('clammovedfilebasic'); |
18b8fbfa |
576 | } |
577 | else { |
c9b5ebf5 |
578 | $notice .= "\n". get_string('clammovedfile', 'moodle', $CFG->quarantinedir.'/'. $now .'-user-'. $userid .'-infected'); |
18b8fbfa |
579 | } |
580 | } |
581 | else { |
582 | if ($basiconly) { |
c9b5ebf5 |
583 | $notice .= "\n". get_string('clamdeletedfile'); |
18b8fbfa |
584 | } |
585 | else { |
c9b5ebf5 |
586 | $notice .= "\n". get_string('clamquarantinedirfailed', 'moodle', $CFG->quarantinedir); |
18b8fbfa |
587 | } |
588 | } |
589 | } |
590 | else { |
591 | if ($basiconly) { |
c9b5ebf5 |
592 | $notice .= "\n". get_string('clamdeletedfile'); |
18b8fbfa |
593 | } |
594 | else { |
c9b5ebf5 |
595 | $notice .= "\n". get_string('clamquarantinedirfailed', 'moodle', $CFG->quarantinedir); |
18b8fbfa |
596 | } |
597 | } |
598 | if ($delete) { |
599 | if (unlink($file)) { |
c9b5ebf5 |
600 | clam_log_infected($file, '', $userid); |
601 | $notice .= "\n". get_string('clamdeletedfile'); |
18b8fbfa |
602 | } |
603 | else { |
604 | if ($basiconly) { |
605 | // still tell the user the file has been deleted. this is only for admins. |
c9b5ebf5 |
606 | $notice .= "\n". get_string('clamdeletedfile'); |
18b8fbfa |
607 | } |
608 | else { |
c9b5ebf5 |
609 | $notice .= "\n". get_string('clamdeletedfilefailed'); |
18b8fbfa |
610 | } |
611 | } |
612 | } |
613 | return $notice; |
614 | } |
615 | |
616 | /** |
c9b5ebf5 |
617 | * Replaces the given file with a string. |
618 | * |
619 | * The replacement string is used to notify that the original file had a virus |
18b8fbfa |
620 | * This is to avoid missing files but could result in the wrong content-type. |
72fb21b6 |
621 | * |
c9b5ebf5 |
622 | * @param string $file Full path to the file. |
623 | * @return boolean |
18b8fbfa |
624 | */ |
625 | function clam_replace_infected_file($file) { |
626 | $newcontents = get_string('virusplaceholder'); |
c9b5ebf5 |
627 | if (!$f = fopen($file, 'w')) { |
18b8fbfa |
628 | return false; |
629 | } |
c9b5ebf5 |
630 | if (!fwrite($f, $newcontents)) { |
18b8fbfa |
631 | return false; |
632 | } |
633 | return true; |
634 | } |
635 | |
636 | |
637 | /** |
c9b5ebf5 |
638 | * If $CFG->runclamonupload is set, we scan a given file. (called from {@link preprocess_files()}) |
639 | * |
18b8fbfa |
640 | * This function will add on a uploadlog index in $file. |
72fb21b6 |
641 | * |
642 | * @global object |
643 | * @global object |
c9b5ebf5 |
644 | * @param mixed $file The file to scan from $files. or an absolute path to a file. |
645 | * @param course $course {@link $COURSE} |
646 | * @return int 1 if good, 0 if something goes wrong (opposite from actual error code from clam) |
18b8fbfa |
647 | */ |
34941e07 |
648 | function clam_scan_moodle_file(&$file, $course) { |
c9b5ebf5 |
649 | global $CFG, $USER; |
18b8fbfa |
650 | |
651 | if (is_array($file) && is_uploaded_file($file['tmp_name'])) { // it's from $_FILES |
652 | $appendlog = true; |
653 | $fullpath = $file['tmp_name']; |
654 | } |
655 | else if (file_exists($file)) { // it's a path to somewhere on the filesystem! |
656 | $fullpath = $file; |
657 | } |
658 | else { |
659 | return false; // erm, what is this supposed to be then, huh? |
660 | } |
661 | |
96038147 |
662 | $CFG->pathtoclam = trim($CFG->pathtoclam); |
663 | |
18b8fbfa |
664 | if (!$CFG->pathtoclam || !file_exists($CFG->pathtoclam) || !is_executable($CFG->pathtoclam)) { |
665 | $newreturn = 1; |
c9b5ebf5 |
666 | $notice = get_string('clamlost', 'moodle', $CFG->pathtoclam); |
18b8fbfa |
667 | if ($CFG->clamfailureonupload == 'actlikevirus') { |
c9b5ebf5 |
668 | $notice .= "\n". get_string('clamlostandactinglikevirus'); |
669 | $notice .= "\n". clam_handle_infected_file($fullpath); |
18b8fbfa |
670 | $newreturn = false; |
671 | } |
3b120e46 |
672 | clam_message_admins($notice); |
96038147 |
673 | if ($appendlog) { |
c9b5ebf5 |
674 | $file['uploadlog'] .= "\n". get_string('clambroken'); |
96038147 |
675 | $file['clam'] = 1; |
676 | } |
18b8fbfa |
677 | return $newreturn; // return 1 if we're allowing clam failures |
678 | } |
679 | |
c9b5ebf5 |
680 | $cmd = $CFG->pathtoclam .' '. $fullpath ." 2>&1"; |
18b8fbfa |
681 | |
682 | // before we do anything we need to change perms so that clamscan can read the file (clamdscan won't work otherwise) |
683 | chmod($fullpath,0644); |
684 | |
c9b5ebf5 |
685 | exec($cmd, $output, $return); |
18b8fbfa |
686 | |
687 | |
688 | switch ($return) { |
689 | case 0: // glee! we're ok. |
690 | return 1; // translate clam return code into reasonable return code consistent with everything else. |
691 | case 1: // bad wicked evil, we have a virus. |
bdd3e83a |
692 | $info = new object(); |
18b8fbfa |
693 | if (!empty($course)) { |
694 | $info->course = $course->fullname; |
695 | } |
696 | else { |
697 | $info->course = 'No course'; |
698 | } |
92e754eb |
699 | $info->user = fullname($USER); |
c9b5ebf5 |
700 | $notice = get_string('virusfound', 'moodle', $info); |
701 | $notice .= "\n\n". implode("\n", $output); |
702 | $notice .= "\n\n". clam_handle_infected_file($fullpath); |
3b120e46 |
703 | clam_message_admins($notice); |
18b8fbfa |
704 | if ($appendlog) { |
705 | $info->filename = $file['originalname']; |
c9b5ebf5 |
706 | $file['uploadlog'] .= "\n". get_string('virusfounduser', 'moodle', $info); |
18b8fbfa |
707 | $file['virus'] = 1; |
708 | } |
709 | return false; // in this case, 0 means bad. |
710 | default: |
711 | // error - clam failed to run or something went wrong |
c9b5ebf5 |
712 | $notice .= get_string('clamfailed', 'moodle', get_clam_error_code($return)); |
713 | $notice .= "\n\n". implode("\n", $output); |
18b8fbfa |
714 | $newreturn = true; |
715 | if ($CFG->clamfailureonupload == 'actlikevirus') { |
c9b5ebf5 |
716 | $notice .= "\n". clam_handle_infected_file($fullpath); |
18b8fbfa |
717 | $newreturn = false; |
718 | } |
3b120e46 |
719 | clam_message_admins($notice); |
18b8fbfa |
720 | if ($appendlog) { |
c9b5ebf5 |
721 | $file['uploadlog'] .= "\n". get_string('clambroken'); |
18b8fbfa |
722 | $file['clam'] = 1; |
723 | } |
724 | return $newreturn; // return 1 if we're allowing failures. |
725 | } |
726 | } |
727 | |
728 | /** |
c9b5ebf5 |
729 | * Emails admins about a clam outcome |
730 | * |
731 | * @param string $notice The body of the email to be sent. |
18b8fbfa |
732 | */ |
3b120e46 |
733 | function clam_message_admins($notice) { |
18b8fbfa |
734 | |
735 | $site = get_site(); |
736 | |
268ddd50 |
737 | $subject = get_string('clamemailsubject', 'moodle', format_string($site->fullname)); |
18b8fbfa |
738 | $admins = get_admins(); |
739 | foreach ($admins as $admin) { |
3b120e46 |
740 | $eventdata = new object(); |
741 | $eventdata->modulename = 'moodle'; |
742 | $eventdata->userfrom = get_admin(); |
743 | $eventdata->userto = $admin; |
744 | $eventdata->subject = $subject; |
745 | $eventdata->fullmessage = $notice; |
746 | $eventdata->fullmessageformat = FORMAT_PLAIN; |
747 | $eventdata->fullmessagehtml = ''; |
748 | $eventdata->smallmessage = ''; |
749 | events_trigger('message_send', $eventdata); |
18b8fbfa |
750 | } |
751 | } |
752 | |
753 | |
c9b5ebf5 |
754 | /** |
755 | * Returns the string equivalent of a numeric clam error code |
756 | * |
757 | * @param int $returncode The numeric error code in question. |
758 | * return string The definition of the error code |
759 | */ |
18b8fbfa |
760 | function get_clam_error_code($returncode) { |
761 | $returncodes = array(); |
762 | $returncodes[0] = 'No virus found.'; |
763 | $returncodes[1] = 'Virus(es) found.'; |
764 | $returncodes[2] = ' An error occured'; // specific to clamdscan |
765 | // all after here are specific to clamscan |
766 | $returncodes[40] = 'Unknown option passed.'; |
767 | $returncodes[50] = 'Database initialization error.'; |
768 | $returncodes[52] = 'Not supported file type.'; |
769 | $returncodes[53] = 'Can\'t open directory.'; |
770 | $returncodes[54] = 'Can\'t open file. (ofm)'; |
771 | $returncodes[55] = 'Error reading file. (ofm)'; |
772 | $returncodes[56] = 'Can\'t stat input file / directory.'; |
773 | $returncodes[57] = 'Can\'t get absolute path name of current working directory.'; |
774 | $returncodes[58] = 'I/O error, please check your filesystem.'; |
775 | $returncodes[59] = 'Can\'t get information about current user from /etc/passwd.'; |
776 | $returncodes[60] = 'Can\'t get information about user \'clamav\' (default name) from /etc/passwd.'; |
777 | $returncodes[61] = 'Can\'t fork.'; |
778 | $returncodes[63] = 'Can\'t create temporary files/directories (check permissions).'; |
779 | $returncodes[64] = 'Can\'t write to temporary directory (please specify another one).'; |
780 | $returncodes[70] = 'Can\'t allocate and clear memory (calloc).'; |
781 | $returncodes[71] = 'Can\'t allocate memory (malloc).'; |
782 | if ($returncodes[$returncode]) |
783 | return $returncodes[$returncode]; |
784 | return get_string('clamunknownerror'); |
785 | |
786 | } |
787 | |
788 | /** |
c9b5ebf5 |
789 | * Adds a file upload to the log table so that clam can resolve the filename to the user later if necessary |
790 | * |
72fb21b6 |
791 | * @global object |
792 | * @global object |
c9b5ebf5 |
793 | * @param string $newfilepath ? |
794 | * @param course $course {@link $COURSE} |
795 | * @param boolean $nourl ? |
796 | * @todo Finish documenting this function |
18b8fbfa |
797 | */ |
c9b5ebf5 |
798 | function clam_log_upload($newfilepath, $course=null, $nourl=false) { |
799 | global $CFG, $USER; |
18b8fbfa |
800 | // get rid of any double // that might have appeared |
c9b5ebf5 |
801 | $newfilepath = preg_replace('/\/\//', '/', $newfilepath); |
802 | if (strpos($newfilepath, $CFG->dataroot) === false) { |
803 | $newfilepath = $CFG->dataroot .'/'. $newfilepath; |
18b8fbfa |
804 | } |
18b8fbfa |
805 | $courseid = 0; |
806 | if ($course) { |
807 | $courseid = $course->id; |
808 | } |
c9b5ebf5 |
809 | add_to_log($courseid, 'upload', 'upload', ((!$nourl) ? substr($_SERVER['HTTP_REFERER'], 0, 100) : ''), $newfilepath); |
18b8fbfa |
810 | } |
811 | |
7604c7db |
812 | /** |
813 | * This function logs to error_log and to the log table that an infected file has been found and what's happened to it. |
c9b5ebf5 |
814 | * |
72fb21b6 |
815 | * @global object |
c9b5ebf5 |
816 | * @param string $oldfilepath Full path to the infected file before it was moved. |
817 | * @param string $newfilepath Full path to the infected file since it was moved to the quarantine directory (if the file was deleted, leave empty). |
818 | * @param int $userid The user id of the user who uploaded the file. |
7604c7db |
819 | */ |
c9b5ebf5 |
820 | function clam_log_infected($oldfilepath='', $newfilepath='', $userid=0) { |
bdd3e83a |
821 | global $DB; |
7604c7db |
822 | |
c9b5ebf5 |
823 | add_to_log(0, 'upload', 'infected', $_SERVER['HTTP_REFERER'], $oldfilepath, 0, $userid); |
7604c7db |
824 | |
bdd3e83a |
825 | $user = $DB->get_record('user', array('id'=>$userid)); |
7604c7db |
826 | |
827 | $errorstr = 'Clam AV has found a file that is infected with a virus. It was uploaded by ' |
436d4c6b |
828 | . ((empty($user)) ? ' an unknown user ' : fullname($user)) |
7604c7db |
829 | . ((empty($oldfilepath)) ? '. The infected file was caught on upload ('.$oldfilepath.')' |
c9b5ebf5 |
830 | : '. The original file path of the infected file was '. $oldfilepath) |
831 | . ((empty($newfilepath)) ? '. The file has been deleted ' : '. The file has been moved to a quarantine directory and the new path is '. $newfilepath); |
7604c7db |
832 | |
833 | error_log($errorstr); |
834 | } |
835 | |
836 | |
18b8fbfa |
837 | /** |
c9b5ebf5 |
838 | * Some of the modules allow moving attachments (glossary), in which case we need to hunt down an original log and change the path. |
839 | * |
72fb21b6 |
840 | * @global object |
c9b5ebf5 |
841 | * @param string $oldpath The old path to the file (should be in the log) |
842 | * @param string $newpath The new path to the file |
843 | * @param boolean $update If true this function will overwrite old record (used for forum moving etc). |
18b8fbfa |
844 | */ |
c9b5ebf5 |
845 | function clam_change_log($oldpath, $newpath, $update=true) { |
bdd3e83a |
846 | global $DB; |
8930f900 |
847 | |
bdd3e83a |
848 | if (!$record = $DB->get_record('log', array('info'=>$oldpath, 'module'=>'upload'))) { |
8930f900 |
849 | return false; |
850 | } |
851 | $record->info = $newpath; |
852 | if ($update) { |
bdd3e83a |
853 | $DB->update_record('log', $record); |
854 | } else { |
8930f900 |
855 | unset($record->id); |
bdd3e83a |
856 | $DB->insert_record('log', $record); |
8930f900 |
857 | } |
18b8fbfa |
858 | } |
9056cec0 |
859 | ?> |