root/releases/0.6rc2/lib/uploadlib.php

Revision 269, 33.3 kB (checked in by ben, 3 years ago)

--

  • Property svn:eol-style set to native
Line 
1 <?php
2
3 /**
4  * uploadlib.php - This class handles all aspects of fileuploading
5  *
6  * @author Penny Leach
7  * @version 1.5
8  * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
9  * @package moodlecore
10  */
11
12
13 /**
14  * This class handles all aspects of fileuploading
15  */
16 class upload_manager {
17
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     * If we're only getting one file.
36     * (for logging and virus notifications)
37     * @var string $inputname
38     */
39     var $inputname;
40    /**
41     * If we're given silent=true in the constructor, this gets built
42     * up to hold info about the process.
43     * @var string $notify
44     */
45     var $notify;
46
47     /**
48      * Constructor, sets up configuration stuff so we know how to act.
49      *
50      * 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.
51      *
52      * @uses $CFG
53      * @param string $inputname If this is given the upload manager will only process the file in $_FILES with this name.
54      * @param boolean $deleteothers Whether to delete other files in the destination directory (optional, defaults to false)
55      * @param boolean $handlecollisions Whether to use {@link handle_filename_collision()} or not. (optional, defaults to false)
56      * @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.
57      * @param int $maxbytes max bytes for this file {@link get_max_upload_file_size()}.
58      * @param boolean $silent Whether to notify errors or not.
59      * @param boolean $allownull Whether we care if there's no file when we've set the input name.
60      * @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.
61      */
62     function upload_manager($inputname='', $deleteothers=false, $handlecollisions=false, $recoverifmultiple=false, $maxbytes=0, $silent=false, $allownull=false, $allownullmultiple=true) {
63         
64         global $CFG;
65         
66         $this->config->deleteothers = $deleteothers;
67         $this->config->handlecollisions = $handlecollisions;
68         $this->config->recoverifmultiple = $recoverifmultiple;
69         $this->config->maxbytes = get_max_upload_file_size($maxbytes);
70         $this->config->silent = $silent;
71         $this->config->allownull = $allownull;
72         $this->files = array();
73         $this->status = false;
74         $this->inputname = $inputname;
75         if (empty($this->inputname)) {
76             $this->config->allownull = $allownullmultiple;
77         }
78     }
79     
80     /**
81      * Gets all entries out of $_FILES and stores them locally in $files and then
82      * checks each one against {@link get_max_upload_file_size()} and calls {@link cleanfilename()}
83      * and scans them for viruses etc.
84      * @uses $CFG
85      * @uses $_FILES
86      * @return boolean
87      */
88     function preprocess_files() {
89         global $CFG;
90
91         foreach ($_FILES as $name => $file) {
92             $this->status = true; // only set it to true here so that we can check if this function has been called.
93             if (empty($this->inputname) || $name == $this->inputname) { // if we have input name, only process if it matches.
94                 $file['originalname'] = $file['name']; // do this first for the log.
95                 $this->files[$name] = $file; // put it in first so we can get uploadlog out in print_upload_log.
96                 $this->status = $this->validate_file($this->files[$name]); // default to only allowing empty on multiple uploads.
97                 if (!$this->status && ($this->files[$name]['error'] == 0 || $this->files[$name]['error'] == 4) && ($this->config->allownull || empty($this->inputname))) {
98                     // this shouldn't cause everything to stop.. modules should be responsible for knowing which if any are compulsory.
99                     continue;
100                 }
101                 if ($this->status && $CFG->runclamonupload) {
102                     $this->status = clam_scan_file($this->files[$name]);
103                 }
104                 if (!$this->status) {
105                     if (!$this->config->recoverifmultiple && count($this->files) > 1) {
106                         $a->name = $this->files[$name]['originalname'];
107                         $a->problem = $this->files[$name]['uploadlog'];
108                         $msg = sprintf(gettext('Your file upload has failed because there was a problem with one of the files, %s.<br /> Here is a log of the problems:<br />%s<br />Not recovering.'), $a->name, $a->problem);
109                         if (!$this->config->silent) {
110                             notify($msg);
111                         }
112                         else {
113                             $this->notify .= '<br />'. $msg;
114                         }
115                         $this->status = false;
116                         return false;
117
118                     } else if (count($this->files) == 1) {
119
120                         if (!$this->config->silent and !$this->config->allownull) {
121                             notify($this->files[$name]['uploadlog']);
122                         } else {
123                             $this->notify .= '<br />'. $this->files[$name]['uploadlog'];
124                         }
125                         $this->status = false;
126                         return false;
127                     }
128                 }
129                 else {
130                     $newname = clean_filename($this->files[$name]['name']);
131                     if ($newname != $this->files[$name]['name']) {
132                         $a->oldname = $this->files[$name]['name'];
133                         $a->newname = $newname;
134                         $this->files[$name]['uploadlog'] .= sprintf(gettext('File was renamed from %s to %s because of invalid characters.'), $a->oldname, $a->newname);
135                     }
136                     $this->files[$name]['name'] = $newname;
137                     $this->files[$name]['clear'] = true; // ok to save.
138                 }
139             }
140         }
141         if (!is_array($_FILES) || count($_FILES) == 0) {
142             return $this->config->allownull;
143         }
144         $this->status = true;
145         return true; // if we've got this far it means that we're recovering so we want status to be ok.
146     }
147
148     /**
149      * Validates a single file entry from _FILES
150      *
151      * @param object $file The entry from _FILES to validate
152      * @return boolean True if ok.
153      */
154     function validate_file(&$file) {
155         if (empty($file)) {
156             return false;
157         }
158         if (!is_uploaded_file($file['tmp_name']) || $file['size'] == 0) {
159             $file['uploadlog'] .= "\n".$this->get_file_upload_error($file);
160             return false;
161         }
162         if ($file['size'] > $this->config->maxbytes) {
163             $file['uploadlog'] .= "\n". sprintf(gettext('Sorry, but that file is too big (limit is %s)'), display_size($this->config->maxbytes));
164             return false;
165         }
166         return true;
167     }
168
169     /**
170      * Moves all the files to the destination directory.
171      *
172      * @uses $CFG
173      * @uses $USER
174      * @param string $destination The destination directory.
175      * @return boolean status;
176      */
177     function save_files($destination) {
178         global $CFG, $USER;
179         $textlib = textlib_get_instance();
180         
181         if (!$this->status) { // preprocess_files hasn't been run
182             $this->preprocess_files();
183         }
184         if ($this->status) {
185             if (!($textlib->strpos($destination, $CFG->dataroot) === false)) {
186                 // take it out for giving to make_upload_directory
187                 $destination = $textlib->substr($destination, $textlib->strlen($CFG->dataroot));
188             }
189
190             if ($destination{$textlib->strlen($destination)-1} == '/') { // strip off a trailing / if we have one
191                 $destination = $textlib->substr($destination, 0, -1);
192             }
193
194             if (!make_upload_directory($destination, true)) { //TODO maybe put this function here instead of moodlelib.php now.
195                 $this->status = false;
196                 return false;
197             }
198             
199             $destination = $CFG->dataroot .'/'. $destination; // now add it back in so we have a full path
200
201             $exceptions = array(); //need this later if we're deleting other files.
202
203             foreach (array_keys($this->files) as $i) {
204
205                 if (!$this->files[$i]['clear']) {
206                     // not ok to save
207                     continue;
208                 }
209
210                 if ($this->config->handlecollisions) {
211                     $this->handle_filename_collision($destination, $this->files[$i]);
212                 }
213                 if (move_uploaded_file($this->files[$i]['tmp_name'], $destination.'/'.$this->files[$i]['name'])) {
214                     chmod($destination .'/'. $this->files[$i]['name'], $CFG->directorypermissions);
215                     $this->files[$i]['fullpath'] = $destination.'/'.$this->files[$i]['name'];
216                     $this->files[$i]['uploadlog'] .= "\n".gettext('File uploaded successfully');
217                     $this->files[$i]['saved'] = true;
218                     $exceptions[] = $this->files[$i]['name'];
219                     // now add it to the log (this is important so we know who to notify if a virus is found later on)
220                     clam_log_upload($this->files[$i]['fullpath']);
221                     $savedsomething=true;
222                 }
223             }
224             if ($savedsomething && $this->config->deleteothers) {
225                 $this->delete_other_files($destination, $exceptions);
226             }
227         }
228         if (empty($savedsomething)) {
229             $this->status = false;
230             if ((empty($this->config->allownull) && !empty($this->inputname)) || (empty($this->inputname) && empty($this->config->allownullmultiple))) {
231                 notify(gettext('No file was found - are you sure you selected one to upload?'));
232             }
233             return false;
234         }
235         return $this->status;
236     }
237     
238     /**
239      * Wrapper function that calls {@link preprocess_files()} and {@link viruscheck_files()} and then {@link save_files()}
240      * Modules that require the insert id in the filepath should not use this and call these functions seperately in the required order.
241      * @parameter string $destination Where to save the uploaded files to.
242      * @return boolean
243      */
244     function process_file_uploads($destination) {
245         if ($this->preprocess_files()) {
246             return $this->save_files($destination);
247         }
248         return false;
249     }
250
251     /**
252      * Deletes all the files in a given directory except for the files in $exceptions (full paths)
253      *
254      * @param string $destination The directory to clean up.
255      * @param array $exceptions Full paths of files to KEEP.
256      */
257     function delete_other_files($destination, $exceptions=null) {
258         if ($filestodel = get_directory_list($destination)) {
259             foreach ($filestodel as $file) {
260                 if (!is_array($exceptions) || !in_array($file, $exceptions)) {
261                     unlink($destination .'/'. $file);
262                     $deletedsomething = true;
263                 }
264             }
265         }
266         if ($deletedsomething) {
267             $msg = gettext('The old file(s) in your upload area have been deleted');
268             if (!$this->config->silent) {
269                 notify($msg);
270             }
271             else {
272                 $this->notify .= '<br />'. $msg;
273             }
274         }
275     }
276     
277     /**
278      * Handles filename collisions - if the desired filename exists it will rename it according to the pattern in $format
279      * @param string $destination Destination directory (to check existing files against)
280      * @param object $file Passed in by reference. The current file from $files we're processing.
281      * @param string $format The printf style format to rename the file to (defaults to filename_number.extn)
282      * @return string The new filename.
283      * @todo verify return type - this function does not appear to return anything since $file is passed in by reference
284      */
285     function handle_filename_collision($destination, &$file, $format='%s_%d.%s') {
286         $bits = explode('.', $file['name']);
287         // check for collisions and append a nice numberydoo.
288         if (file_exists($destination .'/'. $file['name'])) {
289             $a->oldname = $file['name'];
290             for ($i = 1; true; $i++) {
291                 $try = sprintf($format, $bits[0], $i, $bits[1]);
292                 if ($this->check_before_renaming($destination, $try, $file)) {
293                     $file['name'] = $try;
294                     break;
295                 }
296             }
297             $a->newname = $file['name'];
298             $file['uploadlog'] .= "\n". sprintf(gettext('File was renamed from %s to %s because there was a filename conflict.'), $a->oldname, $a->newname);
299         }
300     }
301     
302     /**
303      * This function checks a potential filename against what's on the filesystem already and what's been saved already.
304      * @param string $destination Destination directory (to check existing files against)
305      * @param string $nametocheck The filename to be compared.
306      * @param object $file The current file from $files we're processing.
307      * return boolean
308      */
309     function check_before_renaming($destination, $nametocheck, $file) {
310         if (!file_exists($destination .'/'. $nametocheck)) {
311             return true;
312         }
313         if ($this->config->deleteothers) {
314             foreach ($this->files as $tocheck) {
315                 // 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..
316                 if ($file['tmp_name'] != $tocheck['tmp_name'] && $tocheck['clear'] && $nametocheck == $tocheck['name'] && $tocheck['saved']) {
317                     $collision = true;
318                 }
319             }
320             if (!$collision) {
321                 return true;
322             }
323         }
324         return false;
325     }
326
327     /**
328      * ?
329      *
330      * @param object $file Passed in by reference. The current file from $files we're processing.
331      * @return string
332      * @todo Finish documenting this function
333      */
334     function get_file_upload_error(&$file) {
335         
336         switch ($file['error']) {
337         case 0: // UPLOAD_ERR_OK
338             if ($file['size'] > 0) {
339                 $errmessage = sprintf(gettext('An unknown problem occurred while uploading the file \'%s\' (perhaps it was too large?)'), $file['name']);
340             } else {
341                 $errmessage = gettext('No file was found - are you sure you selected one to upload?'); /// probably a dud file name
342             }
343             break;
344             
345         case 1: // UPLOAD_ERR_INI_SIZE
346             $errmessage = gettext('Uploaded file exceeded the maximum size limit set by the server');
347             break;
348             
349         case 2: // UPLOAD_ERR_FORM_SIZE
350             $errmessage = gettext('Uploaded file exceeded the maximum size limit set by the form');
351             break;
352             
353         case 3: // UPLOAD_ERR_PARTIAL
354             $errmessage = gettext('File was only partially uploaded');
355             break;
356             
357         case 4: // UPLOAD_ERR_NO_FILE
358             $errmessage = gettext('No file was found - are you sure you selected one to upload?');
359             break;
360             
361         default:
362             $errmessage = sprintf(gettext('An unknown problem occurred while uploading the file \'%s\' (perhaps it was too large?)'), $file['name']);
363         }
364         return $errmessage;
365     }
366     
367     /**
368      * prints a log of everything that happened (of interest) to each file in _FILES
369      * @param $return - optional, defaults to false (log is echoed)
370      */
371     function print_upload_log($return=false,$skipemptyifmultiple=false) {
372         foreach (array_keys($this->files) as $i => $key) {
373             if (count($this->files) > 1 && !empty($skipemptyifmultiple) && $this->files[$key]['error'] == 4) {
374                 continue;
375             }
376             $str .= '<strong>'. sprintf(gettext('Upload log for file %u'), $i+1) .' '
377                 .((!empty($this->files[$key]['originalname'])) ? '('.$this->files[$key]['originalname'].')' : '')
378                 .'</strong> :'. nl2br($this->files[$key]['uploadlog']) .'<br />';
379         }
380         if ($return) {
381             return $str;
382         }
383         echo $str;
384     }
385
386     /**
387      * If we're only handling one file (if inputname was given in the constructor) this will return the (possibly changed) filename of the file.
388      @return boolean
389      */
390     function get_new_filename() {
391         if (!empty($this->inputname) && count($this->files) == 1) {
392             return $this->files[$this->inputname]['name'];
393         }
394         return false;
395     }
396
397     /**
398      * 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.
399      * @return boolean
400      */
401     function get_new_filepath() {
402         if (!empty($this->inputname) && count($this->files) == 1) {
403             return $this->files[$this->inputname]['fullpath'];
404         }
405         return false;
406     }
407
408     /**
409      * If we're only handling one file (if inputname was given in the constructor) this will return the ORIGINAL filename of the file.
410      * @return boolean
411      */
412     function get_original_filename() {
413         if (!empty($this->inputname) && count($this->files) == 1) {
414             return $this->files[$this->inputname]['originalname'];
415         }
416         return false;
417     }
418
419     /**
420      * If we're only handling on file (if inputname was given in the constructor) this will return the size of the file.
421      */
422     function get_filesize() {
423         if (!empty($this->inputname) && count($this->files) == 1) {
424             return $this->files[$this->inputname]['size'];
425         }
426     }
427
428
429     /**
430      * This function returns any errors wrapped up in red.
431      * @return string
432      */
433     function get_errors() {
434         return $this->notify ;
435     }
436 }
437
438 /**************************************************************************************
439 THESE FUNCTIONS ARE OUTSIDE THE CLASS BECAUSE THEY NEED TO BE CALLED FROM OTHER PLACES.
440 FOR EXAMPLE CLAM_HANDLE_INFECTED_FILE AND CLAM_REPLACE_INFECTED_FILE USED FROM CRON
441 UPLOAD_PRINT_FORM_FRAGMENT DOESN'T REALLY BELONG IN THE CLASS BUT CERTAINLY IN THIS FILE
442 ***************************************************************************************/
443
444
445 /**
446  * This function prints out a number of upload form elements.
447  *
448  * @param int $numfiles The number of elements required (optional, defaults to 1)
449  * @param array $names Array of element names to use (optional, defaults to FILE_n)
450  * @param array $descriptions Array of strings to be printed out before each file bit.
451  * @param boolean $uselabels -Whether to output text fields for file descriptions or not (optional, defaults to false)
452  * @param array $labelnames Array of element names to use for labels (optional, defaults to LABEL_n)
453  * @param int $maxbytes used to calculate upload max size ( using {@link get_max_upload_file_size})
454  * @param boolean $return -Whether to return the string (defaults to false - string is echoed)
455  * @return string Form returned as string if $return is true
456  */
457 function upload_print_form_fragment($numfiles=1, $names=null, $descriptions=null, $uselabels=false, $labelnames=null, $maxbytes=0, $return=false) {
458     global $CFG;
459     $maxbytes = get_max_upload_file_size($CFG->maxbytes, $coursebytes, $maxbytes);
460     $str = '<input type="hidden" name="MAX_FILE_SIZE" value="'. $maxbytes .'" />'."\n";
461     for ($i = 0; $i < $numfiles; $i++) {
462         if (is_array($descriptions) && !empty($descriptions[$i])) {
463