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