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