1<?php 2/** 3 * CodeIgniter 4 * 5 * An open source application development framework for PHP 6 * 7 * This content is released under the MIT License (MIT) 8 * 9 * Copyright (c) 2014 - 2018, British Columbia Institute of Technology 10 * 11 * Permission is hereby granted, free of charge, to any person obtaining a copy 12 * of this software and associated documentation files (the "Software"), to deal 13 * in the Software without restriction, including without limitation the rights 14 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 * copies of the Software, and to permit persons to whom the Software is 16 * furnished to do so, subject to the following conditions: 17 * 18 * The above copyright notice and this permission notice shall be included in 19 * all copies or substantial portions of the Software. 20 * 21 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 * THE SOFTWARE. 28 * 29 * @package CodeIgniter 30 * @author EllisLab Dev Team 31 * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/) 32 * @copyright Copyright (c) 2014 - 2018, British Columbia Institute of Technology (http://bcit.ca/) 33 * @license http://opensource.org/licenses/MIT MIT License 34 * @link https://codeigniter.com 35 * @since Version 1.0.0 36 * @filesource 37 */ 38defined('BASEPATH') OR exit('No direct script access allowed'); 39 40/** 41 * File Uploading Class 42 * 43 * @package CodeIgniter 44 * @subpackage Libraries 45 * @category Uploads 46 * @author EllisLab Dev Team 47 * @link https://codeigniter.com/user_guide/libraries/file_uploading.html 48 */ 49class CI_Upload { 50 51 /** 52 * Maximum file size 53 * 54 * @var int 55 */ 56 public $max_size = 0; 57 58 /** 59 * Maximum image width 60 * 61 * @var int 62 */ 63 public $max_width = 0; 64 65 /** 66 * Maximum image height 67 * 68 * @var int 69 */ 70 public $max_height = 0; 71 72 /** 73 * Minimum image width 74 * 75 * @var int 76 */ 77 public $min_width = 0; 78 79 /** 80 * Minimum image height 81 * 82 * @var int 83 */ 84 public $min_height = 0; 85 86 /** 87 * Maximum filename length 88 * 89 * @var int 90 */ 91 public $max_filename = 0; 92 93 /** 94 * Maximum duplicate filename increment ID 95 * 96 * @var int 97 */ 98 public $max_filename_increment = 100; 99 100 /** 101 * Allowed file types 102 * 103 * @var string 104 */ 105 public $allowed_types = ''; 106 107 /** 108 * Temporary filename 109 * 110 * @var string 111 */ 112 public $file_temp = ''; 113 114 /** 115 * Filename 116 * 117 * @var string 118 */ 119 public $file_name = ''; 120 121 /** 122 * Original filename 123 * 124 * @var string 125 */ 126 public $orig_name = ''; 127 128 /** 129 * File type 130 * 131 * @var string 132 */ 133 public $file_type = ''; 134 135 /** 136 * File size 137 * 138 * @var int 139 */ 140 public $file_size = NULL; 141 142 /** 143 * Filename extension 144 * 145 * @var string 146 */ 147 public $file_ext = ''; 148 149 /** 150 * Force filename extension to lowercase 151 * 152 * @var string 153 */ 154 public $file_ext_tolower = FALSE; 155 156 /** 157 * Upload path 158 * 159 * @var string 160 */ 161 public $upload_path = ''; 162 163 /** 164 * Overwrite flag 165 * 166 * @var bool 167 */ 168 public $overwrite = FALSE; 169 170 /** 171 * Obfuscate filename flag 172 * 173 * @var bool 174 */ 175 public $encrypt_name = FALSE; 176 177 /** 178 * Is image flag 179 * 180 * @var bool 181 */ 182 public $is_image = FALSE; 183 184 /** 185 * Image width 186 * 187 * @var int 188 */ 189 public $image_width = NULL; 190 191 /** 192 * Image height 193 * 194 * @var int 195 */ 196 public $image_height = NULL; 197 198 /** 199 * Image type 200 * 201 * @var string 202 */ 203 public $image_type = ''; 204 205 /** 206 * Image size string 207 * 208 * @var string 209 */ 210 public $image_size_str = ''; 211 212 /** 213 * Error messages list 214 * 215 * @var array 216 */ 217 public $error_msg = array(); 218 219 /** 220 * Remove spaces flag 221 * 222 * @var bool 223 */ 224 public $remove_spaces = TRUE; 225 226 /** 227 * MIME detection flag 228 * 229 * @var bool 230 */ 231 public $detect_mime = TRUE; 232 233 /** 234 * XSS filter flag 235 * 236 * @var bool 237 */ 238 public $xss_clean = FALSE; 239 240 /** 241 * Apache mod_mime fix flag 242 * 243 * @var bool 244 */ 245 public $mod_mime_fix = TRUE; 246 247 /** 248 * Temporary filename prefix 249 * 250 * @var string 251 */ 252 public $temp_prefix = 'temp_file_'; 253 254 /** 255 * Filename sent by the client 256 * 257 * @var bool 258 */ 259 public $client_name = ''; 260 261 // -------------------------------------------------------------------- 262 263 /** 264 * Filename override 265 * 266 * @var string 267 */ 268 protected $_file_name_override = ''; 269 270 /** 271 * MIME types list 272 * 273 * @var array 274 */ 275 protected $_mimes = array(); 276 277 /** 278 * CI Singleton 279 * 280 * @var object 281 */ 282 protected $_CI; 283 284 // -------------------------------------------------------------------- 285 286 /** 287 * Constructor 288 * 289 * @param array $config 290 * @return void 291 */ 292 public function __construct($config = array()) 293 { 294 empty($config) OR $this->initialize($config, FALSE); 295 296 $this->_mimes =& get_mimes(); 297 $this->_CI =& get_instance(); 298 299 log_message('info', 'Upload Class Initialized'); 300 } 301 302 // -------------------------------------------------------------------- 303 304 /** 305 * Initialize preferences 306 * 307 * @param array $config 308 * @param bool $reset 309 * @return CI_Upload 310 */ 311 public function initialize(array $config = array(), $reset = TRUE) 312 { 313 $reflection = new ReflectionClass($this); 314 315 if ($reset === TRUE) 316 { 317 $defaults = $reflection->getDefaultProperties(); 318 foreach (array_keys($defaults) as $key) 319 { 320 if ($key[0] === '_') 321 { 322 continue; 323 } 324 325 if (isset($config[$key])) 326 { 327 if ($reflection->hasMethod('set_'.$key)) 328 { 329 $this->{'set_'.$key}($config[$key]); 330 } 331 else 332 { 333 $this->$key = $config[$key]; 334 } 335 } 336 else 337 { 338 $this->$key = $defaults[$key]; 339 } 340 } 341 } 342 else 343 { 344 foreach ($config as $key => &$value) 345 { 346 if ($key[0] !== '_' && $reflection->hasProperty($key)) 347 { 348 if ($reflection->hasMethod('set_'.$key)) 349 { 350 $this->{'set_'.$key}($value); 351 } 352 else 353 { 354 $this->$key = $value; 355 } 356 } 357 } 358 } 359 360 // if a file_name was provided in the config, use it instead of the user input 361 // supplied file name for all uploads until initialized again 362 $this->_file_name_override = $this->file_name; 363 return $this; 364 } 365 366 // -------------------------------------------------------------------- 367 368 /** 369 * Perform the file upload 370 * 371 * @param string $field 372 * @return bool 373 */ 374 public function do_upload($field = 'userfile') 375 { 376 // Is $_FILES[$field] set? If not, no reason to continue. 377 if (isset($_FILES[$field])) 378 { 379 $_file = $_FILES[$field]; 380 } 381 // Does the field name contain array notation? 382 elseif (($c = preg_match_all('/(?:^[^\[]+)|\[[^]]*\]/', $field, $matches)) > 1) 383 { 384 $_file = $_FILES; 385 for ($i = 0; $i < $c; $i++) 386 { 387 // We can't track numeric iterations, only full field names are accepted 388 if (($field = trim($matches[0][$i], '[]')) === '' OR ! isset($_file[$field])) 389 { 390 $_file = NULL; 391 break; 392 } 393 394 $_file = $_file[$field]; 395 } 396 } 397 398 if ( ! isset($_file)) 399 { 400 $this->set_error('upload_no_file_selected', 'debug'); 401 return FALSE; 402 } 403 404 // Is the upload path valid? 405 if ( ! $this->validate_upload_path()) 406 { 407 // errors will already be set by validate_upload_path() so just return FALSE 408 return FALSE; 409 } 410 411 // Was the file able to be uploaded? If not, determine the reason why. 412 if ( ! is_uploaded_file($_file['tmp_name'])) 413 { 414 $error = isset($_file['error']) ? $_file['error'] : 4; 415 416 switch ($error) 417 { 418 case UPLOAD_ERR_INI_SIZE: 419 $this->set_error('upload_file_exceeds_limit', 'info'); 420 break; 421 case UPLOAD_ERR_FORM_SIZE: 422 $this->set_error('upload_file_exceeds_form_limit', 'info'); 423 break; 424 case UPLOAD_ERR_PARTIAL: 425 $this->set_error('upload_file_partial', 'debug'); 426 break; 427 case UPLOAD_ERR_NO_FILE: 428 $this->set_error('upload_no_file_selected', 'debug'); 429 break; 430 case UPLOAD_ERR_NO_TMP_DIR: 431 $this->set_error('upload_no_temp_directory', 'error'); 432 break; 433 case UPLOAD_ERR_CANT_WRITE: 434 $this->set_error('upload_unable_to_write_file', 'error'); 435 break; 436 case UPLOAD_ERR_EXTENSION: 437 $this->set_error('upload_stopped_by_extension', 'debug'); 438 break; 439 default: 440 $this->set_error('upload_no_file_selected', 'debug'); 441 break; 442 } 443 444 return FALSE; 445 } 446 447 // Set the uploaded data as class variables 448 $this->file_temp = $_file['tmp_name']; 449 $this->file_size = $_file['size']; 450 451 // Skip MIME type detection? 452 if ($this->detect_mime !== FALSE) 453 { 454 $this->_file_mime_type($_file); 455 } 456 457 $this->file_type = preg_replace('/^(.+?);.*$/', '\\1', $this->file_type); 458 $this->file_type = strtolower(trim(stripslashes($this->file_type), '"')); 459 $this->file_name = $this->_prep_filename($_file['name']); 460 $this->file_ext = $this->get_extension($this->file_name); 461 $this->client_name = $this->file_name; 462 463 // Is the file type allowed to be uploaded? 464 if ( ! $this->is_allowed_filetype()) 465 { 466 $this->set_error('upload_invalid_filetype', 'debug'); 467 return FALSE; 468 } 469 470 // if we're overriding, let's now make sure the new name and type is allowed 471 if ($this->_file_name_override !== '') 472 { 473 $this->file_name = $this->_prep_filename($this->_file_name_override); 474 475 // If no extension was provided in the file_name config item, use the uploaded one 476 if (strpos($this->_file_name_override, '.') === FALSE) 477 { 478 $this->file_name .= $this->file_ext; 479 } 480 else 481 { 482 // An extension was provided, let's have it! 483 $this->file_ext = $this->get_extension($this->_file_name_override); 484 } 485 486 if ( ! $this->is_allowed_filetype(TRUE)) 487 { 488 $this->set_error('upload_invalid_filetype', 'debug'); 489 return FALSE; 490 } 491 } 492 493 // Convert the file size to kilobytes 494 if ($this->file_size > 0) 495 { 496 $this->file_size = round($this->file_size/1024, 2); 497 } 498 499 // Is the file size within the allowed maximum? 500 if ( ! $this->is_allowed_filesize()) 501 { 502 $this->set_error('upload_invalid_filesize', 'info'); 503 return FALSE; 504 } 505 506 // Are the image dimensions within the allowed size? 507 // Note: This can fail if the server has an open_basedir restriction. 508 if ( ! $this->is_allowed_dimensions()) 509 { 510 $this->set_error('upload_invalid_dimensions', 'info'); 511 return FALSE; 512 } 513 514 // Sanitize the file name for security 515 $this->file_name = $this->_CI->security->sanitize_filename($this->file_name); 516 517 // Truncate the file name if it's too long 518 if ($this->max_filename > 0) 519 { 520 $this->file_name = $this->limit_filename_length($this->file_name, $this->max_filename); 521 } 522 523 // Remove white spaces in the name 524 if ($this->remove_spaces === TRUE) 525 { 526 $this->file_name = preg_replace('/\s+/', '_', $this->file_name); 527 } 528 529 if ($this->file_ext_tolower && ($ext_length = strlen($this->file_ext))) 530 { 531 // file_ext was previously lower-cased by a get_extension() call 532 $this->file_name = substr($this->file_name, 0, -$ext_length).$this->file_ext; 533 } 534 535 /* 536 * Validate the file name 537 * This function appends an number onto the end of 538 * the file if one with the same name already exists. 539 * If it returns false there was a problem. 540 */ 541 $this->orig_name = $this->file_name; 542 if (FALSE === ($this->file_name = $this->set_filename($this->upload_path, $this->file_name))) 543 { 544 return FALSE; 545 } 546 547 /* 548 * Run the file through the XSS hacking filter 549 * This helps prevent malicious code from being 550 * embedded within a file. Scripts can easily 551 * be disguised as images or other file types. 552 */ 553 if ($this->xss_clean && $this->do_xss_clean() === FALSE) 554 { 555 $this->set_error('upload_unable_to_write_file', 'error'); 556 return FALSE; 557 } 558 559 /* 560 * Move the file to the final destination 561 * To deal with different server configurations 562 * we'll attempt to use copy() first. If that fails 563 * we'll use move_uploaded_file(). One of the two should 564 * reliably work in most environments 565 */ 566 if ( ! @copy($this->file_temp, $this->upload_path.$this->file_name)) 567 { 568 if ( ! @move_uploaded_file($this->file_temp, $this->upload_path.$this->file_name)) 569 { 570 $this->set_error('upload_destination_error', 'error'); 571 return FALSE; 572 } 573 } 574 575 /* 576 * Set the finalized image dimensions 577 * This sets the image width/height (assuming the 578 * file was an image). We use this information 579 * in the "data" function. 580 */ 581 $this->set_image_properties($this->upload_path.$this->file_name); 582 583 return TRUE; 584 } 585 586 // -------------------------------------------------------------------- 587 588 /** 589 * Finalized Data Array 590 * 591 * Returns an associative array containing all of the information 592 * related to the upload, allowing the developer easy access in one array. 593 * 594 * @param string $index 595 * @return mixed 596 */ 597 public function data($index = NULL) 598 { 599 $data = array( 600 'file_name' => $this->file_name, 601 'file_type' => $this->file_type, 602 'file_path' => $this->upload_path, 603 'full_path' => $this->upload_path.$this->file_name, 604 'raw_name' => substr($this->file_name, 0, -strlen($this->file_ext)), 605 'orig_name' => $this->orig_name, 606 'client_name' => $this->client_name, 607 'file_ext' => $this->file_ext, 608 'file_size' => $this->file_size, 609 'is_image' => $this->is_image(), 610 'image_width' => $this->image_width, 611 'image_height' => $this->image_height, 612 'image_type' => $this->image_type, 613 'image_size_str' => $this->image_size_str, 614 ); 615 616 if ( ! empty($index)) 617 { 618 return isset($data[$index]) ? $data[$index] : NULL; 619 } 620 621 return $data; 622 } 623 624 // -------------------------------------------------------------------- 625 626 /** 627 * Set Upload Path 628 * 629 * @param string $path 630 * @return CI_Upload 631 */ 632 public function set_upload_path($path) 633 { 634 // Make sure it has a trailing slash 635 $this->upload_path = rtrim($path, '/').'/'; 636 return $this; 637 } 638 639 // -------------------------------------------------------------------- 640 641 /** 642 * Set the file name 643 * 644 * This function takes a filename/path as input and looks for the 645 * existence of a file with the same name. If found, it will append a 646 * number to the end of the filename to avoid overwriting a pre-existing file. 647 * 648 * @param string $path 649 * @param string $filename 650 * @return string 651 */ 652 public function set_filename($path, $filename) 653 { 654 if ($this->encrypt_name === TRUE) 655 { 656 $filename = md5(uniqid(mt_rand())).$this->file_ext; 657 } 658 659 if ($this->overwrite === TRUE OR ! file_exists($path.$filename)) 660 { 661 return $filename; 662 } 663 664 $filename = str_replace($this->file_ext, '', $filename); 665 666 $new_filename = ''; 667 for ($i = 1; $i < $this->max_filename_increment; $i++) 668 { 669 if ( ! file_exists($path.$filename.$i.$this->file_ext)) 670 { 671 $new_filename = $filename.$i.$this->file_ext; 672 break; 673 } 674 } 675 676 if ($new_filename === '') 677 { 678 $this->set_error('upload_bad_filename', 'debug'); 679 return FALSE; 680 } 681 682 return $new_filename; 683 } 684 685 // -------------------------------------------------------------------- 686 687 /** 688 * Set Maximum File Size 689 * 690 * @param int $n 691 * @return CI_Upload 692 */ 693 public function set_max_filesize($n) 694 { 695 $this->max_size = ($n < 0) ? 0 : (int) $n; 696 return $this; 697 } 698 699 // -------------------------------------------------------------------- 700 701 /** 702 * Set Maximum File Size 703 * 704 * An internal alias to set_max_filesize() to help with configuration 705 * as initialize() will look for a set_<property_name>() method ... 706 * 707 * @param int $n 708 * @return CI_Upload 709 */ 710 protected function set_max_size($n) 711 { 712 return $this->set_max_filesize($n); 713 } 714 715 // -------------------------------------------------------------------- 716 717 /** 718 * Set Maximum File Name Length 719 * 720 * @param int $n 721 * @return CI_Upload 722 */ 723 public function set_max_filename($n) 724 { 725 $this->max_filename = ($n < 0) ? 0 : (int) $n; 726 return $this; 727 } 728 729 // -------------------------------------------------------------------- 730 731 /** 732 * Set Maximum Image Width 733 * 734 * @param int $n 735 * @return CI_Upload 736 */ 737 public function set_max_width($n) 738 { 739 $this->max_width = ($n < 0) ? 0 : (int) $n; 740 return $this; 741 } 742 743 // -------------------------------------------------------------------- 744 745 /** 746 * Set Maximum Image Height 747 * 748 * @param int $n 749 * @return CI_Upload 750 */ 751 public function set_max_height($n) 752 { 753 $this->max_height = ($n < 0) ? 0 : (int) $n; 754 return $this; 755 } 756 757 // -------------------------------------------------------------------- 758 759 /** 760 * Set minimum image width 761 * 762 * @param int $n 763 * @return CI_Upload 764 */ 765 public function set_min_width($n) 766 { 767 $this->min_width = ($n < 0) ? 0 : (int) $n; 768 return $this; 769 } 770 771 // -------------------------------------------------------------------- 772 773 /** 774 * Set minimum image height 775 * 776 * @param int $n 777 * @return CI_Upload 778 */ 779 public function set_min_height($n) 780 { 781 $this->min_height = ($n < 0) ? 0 : (int) $n; 782 return $this; 783 } 784 785 // -------------------------------------------------------------------- 786 787 /** 788 * Set Allowed File Types 789 * 790 * @param mixed $types 791 * @return CI_Upload 792 */ 793 public function set_allowed_types($types) 794 { 795 $this->allowed_types = (is_array($types) OR $types === '*') 796 ? $types 797 : explode('|', $types); 798 return $this; 799 } 800 801 // -------------------------------------------------------------------- 802 803 /** 804 * Set Image Properties 805 * 806 * Uses GD to determine the width/height/type of image 807 * 808 * @param string $path 809 * @return CI_Upload 810 */ 811 public function set_image_properties($path = '') 812 { 813 if ($this->is_image() && function_exists('getimagesize')) 814 { 815 if (FALSE !== ($D = @getimagesize($path))) 816 { 817 $types = array(1 => 'gif', 2 => 'jpeg', 3 => 'png'); 818 819 $this->image_width = $D[0]; 820 $this->image_height = $D[1]; 821 $this->image_type = isset($types[$D[2]]) ? $types[$D[2]] : 'unknown'; 822 $this->image_size_str = $D[3]; // string containing height and width 823 } 824 } 825 826 return $this; 827 } 828 829 // -------------------------------------------------------------------- 830 831 /** 832 * Set XSS Clean 833 * 834 * Enables the XSS flag so that the file that was uploaded 835 * will be run through the XSS filter. 836 * 837 * @param bool $flag 838 * @return CI_Upload 839 */ 840 public function set_xss_clean($flag = FALSE) 841 { 842 $this->xss_clean = ($flag === TRUE); 843 return $this; 844 } 845 846 // -------------------------------------------------------------------- 847 848 /** 849 * Validate the image 850 * 851 * @return bool 852 */ 853 public function is_image() 854 { 855 // IE will sometimes return odd mime-types during upload, so here we just standardize all 856 // jpegs or pngs to the same file type. 857 858 $png_mimes = array('image/x-png'); 859 $jpeg_mimes = array('image/jpg', 'image/jpe', 'image/jpeg', 'image/pjpeg'); 860 861 if (in_array($this->file_type, $png_mimes)) 862 { 863 $this->file_type = 'image/png'; 864 } 865 elseif (in_array($this->file_type, $jpeg_mimes)) 866 { 867 $this->file_type = 'image/jpeg'; 868 } 869 870 $img_mimes = array('image/gif', 'image/jpeg', 'image/png'); 871 872 return in_array($this->file_type, $img_mimes, TRUE); 873 } 874 875 // -------------------------------------------------------------------- 876 877 /** 878 * Verify that the filetype is allowed 879 * 880 * @param bool $ignore_mime 881 * @return bool 882 */ 883 public function is_allowed_filetype($ignore_mime = FALSE) 884 { 885 if ($this->allowed_types === '*') 886 { 887 return TRUE; 888 } 889 890 if (empty($this->allowed_types) OR ! is_array($this->allowed_types)) 891 { 892 $this->set_error('upload_no_file_types', 'debug'); 893 return FALSE; 894 } 895 896 $ext = strtolower(ltrim($this->file_ext, '.')); 897 898 if ( ! in_array($ext, $this->allowed_types, TRUE)) 899 { 900 return FALSE; 901 } 902 903 // Images get some additional checks 904 if (in_array($ext, array('gif', 'jpg', 'jpeg', 'jpe', 'png'), TRUE) && @getimagesize($this->file_temp) === FALSE) 905 { 906 return FALSE; 907 } 908 909 if ($ignore_mime === TRUE) 910 { 911 return TRUE; 912 } 913 914 if (isset($this->_mimes[$ext])) 915 { 916 return is_array($this->_mimes[$ext]) 917 ? in_array($this->file_type, $this->_mimes[$ext], TRUE) 918 : ($this->_mimes[$ext] === $this->file_type); 919 } 920 921 return FALSE; 922 } 923 924 // -------------------------------------------------------------------- 925 926 /** 927 * Verify that the file is within the allowed size 928 * 929 * @return bool 930 */ 931 public function is_allowed_filesize() 932 { 933 return ($this->max_size === 0 OR $this->max_size > $this->file_size); 934 } 935 936 // -------------------------------------------------------------------- 937 938 /** 939 * Verify that the image is within the allowed width/height 940 * 941 * @return bool 942 */ 943 public function is_allowed_dimensions() 944 { 945 if ( ! $this->is_image()) 946 { 947 return TRUE; 948 } 949 950 if (function_exists('getimagesize')) 951 { 952 $D = @getimagesize($this->file_temp); 953 954 if ($this->max_width > 0 && $D[0] > $this->max_width) 955 { 956 return FALSE; 957 } 958 959 if ($this->max_height > 0 && $D[1] > $this->max_height) 960 { 961 return FALSE; 962 } 963 964 if ($this->min_width > 0 && $D[0] < $this->min_width) 965 { 966 return FALSE; 967 } 968 969 if ($this->min_height > 0 && $D[1] < $this->min_height) 970 { 971 return FALSE; 972 } 973 } 974 975 return TRUE; 976 } 977 978 // -------------------------------------------------------------------- 979 980 /** 981 * Validate Upload Path 982 * 983 * Verifies that it is a valid upload path with proper permissions. 984 * 985 * @return bool 986 */ 987 public function validate_upload_path() 988 { 989 if ($this->upload_path === '') 990 { 991 $this->set_error('upload_no_filepath', 'error'); 992 return FALSE; 993 } 994 995 if (realpath($this->upload_path) !== FALSE) 996 { 997 $this->upload_path = str_replace('\\', '/', realpath($this->upload_path)); 998 } 999 1000 if ( ! is_dir($this->upload_path)) 1001 { 1002 $this->set_error('upload_no_filepath', 'error'); 1003 return FALSE; 1004 } 1005 1006 if ( ! is_really_writable($this->upload_path)) 1007 { 1008 $this->set_error('upload_not_writable', 'error'); 1009 return FALSE; 1010 } 1011 1012 $this->upload_path = preg_replace('/(.+?)\/*$/', '\\1/', $this->upload_path); 1013 return TRUE; 1014 } 1015 1016 // -------------------------------------------------------------------- 1017 1018 /** 1019 * Extract the file extension 1020 * 1021 * @param string $filename 1022 * @return string 1023 */ 1024 public function get_extension($filename) 1025 { 1026 $x = explode('.', $filename); 1027 1028 if (count($x) === 1) 1029 { 1030 return ''; 1031 } 1032 1033 $ext = ($this->file_ext_tolower) ? strtolower(end($x)) : end($x); 1034 return '.'.$ext; 1035 } 1036 1037 // -------------------------------------------------------------------- 1038 1039 /** 1040 * Limit the File Name Length 1041 * 1042 * @param string $filename 1043 * @param int $length 1044 * @return string 1045 */ 1046 public function limit_filename_length($filename, $length) 1047 { 1048 if (strlen($filename) < $length) 1049 { 1050 return $filename; 1051 } 1052 1053 $ext = ''; 1054 if (strpos($filename, '.') !== FALSE) 1055 { 1056 $parts = explode('.', $filename); 1057 $ext = '.'.array_pop($parts); 1058 $filename = implode('.', $parts); 1059 } 1060 1061 return substr($filename, 0, ($length - strlen($ext))).$ext; 1062 } 1063 1064 // -------------------------------------------------------------------- 1065 1066 /** 1067 * Runs the file through the XSS clean function 1068 * 1069 * This prevents people from embedding malicious code in their files. 1070 * I'm not sure that it won't negatively affect certain files in unexpected ways, 1071 * but so far I haven't found that it causes trouble. 1072 * 1073 * @return string 1074 */ 1075 public function do_xss_clean() 1076 { 1077 $file = $this->file_temp; 1078 1079 if (filesize($file) == 0) 1080 { 1081 return FALSE; 1082 } 1083 1084 if (memory_get_usage() && ($memory_limit = ini_get('memory_limit')) > 0) 1085 { 1086 $memory_limit = str_split($memory_limit, strspn($memory_limit, '1234567890')); 1087 if ( ! empty($memory_limit[1])) 1088 { 1089 switch ($memory_limit[1][0]) 1090 { 1091 case 'g': 1092 case 'G': 1093 $memory_limit[0] *= 1024 * 1024 * 1024; 1094 break; 1095 case 'm': 1096 case 'M': 1097 $memory_limit[0] *= 1024 * 1024; 1098 break; 1099 default: 1100 break; 1101 } 1102 } 1103 1104 $memory_limit = (int) ceil(filesize($file) + $memory_limit[0]); 1105 ini_set('memory_limit', $memory_limit); // When an integer is used, the value is measured in bytes. - PHP.net 1106 } 1107 1108 // If the file being uploaded is an image, then we should have no problem with XSS attacks (in theory), but 1109 // IE can be fooled into mime-type detecting a malformed image as an html file, thus executing an XSS attack on anyone 1110 // using IE who looks at the image. It does this by inspecting the first 255 bytes of an image. To get around this 1111 // CI will itself look at the first 255 bytes of an image to determine its relative safety. This can save a lot of 1112 // processor power and time if it is actually a clean image, as it will be in nearly all instances _except_ an 1113 // attempted XSS attack. 1114 1115 if (function_exists('getimagesize') && @getimagesize($file) !== FALSE) 1116 { 1117 if (($file = @fopen($file, 'rb')) === FALSE) // "b" to force binary 1118 { 1119 return FALSE; // Couldn't open the file, return FALSE 1120 } 1121 1122 $opening_bytes = fread($file, 256); 1123 fclose($file); 1124 1125 // These are known to throw IE into mime-type detection chaos 1126 // <a, <body, <head, <html, <img, <plaintext, <pre, <script, <table, <title 1127 // title is basically just in SVG, but we filter it anyhow 1128 1129 // if it's an image or no "triggers" detected in the first 256 bytes - we're good 1130 return ! preg_match('/<(a|body|head|html|img|plaintext|pre|script|table|title)[\s>]/i', $opening_bytes); 1131 } 1132 1133 if (($data = @file_get_contents($file)) === FALSE) 1134 { 1135 return FALSE; 1136 } 1137 1138 return $this->_CI->security->xss_clean($data, TRUE); 1139 } 1140 1141 // -------------------------------------------------------------------- 1142 1143 /** 1144 * Set an error message 1145 * 1146 * @param string $msg 1147 * @return CI_Upload 1148 */ 1149 public function set_error($msg, $log_level = 'error') 1150 { 1151 $this->_CI->lang->load('upload'); 1152 1153 is_array($msg) OR $msg = array($msg); 1154 foreach ($msg as $val) 1155 { 1156 $msg = ($this->_CI->lang->line($val) === FALSE) ? $val : $this->_CI->lang->line($val); 1157 $this->error_msg[] = $msg; 1158 log_message($log_level, $msg); 1159 } 1160 1161 return $this; 1162 } 1163 1164 // -------------------------------------------------------------------- 1165 1166 /** 1167 * Display the error message 1168 * 1169 * @param string $open 1170 * @param string $close 1171 * @return string 1172 */ 1173 public function display_errors($open = '<p>', $close = '</p>') 1174 { 1175 return (count($this->error_msg) > 0) ? $open.implode($close.$open, $this->error_msg).$close : ''; 1176 } 1177 1178 // -------------------------------------------------------------------- 1179 1180 /** 1181 * Prep Filename 1182 * 1183 * Prevents possible script execution from Apache's handling 1184 * of files' multiple extensions. 1185 * 1186 * @link http://httpd.apache.org/docs/1.3/mod/mod_mime.html#multipleext 1187 * 1188 * @param string $filename 1189 * @return string 1190 */ 1191 protected function _prep_filename($filename) 1192 { 1193 if ($this->mod_mime_fix === FALSE OR $this->allowed_types === '*' OR ($ext_pos = strrpos($filename, '.')) === FALSE) 1194 { 1195 return $filename; 1196 } 1197 1198 $ext = substr($filename, $ext_pos); 1199 $filename = substr($filename, 0, $ext_pos); 1200 return str_replace('.', '_', $filename).$ext; 1201 } 1202 1203 // -------------------------------------------------------------------- 1204 1205 /** 1206 * File MIME type 1207 * 1208 * Detects the (actual) MIME type of the uploaded file, if possible. 1209 * The input array is expected to be $_FILES[$field] 1210 * 1211 * @param array $file 1212 * @return void 1213 */ 1214 protected function _file_mime_type($file) 1215 { 1216 // We'll need this to validate the MIME info string (e.g. text/plain; charset=us-ascii) 1217 $regexp = '/^([a-z\-]+\/[a-z0-9\-\.\+]+)(;\s.+)?$/'; 1218 1219 /** 1220 * Fileinfo extension - most reliable method 1221 * 1222 * Apparently XAMPP, CentOS, cPanel and who knows what 1223 * other PHP distribution channels EXPLICITLY DISABLE 1224 * ext/fileinfo, which is otherwise enabled by default 1225 * since PHP 5.3 ... 1226 */ 1227 if (function_exists('finfo_file')) 1228 { 1229 $finfo = @finfo_open(FILEINFO_MIME); 1230 if (is_resource($finfo)) // It is possible that a FALSE value is returned, if there is no magic MIME database file found on the system 1231 { 1232 $mime = @finfo_file($finfo, $file['tmp_name']); 1233 finfo_close($finfo); 1234 1235 /* According to the comments section of the PHP manual page, 1236 * it is possible that this function returns an empty string 1237 * for some files (e.g. if they don't exist in the magic MIME database) 1238 */ 1239 if (is_string($mime) && preg_match($regexp, $mime, $matches)) 1240 { 1241 $this->file_type = $matches[1]; 1242 return; 1243 } 1244 } 1245 } 1246 1247 /* This is an ugly hack, but UNIX-type systems provide a "native" way to detect the file type, 1248 * which is still more secure than depending on the value of $_FILES[$field]['type'], and as it 1249 * was reported in issue #750 (https://github.com/EllisLab/CodeIgniter/issues/750) - it's better 1250 * than mime_content_type() as well, hence the attempts to try calling the command line with 1251 * three different functions. 1252 * 1253 * Notes: 1254 * - the DIRECTORY_SEPARATOR comparison ensures that we're not on a Windows system 1255 * - many system admins would disable the exec(), shell_exec(), popen() and similar functions 1256 * due to security concerns, hence the function_usable() checks 1257 */ 1258 if (DIRECTORY_SEPARATOR !== '\\') 1259 { 1260 $cmd = function_exists('escapeshellarg') 1261 ? 'file --brief --mime '.escapeshellarg($file['tmp_name']).' 2>&1' 1262 : 'file --brief --mime '.$file['tmp_name'].' 2>&1'; 1263 1264 if (function_usable('exec')) 1265 { 1266 /* This might look confusing, as $mime is being populated with all of the output when set in the second parameter. 1267 * However, we only need the last line, which is the actual return value of exec(), and as such - it overwrites 1268 * anything that could already be set for $mime previously. This effectively makes the second parameter a dummy 1269 * value, which is only put to allow us to get the return status code. 1270 */ 1271 $mime = @exec($cmd, $mime, $return_status); 1272 if ($return_status === 0 && is_string($mime) && preg_match($regexp, $mime, $matches)) 1273 { 1274 $this->file_type = $matches[1]; 1275 return; 1276 } 1277 } 1278 1279 if ( ! ini_get('safe_mode') && function_usable('shell_exec')) 1280 { 1281 $mime = @shell_exec($cmd); 1282 if (strlen($mime) > 0) 1283 { 1284 $mime = explode("\n", trim($mime)); 1285 if (preg_match($regexp, $mime[(count($mime) - 1)], $matches)) 1286 { 1287 $this->file_type = $matches[1]; 1288 return; 1289 } 1290 } 1291 } 1292 1293 if (function_usable('popen')) 1294 { 1295 $proc = @popen($cmd, 'r'); 1296 if (is_resource($proc)) 1297 { 1298 $mime = @fread($proc, 512); 1299 @pclose($proc); 1300 if ($mime !== FALSE) 1301 { 1302 $mime = explode("\n", trim($mime)); 1303 if (preg_match($regexp, $mime[(count($mime) - 1)], $matches)) 1304 { 1305 $this->file_type = $matches[1]; 1306 return; 1307 } 1308 } 1309 } 1310 } 1311 } 1312 1313 // Fall back to mime_content_type(), if available (still better than $_FILES[$field]['type']) 1314 if (function_exists('mime_content_type')) 1315 { 1316 $this->file_type = @mime_content_type($file['tmp_name']); 1317 if (strlen($this->file_type) > 0) // It's possible that mime_content_type() returns FALSE or an empty string 1318 { 1319 return; 1320 } 1321 } 1322 1323 $this->file_type = $file['type']; 1324 } 1325 1326} 1327