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