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