root/releases/0.9/lib/uploadlib.php

Revision 1423, 33.5 kB (checked in by rho, 1 year ago)

fixed issue uploading file without checking the checkbox.

This doesn't allow to script to do header redirect because
it's already output text

Signed-off: Rolando Espinoza La fuente <rho@prosoftpeople.com>

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