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