blob: dc3fd4a1201084e4b617e268beaa4edd1aa2c998 [file] [log] [blame]
Derek Allardd2df9bc2007-04-15 17:41:17 +00001<?php if (!defined('BASEPATH')) exit('No direct script access allowed');
2/**
3 * CodeIgniter
4 *
5 * An open source application development framework for PHP 4.3.2 or newer
6 *
7 * @package CodeIgniter
8 * @author Rick Ellis
9 * @copyright Copyright (c) 2006, EllisLab, Inc.
10 * @license http://www.codeignitor.com/user_guide/license.html
11 * @link http://www.codeigniter.com
12 * @since Version 1.0
13 * @filesource
14 */
15
16// ------------------------------------------------------------------------
17
18/**
19 * File Uploading Class
20 *
21 * @package CodeIgniter
22 * @subpackage Libraries
23 * @category Uploads
24 * @author Rick Ellis
25 * @link http://www.codeigniter.com/user_guide/libraries/file_uploading.html
26 */
27class CI_Upload {
28
29 var $max_size = 0;
30 var $max_width = 0;
31 var $max_height = 0;
32 var $allowed_types = "";
33 var $file_temp = "";
34 var $file_name = "";
35 var $orig_name = "";
36 var $file_type = "";
37 var $file_size = "";
38 var $file_ext = "";
39 var $upload_path = "";
40 var $overwrite = FALSE;
41 var $encrypt_name = FALSE;
42 var $is_image = FALSE;
43 var $image_width = '';
44 var $image_height = '';
45 var $image_type = '';
46 var $image_size_str = '';
47 var $error_msg = array();
48 var $mimes = array();
49 var $remove_spaces = TRUE;
50 var $xss_clean = FALSE;
51 var $temp_prefix = "temp_file_";
52
53 /**
54 * Constructor
55 *
56 * @access public
57 */
58 function CI_Upload($props = array())
59 {
60 if (count($props) > 0)
61 {
62 $this->initialize($props);
63 }
64
65 log_message('debug', "Upload Class Initialized");
66 }
67
68 // --------------------------------------------------------------------
69
70 /**
71 * Initialize preferences
72 *
73 * @access public
74 * @param array
75 * @return void
76 */
77 function initialize($config = array())
78 {
79 $defaults = array(
80 'max_size' => 0,
81 'max_width' => 0,
82 'max_height' => 0,
83 'allowed_types' => "",
84 'file_temp' => "",
85 'file_name' => "",
86 'orig_name' => "",
87 'file_type' => "",
88 'file_size' => "",
89 'file_ext' => "",
90 'upload_path' => "",
91 'overwrite' => FALSE,
92 'encrypt_name' => FALSE,
93 'is_image' => FALSE,
94 'image_width' => '',
95 'image_height' => '',
96 'image_type' => '',
97 'image_size_str' => '',
98 'error_msg' => array(),
99 'mimes' => array(),
100 'remove_spaces' => TRUE,
101 'xss_clean' => FALSE,
102 'temp_prefix' => "temp_file_"
103 );
104
105
106 foreach ($defaults as $key => $val)
107 {
108 if (isset($config[$key]))
109 {
110 $method = 'set_'.$key;
111 if (method_exists($this, $method))
112 {
113 $this->$method($config[$key]);
114 }
115 else
116 {
117 $this->$key = $config[$key];
118 }
119 }
120 else
121 {
122 $this->$key = $val;
123 }
124 }
125 }
126
127 // --------------------------------------------------------------------
128
129 /**
130 * Perform the file upload
131 *
132 * @access public
133 * @return bool
134 */
135 function do_upload($field = 'userfile')
136 {
137 // Is $_FILES[$field] set? If not, no reason to continue.
138 if ( ! isset($_FILES[$field]))
139 {
140 $this->set_error('upload_userfile_not_set');
141 return FALSE;
142 }
143
144 // Is the upload path valid?
145 if ( ! $this->validate_upload_path())
146 {
147 return FALSE;
148 }
149
150 // Was the file able to be uploaded? If not, determine the reason why.
151 if ( ! is_uploaded_file($_FILES[$field]['tmp_name']))
152 {
153 $error = ( ! isset($_FILES[$field]['error'])) ? 4 : $_FILES[$field]['error'];
154
155 switch($error)
156 {
157 case 1 : $this->set_error('upload_file_exceeds_limit');
158 break;
159 case 3 : $this->set_error('upload_file_partial');
160 break;
161 case 4 : $this->set_error('upload_no_file_selected');
162 break;
163 default : $this->set_error('upload_no_file_selected');
164 break;
165 }
166
167 return FALSE;
168 }
169
170 // Set the uploaded data as class variables
171 $this->file_temp = $_FILES[$field]['tmp_name'];
172 $this->file_name = $_FILES[$field]['name'];
173 $this->file_size = $_FILES[$field]['size'];
174 $this->file_type = preg_replace("/^(.+?);.*$/", "\\1", $_FILES[$field]['type']);
175 $this->file_type = strtolower($this->file_type);
176 $this->file_ext = $this->get_extension($_FILES[$field]['name']);
177
178 // Convert the file size to kilobytes
179 if ($this->file_size > 0)
180 {
181 $this->file_size = round($this->file_size/1024, 2);
182 }
183
184 // Is the file type allowed to be uploaded?
185 if ( ! $this->is_allowed_filetype())
186 {
187 $this->set_error('upload_invalid_filetype');
188 return FALSE;
189 }
190
191 // Is the file size within the allowed maximum?
192 if ( ! $this->is_allowed_filesize())
193 {
194 $this->set_error('upload_invalid_filesize');
195 return FALSE;
196 }
197
198 // Are the image dimensions within the allowed size?
199 // Note: This can fail if the server has an open_basdir restriction.
200 if ( ! $this->is_allowed_dimensions())
201 {
202 $this->set_error('upload_invalid_dimensions');
203 return FALSE;
204 }
205
206 // Sanitize the file name for security
207 $this->file_name = $this->clean_file_name($this->file_name);
208
209 // Remove white spaces in the name
210 if ($this->remove_spaces == TRUE)
211 {
212 $this->file_name = preg_replace("/\s+/", "_", $this->file_name);
213 }
214
215 /*
216 * Validate the file name
217 * This function appends an number onto the end of
218 * the file if one with the same name already exists.
219 * If it returns false there was a problem.
220 */
221 $this->orig_name = $this->file_name;
222
223 if ($this->overwrite == FALSE)
224 {
225 $this->file_name = $this->set_filename($this->upload_path, $this->file_name);
226
227 if ($this->file_name === FALSE)
228 {
229 return FALSE;
230 }
231 }
232
233 /*
234 * Move the file to the final destination
235 * To deal with different server configurations
236 * we'll attempt to use copy() first. If that fails
237 * we'll use move_uploaded_file(). One of the two should
238 * reliably work in most environments
239 */
240 if ( ! @copy($this->file_temp, $this->upload_path.$this->file_name))
241 {
242 if ( ! @move_uploaded_file($this->file_temp, $this->upload_path.$this->file_name))
243 {
244 $this->set_error('upload_destination_error');
245 return FALSE;
246 }
247 }
248
249 /*
250 * Run the file through the XSS hacking filter
251 * This helps prevent malicious code from being
252 * embedded within a file. Scripts can easily
253 * be disguised as images or other file types.
254 */
255 if ($this->xss_clean == TRUE)
256 {
257 $this->do_xss_clean();
258 }
259
260 /*
261 * Set the finalized image dimensions
262 * This sets the image width/height (assuming the
263 * file was an image). We use this information
264 * in the "data" function.
265 */
266 $this->set_image_properties($this->upload_path.$this->file_name);
267
268 return TRUE;
269 }
270
271 // --------------------------------------------------------------------
272
273 /**
274 * Finalized Data Array
275 *
276 * Returns an associative array containing all of the information
277 * related to the upload, allowing the developer easy access in one array.
278 *
279 * @access public
280 * @return array
281 */
282 function data()
283 {
284 return array (
285 'file_name' => $this->file_name,
286 'file_type' => $this->file_type,
287 'file_path' => $this->upload_path,
288 'full_path' => $this->upload_path.$this->file_name,
289 'raw_name' => str_replace($this->file_ext, '', $this->file_name),
290 'orig_name' => $this->orig_name,
291 'file_ext' => $this->file_ext,
292 'file_size' => $this->file_size,
293 'is_image' => $this->is_image(),
294 'image_width' => $this->image_width,
295 'image_height' => $this->image_height,
296 'image_type' => $this->image_type,
297 'image_size_str' => $this->image_size_str,
298 );
299 }
300
301 // --------------------------------------------------------------------
302
303 /**
304 * Set Upload Path
305 *
306 * @access public
307 * @param string
308 * @return void
309 */
310 function set_upload_path($path)
311 {
312 $this->upload_path = $path;
313 }
314
315 // --------------------------------------------------------------------
316
317 /**
318 * Set the file name
319 *
320 * This function takes a filename/path as input and looks for the
321 * existence of a file with the same name. If found, it will append a
322 * number to the end of the filename to avoid overwriting a pre-existing file.
323 *
324 * @access public
325 * @param string
326 * @param string
327 * @return string
328 */
329 function set_filename($path, $filename)
330 {
331 if ($this->encrypt_name == TRUE)
332 {
333 mt_srand();
334 $filename = md5(uniqid(mt_rand())).$this->file_ext;
335 }
336
337 if ( ! file_exists($path.$filename))
338 {
339 return $filename;
340 }
341
342 $filename = str_replace($this->file_ext, '', $filename);
343
344 $new_filename = '';
345 for ($i = 1; $i < 100; $i++)
346 {
347 if ( ! file_exists($path.$filename.$i.$this->file_ext))
348 {
349 $new_filename = $filename.$i.$this->file_ext;
350 break;
351 }
352 }
353
354 if ($new_filename == '')
355 {
356 $this->set_error('upload_bad_filename');
357 return FALSE;
358 }
359 else
360 {
361 return $new_filename;
362 }
363 }
364
365 // --------------------------------------------------------------------
366
367 /**
368 * Set Maximum File Size
369 *
370 * @access public
371 * @param integer
372 * @return void
373 */
374 function set_max_filesize($n)
375 {
376 $this->max_size = ( ! eregi("^[[:digit:]]+$", $n)) ? 0 : $n;
377 }
378
379 // --------------------------------------------------------------------
380
381 /**
382 * Set Maximum Image Width
383 *
384 * @access public
385 * @param integer
386 * @return void
387 */
388 function set_max_width($n)
389 {
390 $this->max_width = ( ! eregi("^[[:digit:]]+$", $n)) ? 0 : $n;
391 }
392
393 // --------------------------------------------------------------------
394
395 /**
396 * Set Maximum Image Height
397 *
398 * @access public
399 * @param integer
400 * @return void
401 */
402 function set_max_height($n)
403 {
404 $this->max_height = ( ! eregi("^[[:digit:]]+$", $n)) ? 0 : $n;
405 }
406
407 // --------------------------------------------------------------------
408
409 /**
410 * Set Allowed File Types
411 *
412 * @access public
413 * @param string
414 * @return void
415 */
416 function set_allowed_types($types)
417 {
418 $this->allowed_types = explode('|', $types);
419 }
420
421 // --------------------------------------------------------------------
422
423 /**
424 * Set Image Properties
425 *
426 * Uses GD to determine the width/height/type of image
427 *
428 * @access public
429 * @param string
430 * @return void
431 */
432 function set_image_properties($path = '')
433 {
434 if ( ! $this->is_image())
435 {
436 return;
437 }
438
439 if (function_exists('getimagesize'))
440 {
441 if (FALSE !== ($D = @getimagesize($path)))
442 {
443 $types = array(1 => 'gif', 2 => 'jpeg', 3 => 'png');
444
445 $this->image_width = $D['0'];
446 $this->image_height = $D['1'];
447 $this->image_type = ( ! isset($types[$D['2']])) ? 'unknown' : $types[$D['2']];
448 $this->image_size_str = $D['3']; // string containing height and width
449 }
450 }
451 }
452
453 // --------------------------------------------------------------------
454
455 /**
456 * Set XSS Clean
457 *
458 * Enables the XSS flag so that the file that was uploaded
459 * will be run through the XSS filter.
460 *
461 * @access public
462 * @param bool
463 * @return void
464 */
465 function set_xss_clean($flag = FALSE)
466 {
467 $this->xss_clean = ($flag == TRUE) ? TRUE : FALSE;
468 }
469
470 // --------------------------------------------------------------------
471
472 /**
473 * Validate the image
474 *
475 * @access public
476 * @return bool
477 */
478 function is_image()
479 {
480 $img_mimes = array(
481 'image/gif',
482 'image/jpg',
483 'image/jpe',
484 'image/jpeg',
485 'image/pjpeg',
486 'image/png',
487 'image/x-png'
488 );
489
490
491 return (in_array($this->file_type, $img_mimes, TRUE)) ? TRUE : FALSE;
492 }
493
494 // --------------------------------------------------------------------
495
496 /**
497 * Verify that the filetype is allowed
498 *
499 * @access public
500 * @return bool
501 */
502 function is_allowed_filetype()
503 {
504 if (count($this->allowed_types) == 0)
505 {
506 $this->set_error('upload_no_file_types');
507 return FALSE;
508 }
509
510 foreach ($this->allowed_types as $val)
511 {
512 $mime = $this->mimes_types(strtolower($val));
513
514 if (is_array($mime))
515 {
516 if (in_array($this->file_type, $mime, TRUE))
517 {
518 return TRUE;
519 }
520 }
521 else
522 {
523 if ($mime == $this->file_type)
524 {
525 return TRUE;
526 }
527 }
528 }
529
530 return FALSE;
531 }
532
533 // --------------------------------------------------------------------
534
535 /**
536 * Verify that the file is within the allowed size
537 *
538 * @access public
539 * @return bool
540 */
541 function is_allowed_filesize()
542 {
543 if ($this->max_size != 0 AND $this->file_size > $this->max_size)
544 {
545 return FALSE;
546 }
547 else
548 {
549 return TRUE;
550 }
551 }
552
553 // --------------------------------------------------------------------
554
555 /**
556 * Verify that the image is within the allowed width/height
557 *
558 * @access public
559 * @return bool
560 */
561 function is_allowed_dimensions()
562 {
563 if ( ! $this->is_image())
564 {
565 return TRUE;
566 }
567
568 if (function_exists('getimagesize'))
569 {
570 $D = @getimagesize($this->file_temp);
571
572 if ($this->max_width > 0 AND $D['0'] > $this->max_width)
573 {
574 return FALSE;
575 }
576
577 if ($this->max_height > 0 AND $D['1'] > $this->max_height)
578 {
579 return FALSE;
580 }
581
582 return TRUE;
583 }
584
585 return TRUE;
586 }
587
588 // --------------------------------------------------------------------
589
590 /**
591 * Validate Upload Path
592 *
593 * Verifies that it is a valid upload path with proper permissions.
594 *
595 *
596 * @access public
597 * @return bool
598 */
599 function validate_upload_path()
600 {
601 if ($this->upload_path == '')
602 {
603 $this->set_error('upload_no_filepath');
604 return FALSE;
605 }
606
607 if (function_exists('realpath') AND @realpath($this->upload_path) !== FALSE)
608 {
609 $this->upload_path = str_replace("\\", "/", realpath($this->upload_path));
610 }
611
612 if ( ! @is_dir($this->upload_path))
613 {
614 $this->set_error('upload_no_filepath');
615 return FALSE;
616 }
617
618 if ( ! is_writable($this->upload_path))
619 {
620 $this->set_error('upload_not_writable');
621 return FALSE;
622 }
623
624 $this->upload_path = preg_replace("/(.+?)\/*$/", "\\1/", $this->upload_path);
625 return TRUE;
626 }
627
628 // --------------------------------------------------------------------
629
630 /**
631 * Extract the file extension
632 *
633 * @access public
634 * @param string
635 * @return string
636 */
637 function get_extension($filename)
638 {
639 $x = explode('.', $filename);
640 return '.'.end($x);
641 }
642
643 // --------------------------------------------------------------------
644
645 /**
646 * Clean the file name for security
647 *
648 * @access public
649 * @param string
650 * @return string
651 */
652 function clean_file_name($filename)
653 {
654 $bad = array(
655 "<!--",
656 "-->",
657 "'",
658 "<",
659 ">",
660 '"',
661 '&',
662 '$',
663 '=',
664 ';',
665 '?',
666 '/',
667 "%20",
668 "%22",
669 "%3c", // <
670 "%253c", // <
671 "%3e", // >
672 "%0e", // >
673 "%28", // (
674 "%29", // )
675 "%2528", // (
676 "%26", // &
677 "%24", // $
678 "%3f", // ?
679 "%3b", // ;
680 "%3d" // =
681 );
682
683 foreach ($bad as $val)
684 {
685 $filename = str_replace($val, '', $filename);
686 }
687
688 return $filename;
689 }
690
691 // --------------------------------------------------------------------
692
693 /**
694 * Runs the file through the XSS clean function
695 *
696 * This prevents people from embedding malicious code in their files.
697 * I'm not sure that it won't negatively affect certain files in unexpected ways,
698 * but so far I haven't found that it causes trouble.
699 *
700 * @access public
701 * @return void
702 */
703 function do_xss_clean()
704 {
705 $file = $this->upload_path.$this->file_name;
706
707 if (filesize($file) == 0)
708 {
709 return FALSE;
710 }
Rick Ellis63966df2007-06-11 04:44:11 +0000711
712 if (($data = @file_get_contents($file)) !== FALSE)
Rick Ellis64bbd042007-06-11 04:35:52 +0000713 {
714 return FALSE;
715 }
716
Rick Ellise6b4a802007-06-11 04:20:44 +0000717 if ( ! $fp = @fopen($file, 'r+b'))
Derek Allardd2df9bc2007-04-15 17:41:17 +0000718 {
719 return FALSE;
720 }
Derek Allardd2df9bc2007-04-15 17:41:17 +0000721
Derek Allardd2df9bc2007-04-15 17:41:17 +0000722 $CI =& get_instance();
723 $data = $CI->input->xss_clean($data);
Rick Ellis64bbd042007-06-11 04:35:52 +0000724
725 flock($fp, LOCK_EX);
Derek Allardd2df9bc2007-04-15 17:41:17 +0000726 fwrite($fp, $data);
727 flock($fp, LOCK_UN);
728 fclose($fp);
729 }
730
731 // --------------------------------------------------------------------
732
733 /**
734 * Set an error message
735 *
736 * @access public
737 * @param string
738 * @return void
739 */
740 function set_error($msg)
741 {
742 $CI =& get_instance();
743 $CI->lang->load('upload');
744
745 if (is_array($msg))
746 {
747 foreach ($msg as $val)
748 {
749 $msg = ($CI->lang->line($val) == FALSE) ? $val : $CI->lang->line($val);
750 $this->error_msg[] = $msg;
751 log_message('error', $msg);
752 }
753 }
754 else
755 {
756 $msg = ($CI->lang->line($msg) == FALSE) ? $msg : $CI->lang->line($msg);
757 $this->error_msg[] = $msg;
758 log_message('error', $msg);
759 }
760 }
761
762 // --------------------------------------------------------------------
763
764 /**
765 * Display the error message
766 *
767 * @access public
768 * @param string
769 * @param string
770 * @return string
771 */
772 function display_errors($open = '<p>', $close = '</p>')
773 {
774 $str = '';
775 foreach ($this->error_msg as $val)
776 {
777 $str .= $open.$val.$close;
778 }
779
780 return $str;
781 }
782
783 // --------------------------------------------------------------------
784
785 /**
786 * List of Mime Types
787 *
788 * This is a list of mime types. We use it to validate
789 * the "allowed types" set by the developer
790 *
791 * @access public
792 * @param string
793 * @return string
794 */
795 function mimes_types($mime)
796 {
797 if (count($this->mimes) == 0)
798 {
799 if (@include(APPPATH.'config/mimes'.EXT))
800 {
801 $this->mimes = $mimes;
802 unset($mimes);
803 }
804 }
805
806 return ( ! isset($this->mimes[$mime])) ? FALSE : $this->mimes[$mime];
807 }
808
809}
810// END Upload Class
adminb0dd10f2006-08-25 17:25:49 +0000811?>