<?php

/**
 * This source file is part of the open source project
 * ExpressionEngine (https://expressionengine.com)
 *
 * @link      https://expressionengine.com/
 * @copyright Copyright (c) 2003-2023, Packet Tide, LLC (https://www.packettide.com)
 * @license   https://expressionengine.com/license Licensed under Apache License, Version 2.0
 */

use ExpressionEngine\Dependency\League\Flysystem\Adapter\Local;

/**
 * Core Upload
 */
class EE_Upload
{
    public $max_size = 0;
    public $max_width = 0;
    public $max_height = 0;
    public $max_filename = 0;
    public $allowed_types = "";
    public $file_temp = "";
    public $file_name = "";
    public $orig_name = "";
    public $file_type = "";
    public $file_size = "";
    public $file_ext = "";
    public $upload_path = "";
    public $upload_destination = null;
    public $overwrite = false;
    public $encrypt_name = false;
    public $is_image = false;
    public $image_width = '';
    public $image_height = '';
    public $image_type = '';
    public $image_size_str = '';
    public $error_msg = array();
    public $mimes = array();
    public $remove_spaces = true;
    public $xss_clean = false;
    public $temp_prefix = "temp_file_";
    public $client_name = '';
    public $auto_resize = false;
    public $image_processed = false;

    protected $use_temp_dir = false;
    protected $raw_upload = false;
    protected $_file_name_override = '';
    protected $blocked_extensions = array();

    /**
     * Constructor
     */
    public function __construct($props = array())
    {
        if (count($props) > 0) {
            $this->initialize($props);
        }

        ee()->load->helper('xss');
        ee()->load->helper('multibyte');

        $props['xss_clean'] = xss_check();

        $this->initialize($props);

        ee()->load->library('mime_type');
        log_message('debug', "Upload Class Initialized");

        $this->blocked_extensions = array(
            'php',
            'php3',
            'php4',
            'php5',
            'php7',
            'phps',
            'phtml',
            'phar'
        );
    }

    /**
     * Take raw file data and populate our tmp directory and FILES array and
     * then pass it through the normal do_upload routine.
     *
     * @access  public
     * @param string $name The file name
     * @param string $type The mime type
     * @param string $data The raw file data
     * @return mixed The result of do_upload
     */
    public function raw_upload($name, $data)
    {
        // This will force do_upload to skip its is_uploaded_file checks
        $this->raw_upload = true;

        $tmp = tempnam(sys_get_temp_dir(), 'raw');

        if (file_put_contents($tmp, $data) === false) {
            throw new Exception('Could not upload file');
        }

        $_FILES['userfile'] = array(
            'name' => $name,
            'size' => ee_mb_strlen($data),
            'tmp_name' => $tmp,
            'error' => UPLOAD_ERR_OK
        );

        return $this->do_upload();
    }

    /**
     * Perform the file upload
     *
     * @return  bool
     */
    public function do_upload($field = 'userfile')
    {
        // Is $_FILES[$field] set? If not, no reason to continue.
        if (! isset($_FILES[$field])) {
            $this->set_error('upload_no_file_selected');

            return false;
        }

        // Is the upload path valid?
        if (! $this->validate_upload_path()) {
            // errors will already be set by validate_upload_path() so just return FALSE
            return false;
        }

        // Was the file able to be uploaded? If not, determine the reason why.
        if (! $this->raw_upload && ! is_uploaded_file($_FILES[$field]['tmp_name'])) {
            $error = (! isset($_FILES[$field]['error'])) ? 4 : $_FILES[$field]['error'];

            switch ($error) {
                case 1: // UPLOAD_ERR_INI_SIZE
                    $this->set_error('upload_file_exceeds_limit');

                    break;
                case 2: // UPLOAD_ERR_FORM_SIZE
                    $this->set_error('upload_file_exceeds_form_limit');

                    break;
                case 3: // UPLOAD_ERR_PARTIAL
                    $this->set_error('upload_file_partial');

                    break;
                case 4: // UPLOAD_ERR_NO_FILE
                    $this->set_error('upload_no_file_selected');

                    break;
                case 6: // UPLOAD_ERR_NO_TMP_DIR
                    $this->set_error('upload_no_temp_directory');

                    break;
                case 7: // UPLOAD_ERR_CANT_WRITE
                    $this->set_error('upload_unable_to_write_file');

                    break;
                case 8: // UPLOAD_ERR_EXTENSION
                    $this->set_error('upload_stopped_by_extension');

                    break;
                default:
                    $this->set_error('upload_no_file_selected');

                    break;
            }

            return false;
        }

        // Set the uploaded data as class variables
        $this->file_temp = $_FILES[$field]['tmp_name'];
        $this->file_size = $_FILES[$field]['size'];
        $this->file_type = ee('MimeType')->ofFile($this->file_temp);
        $this->file_name = $this->_prep_filename($_FILES[$field]['name']);
        $this->file_ext = $this->get_extension($this->file_name);
        $this->client_name = $this->file_name;

        // Is this a hidden file? Not allowed
        if (strncmp($this->file_name, '.', 1) == 0) {
            $this->set_error('upload_invalid_file');

            return false;
        }

        // Disallowed File Names
        $disallowed_names = ee()->config->item('upload_blocked_file_names') ?: ee()->config->item('upload_file_name_blacklist');

        if ($disallowed_names !== false) {
            if (! is_array($disallowed_names)) {
                $disallowed_names = array($disallowed_names);
            }
            $disallowed_names = array_map("strtolower", $disallowed_names);
        } else {
            $disallowed_names = array();
        }

        // Yes ".htaccess" is covered by the above hidden file check
        // but this is here as an extra sanity-saving precation.
        $disallowed_names[] = '.htaccess';
        $disallowed_names[] = 'web.config';

        if (in_array(strtolower($this->file_name), $disallowed_names)) {
            $this->set_error('upload_invalid_file');

            return false;
        }

        // Sanitize the file name for security
        $this->file_name = $this->clean_file_name($this->file_name);

        // Is the file type allowed to be uploaded?
        if (! $this->is_allowed_filetype()) {
            $this->set_error('upload_invalid_file');

            return false;
        }

        // if we're overriding, let's now make sure the new name and type is allowed
        if ($this->_file_name_override != '') {
            $this->file_name = $this->_prep_filename($this->_file_name_override);
            $this->file_ext = $this->get_extension($this->file_name);

            if (! $this->is_allowed_filetype(true)) {
                $this->set_error('upload_invalid_file');

                return false;
            }
        }

        // Are the image dimensions within the allowed size?
        // Note: This can fail if the server has an open_basedir restriction.
        if (! $this->is_allowed_dimensions()) {
            //try to resize, if configured
            if ($this->auto_resize) {
                ee()->load->library('filemanager');
                $file_data = [
                    'max_width' => $this->max_width,
                    'max_height' => $this->max_height,
                    'width' => $this->image_width,
                    'height' => $this->image_height,
                    'filesystem' => null,
                ];

                $orientation = ee()->filemanager->orientation_check($this->file_temp, $file_data);

                if (!empty($orientation)) {
                    $file_data = $orientation;
                }

                $auto_resize = ee()->filemanager->max_hw_check($this->file_temp, $file_data);

                if ($auto_resize === false) {
                    $this->set_error('upload_invalid_dimensions');
                    return false;
                }
                $this->file_size = filesize($this->file_temp);
            } else {
                $this->set_error('upload_invalid_dimensions');
                return false;
            }
            $this->image_processed = true;
        }

        // Convert the file size to kilobytes
        if ($this->file_size > 0) {
            $this->file_size = round($this->file_size / 1024, 2);
        }

        // Is the file size within the allowed maximum?
        if (! $this->is_allowed_filesize()) {
            ee()->lang->load('upload');
            $this->set_error(sprintf(lang('upload_invalid_filesize'), $this->max_size));

            return false;
        }

        // Remove invisible control characters
        $this->file_name = preg_replace('#\\p{C}+#u', '', $this->file_name);

        // Truncate the file name if it's too long
        if ($this->max_filename > 0) {
            $this->file_name = $this->limit_filename_length($this->file_name, $this->max_filename);
        }

        // Remove white spaces in the name
        if ($this->remove_spaces == true) {
            $this->file_name = preg_replace("/\s+/", "_", $this->file_name);
        }

        /*
         * Validate the file name
         * This function appends an number onto the end of
         * the file if one with the same name already exists.
         * If it returns false there was a problem.
         */
        $this->orig_name = $this->file_name;

        if ($this->overwrite == false) {
            $this->file_name = $this->set_filename($this->upload_path, $this->file_name, $this->upload_destination);

            if ($this->file_name === false) {
                return false;
            }
        }

        /*
         * Run the file through the XSS hacking filter
         * This helps prevent malicious code from being
         * embedded within a file.  Scripts can easily
         * be disguised as images or other file types.
         */
        if ($this->xss_clean) {
            if ($this->do_xss_clean() === false) {
                $this->set_error('upload_unable_to_write_file');

                return false;
            }
        }

        // If this is an image make sure it doesn't have PHP embedded in it
        if ($this->is_image) {
            if ($this->do_embedded_php_check() === false) {
                $this->set_error('upload_unable_to_write_file');

                return false;
            }
        }

        /*
         * Set the finalized image dimensions
         * This sets the image width/height (assuming the file was an image).
         * We use this information in the "data" function.
         */
        $this->set_image_properties($this->file_temp);

        /*
         * Move the file to the final destination
         * To deal with different server configurations
         * we'll attempt to use copy() first.  If that fails
         * we'll use move_uploaded_file().  One of the two should
         * reliably work in most environments
         */
        $result = true;

        if ($this->upload_destination) {
            $stream = fopen($this->file_temp, 'r+');
            $result = $this->upload_destination->getFilesystem()->writeStream($this->upload_path . $this->file_name, $stream);
        } elseif (! @copy($this->file_temp, $this->upload_path . $this->file_name)) {
            if (! @move_uploaded_file($this->file_temp, $this->upload_path . $this->file_name)) {
                $this->set_error('upload_destination_error');

                return false;
            }
        }

        if (!$this->upload_destination || $this->upload_destination->getFilesystem()->isLocal()) {
            @chmod($this->upload_path . $this->file_name, FILE_WRITE_MODE);
        }

        return $result;
    }

    /**
     * Finalized Data Array
     *
     * Returns an associative array containing all of the information
     * related to the upload, allowing the developer easy access in one array.
     *
     * @return  array
     */
    public function data()
    {
        return array(
            'file_name' => $this->file_name,
            'file_type' => $this->file_type,
            'file_temp' => $this->file_temp,
            'file_path' => $this->upload_path,
            'full_path' => $this->upload_path . $this->file_name,
            'raw_name' => str_replace($this->file_ext, '', $this->file_name),
            'orig_name' => $this->orig_name,
            'client_name' => $this->client_name,
            'file_ext' => $this->file_ext,
            'file_size' => $this->file_size,
            'is_image' => $this->is_image(),
            'image_width' => $this->image_width,
            'image_height' => $this->image_height,
            'image_type' => $this->image_type,
            'image_size_str' => $this->image_size_str,
            'image_processed' => $this->image_processed,
        );
    }

    /**
     * Set Upload Path
     *
     * @param   string
     * @return  void
     */
    public function set_upload_path($path)
    {
        // Make sure it has a trailing slash
        $this->upload_path = (empty($path)) ? $path : rtrim($path, '/') . '/';
    }

    /**
     * Set Upload Destination
     *
     * @param   string
     * @return  void
     */
    public function set_upload_destination($destination = null)
    {
        $this->upload_destination = $destination;
    }

    /**
     * Set the file name
     *
     * This function takes a filename/path as input and looks for the
     * existence of a file with the same name. If found, it will append a
     * number to the end of the filename to avoid overwriting a pre-existing file.
     *
     * @param   string
     * @param   string
     * @return  string
     */
    public function set_filename($path, $filename, $upload_destination = null)
    {
        if ($this->encrypt_name == true) {
            mt_srand();
            $filename = md5(uniqid(mt_rand())) . $this->file_ext;
        }

        $filesystem = ($upload_destination) ? $upload_destination->getFilesystem() : ee('Filesystem');

        if (! $filesystem->exists($path . $filename)) {
            return $filename;
        }

        $new_filename = $filesystem->getUniqueFilename($path . $filename);
        $new_filename = str_replace($path, '', $new_filename);

        if ($new_filename == '') {
            $this->set_error('upload_bad_filename');

            return false;
        } else {
            return $new_filename;
        }
    }

    /**
     * Set Maximum File Size
     *
     * @param   integer
     * @return  void
     */
    public function set_max_filesize($n)
    {
        $this->max_size = ((int) $n < 0) ? 0 : (int) $n;
    }

    /**
     * Set Maximum File Name Length
     *
     * @param   integer
     * @return  void
     */
    public function set_max_filename($n)
    {
        $this->max_filename = ((int) $n < 0) ? 0 : (int) $n;
    }

    /**
     * Set Maximum Image Width
     *
     * @param   integer
     * @return  void
     */
    public function set_max_width($n)
    {
        $this->max_width = ((int) $n < 0) ? 0 : (int) $n;
    }

    /**
     * Set Maximum Image Height
     *
     * @param   integer
     * @return  void
     */
    public function set_max_height($n)
    {
        $this->max_height = ((int) $n < 0) ? 0 : (int) $n;
    }

    /**
     * Set Allowed File Types
     *
     * @param   string
     * @return  void
     */
    public function set_allowed_types($types)
    {
        if (! is_array($types) && $types == '*') {
            $this->allowed_types = '*';

            return;
        }
        $this->allowed_types = explode('|', $types);
    }

    /**
     * Set Image Properties
     *
     * Uses GD to determine the width/height/type of image
     *
     * @param   string
     * @return  void
     */
    public function set_image_properties($path = '')
    {
        if (! $this->is_image()) {
            return;
        }

        if (function_exists('getimagesize')) {
            if (false !== ($D = @getimagesize($path))) {
                $types = array(1 => 'gif', 2 => 'jpeg', 3 => 'png');

                $this->image_width = $D['0'];
                $this->image_height = $D['1'];
                $this->image_type = (! isset($types[$D['2']])) ? 'unknown' : $types[$D['2']];
                $this->image_size_str = $D['3'];  // string containing height and width
            }
        }
    }

    /**
     * Set XSS Clean
     *
     * Enables the XSS flag so that the file that was uploaded
     * will be run through the XSS filter.
     *
     * @param   bool
     * @return  void
     */
    public function set_xss_clean($flag = false)
    {
        $this->xss_clean = ($flag == true) ? true : false;
    }

    /**
     * Validate the image
     *
     * @return  bool
     */
    public function is_image()
    {
        return ee('MimeType')->fileIsImage($this->file_temp);
    }

    /**
     * Verify that the filetype is allowed
     *
     * @return  bool
     */
    public function is_allowed_filetype($ignore_mime = false)
    {
        $ext = strtolower(ltrim($this->file_ext, '.'));

        if (in_array($ext, $this->blocked_extensions)) {
            return false;
        }

        if (! empty($this->allowed_types) && is_array($this->allowed_types) && ! in_array($ext, $this->allowed_types)) {
            return false;
        }

        if ($ignore_mime === true) {
            return true;
        }

        if ($this->is_image) {
            return ee('MimeType')->fileIsImage($this->file_temp);
        }

        return ee('MimeType')->fileIsSafeForUpload($this->file_temp);
    }

    /**
     * Verify that the file is within the allowed size
     *
     * @return  bool
     */
    public function is_allowed_filesize()
    {
        if ($this->max_size != 0 && $this->file_size > $this->max_size) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * Verify that the image is within the allowed width/height
     *
     * @return  bool
     */
    public function is_allowed_dimensions()
    {
        if (! $this->is_image()) {
            return true;
        }

        if (function_exists('getimagesize') && $D = @getimagesize($this->file_temp)) {
            $this->image_width = $D['0'];
            $this->image_height = $D['1'];

            if ($this->max_width > 0 && $D['0'] > $this->max_width) {
                return false;
            }

            if ($this->max_height > 0 && $D['1'] > $this->max_height) {
                return false;
            }

            return true;
        }

        return true;
    }

    /**
     * Extract the file extension
     *
     * @param   string
     * @return  string
     */
    public function get_extension($filename)
    {
        $x = explode('.', $filename);

        return '.' . end($x);
    }

    /**
     * Clean the file name for security
     *
     * @param   string
     * @return  string
     */
    public function clean_file_name($filename)
    {
        $bad = array(
            "<!--",
            "-->",
            "'",
            "<",
            ">",
            '"',
            '&',
            '$',
            '=',
            ';',
            '?',
            '/',
            "%20",
            "%22",
            "%3c",      // <
            "%253c",    // <
            "%3e",      // >
            "%0e",      // >
            "%28",      // (
            "%29",      // )
            "%2528",    // (
            "%26",      // &
            "%24",      // $
            "%3f",      // ?
            "%3b",      // ;
            "%3d"       // =
        );

        $filename = str_replace($bad, '_', $filename);

        return stripslashes($filename);
    }

    /**
     * Limit the File Name Length
     *
     * @param   string
     * @return  string
     */
    public function limit_filename_length($filename, $length)
    {
        if (strlen($filename) < $length) {
            return $filename;
        }

        $ext = '';
        if (strpos($filename, '.') !== false) {
            $parts = explode('.', $filename);
            $ext = '.' . array_pop($parts);
            $filename = implode('.', $parts);
        }

        return substr($filename, 0, ($length - strlen($ext))) . $ext;
    }

    /**
     * Runs the file through the XSS clean function
     *
     * This prevents people from embedding malicious code in their files.
     * I'm not sure that it won't negatively affect certain files in unexpected ways,
     * but so far I haven't found that it causes trouble.
     *
     * @return  void
     */
    public function do_xss_clean()
    {
        $file = $this->file_temp;

        if (filesize($file) == 0) {
            return false;
        }

        $this->increase_memory_limit(filesize($file));

        // If the file being uploaded is an image, then we should have no
        // problem with XSS attacks (in theory), but IE can be fooled into mime-
        // type detecting a malformed image as an html file, thus executing an
        // XSS attack on anyone using IE who looks at the image.  It does this
        // by inspecting the first 255 bytes of an image.  To get around this CI
        // will itself look at the first 255 bytes of an image to determine its
        // relative safety.  This can save a lot of processor power and time if
        // it is actually a clean image, as it will be in nearly all instances
        // _except_ an attempted XSS attack.

        if (function_exists('getimagesize') && ($image = getimagesize($file)) !== false) {
            if (ee('MimeType')->fileIsSafeForUpload($file) === false) {
                return false; // tricky tricky
            }

            if (($file = @fopen($file, 'rb')) === false) { // "b" to force binary
                return false; // Couldn't open the file, return FALSE
            }

            $opening_bytes = fread($file, 256);
            fclose($file);

            // These are known to throw IE into mime-type detection chaos <a,
            // <body, <head, <html, <img, <plaintext, <pre, <script, <table,
            // <title
            // title is basically just in SVG, but we filter it anyhow

            if (! preg_match('/<(a|body|head|html|img|plaintext|pre|script|table|title)[\s>]/i', $opening_bytes)) {
                return true; // its an image, no "triggers" detected in the first 256 bytes, we're good
            } else {
                return false;
            }
        }

        if (($data = @file_get_contents($file)) === false) {
            return false;
        }

        $checkAsImage = true;
        if (strpos($this->file_type, 'image/svg') === 0) {
            $checkAsImage = false;

            // if it's an SVG, we need to check for XSS in the SVG itself
            if ($data !== ee('Security/XSS')->clean($data)) {
                return false;
            }
        }

        return ee('Security/XSS')->clean($data, $checkAsImage);
    }

    public function do_embedded_php_check()
    {
        $file = $this->file_temp;

        if (filesize($file) == 0) {
            return false;
        }

        $this->increase_memory_limit(filesize($file));

        if (($data = @file_get_contents($file)) === false) {
            return false;
        }

        // We can't simply check for `<?` because that's valid XML and is
        // allowed in files.
        return (stripos($data, '<?php') === false);
    }

    /**
     * Set an error message
     *
     * @param   string
     * @return  void
     */
    public function set_error($msg)
    {
        ee()->lang->load('upload');

        if (is_array($msg)) {
            foreach ($msg as $val) {
                $msg = (ee()->lang->line($val) == false) ? $val : ee()->lang->line($val);
                $this->error_msg[] = $msg;
                log_message('error', $msg);
            }
        } else {
            $msg = (ee()->lang->line($msg) == false) ? $msg : ee()->lang->line($msg);
            $this->error_msg[] = $msg;
            log_message('error', $msg);
        }
    }

    /**
     * Display the error message
     *
     * @param   string
     * @param   string
     * @return  string
     */
    public function display_errors($open = '<p>', $close = '</p>')
    {
        $str = '';
        foreach ($this->error_msg as $val) {
            $str .= $open . $val . $close;
        }

        return $str;
    }

    /**
     * List of Mime Types
     *
     * This is a list of mime types.  We use it to validate
     * the "allowed types" set by the developer
     *
     * @param   string
     * @return  string
     */
    public function mimes_types($mime)
    {
        return ee('MimeType')->isSafeForUpload($mime);
    }

    /**
     * Overwrite OR Rename Files Manually
     *
     * @access  public
     * @param string $original_files Path to the original file
     * @param string $new The new file name
     * @param boolean $type_match Should we make sure the extensions match?
     * @return boolean TRUE if it was renamed properly, FALSE otherwise
     */
    public function file_overwrite($original_file = '', $new = '', $type_match = true)
    {
        $this->file_name = $new;

        // If renaming a file, it should have same file type suffix as the original
        if ($type_match === true) {
            $filename_parts = explode('.', $this->file_name);
            $original_parts = explode('.', $original_file);

            if (sizeof($filename_parts) == 1 || (array_pop($filename_parts) != array_pop($original_parts))) {
                $this->set_error('invalid_filetype');

                return false;
            }
        }

        if ($this->remove_spaces == 1) {
            $this->file_name = preg_replace("/\s+/", "_", $this->file_name);
            $original_file = preg_replace("/\s+/", "_", $original_file);
        }

        // Check to make sure the file doesn't already exist
        if (file_exists($this->upload_path . $this->file_name)) {
            $this->set_error('file_exists');

            return false;
        }

        if (! @copy($this->upload_path . $original_file, $this->upload_path . $this->file_name)) {
            $this->set_error('copy_error');

            return false;
        }

        unlink($this->upload_path . $original_file);

        return true;
    }

    /**
     * Validate Upload Path
     *
     * Verifies that it is a valid upload path with proper permissions.
     *
     * @access  public
     */
    public function validate_upload_path()
    {
        if ($this->use_temp_dir) {
            $path = $this->_discover_temp_path();

            if ($path) {
                $this->upload_path = $path;
            } else {
                $this->set_error('No usable temp directory found.');

                return false;
            }
        }

        $fs = (!empty($this->upload_destination)) ? $this->upload_destination->getFilesystem() : ee('Filesystem');

        if ($fs->isLocal()) {
            if ($this->upload_path == '') {
                $this->set_error('upload_no_filepath');

                return false;
            }

            // If realpath() is available and expands the upload_path into a
            // path that exists within the filesystem we'll use that instead
            if (function_exists('realpath') && @realpath($this->upload_path) !== false && $fs->exists(realpath($this->upload_path))) {
                $this->upload_path = str_replace("\\", "/", realpath($this->upload_path));
            }
        }

        // if (! $this->upload_destination->getFilesystem()->isDir()) {
        //     $this->set_error('upload_no_filepath');

        //     return false;
        // }

        if (! $fs->isWritable($this->upload_path)) {
            $this->set_error('upload_not_writable');

            return false;
        }

        $this->upload_path = preg_replace("/(.+?)\/*$/", "\\1/", $this->upload_path);

        return true;
    }

    /**
     * Keep the file in the temp directory?
     *
     * @access  public
     */
    public function initialize($config = array())
    {
        if (isset($config['use_temp_dir']) && $config['use_temp_dir'] === true) {
            $this->use_temp_dir = true;
        } else {
            $this->use_temp_dir = false;
        }

        $defaults = array(
            'max_size' => 0,
            'max_width' => 0,
            'max_height' => 0,
            'max_filename' => 0,
            'allowed_types' => "",
            'file_temp' => "",
            'file_name' => "",
            'orig_name' => "",
            'file_type' => "",
            'file_size' => "",
            'file_ext' => "",
            'upload_path' => "",
            'upload_destination' => null,
            'overwrite' => false,
            'encrypt_name' => false,
            'is_image' => false,
            'image_width' => '',
            'image_height' => '',
            'image_type' => '',
            'image_size_str' => '',
            'error_msg' => array(),
            'mimes' => array(),
            'remove_spaces' => true,
            'xss_clean' => false,
            'temp_prefix' => "temp_file_",
            'client_name' => '',
            'auto_resize' => false
        );

        foreach ($defaults as $key => $val) {
            if (isset($config[$key])) {
                $method = 'set_' . $key;
                if (method_exists($this, $method)) {
                    $this->$method($config[$key]);
                } else {
                    $this->$key = $config[$key];
                }
            } else {
                $this->$key = $val;
            }
        }

        // if a file_name was provided in the config, use it instead of the user input
        // supplied file name for all uploads until initialized again
        $this->_file_name_override = $this->file_name;
    }

    /**
     * Find a valid temp directory?
     *
     * @access  public
     */
    public function _discover_temp_path()
    {
        $attempt = array();
        $ini_path = ini_get('upload_tmp_dir');

        if ($ini_path) {
            $attempt[] = realpath($ini_path);
        }

        $attempt[] = sys_get_temp_dir();
        $attempt[] = @getenv('TMP');
        $attempt[] = @getenv('TMPDIR');
        $attempt[] = @getenv('TEMP');

        $valid_temps = array_filter($attempt);  // remove false's

        foreach ($valid_temps as $dir) {
            if (is_readable($dir) && is_writable($dir)) {
                return $dir;
            }
        }

        return false;
    }

    /**
     * If possible, will increase PHP's memory limit by the specified number of
     * bytes
     *
     * @param int $size The number of bytes in increase by
     * @return void
     */
    protected function increase_memory_limit($size)
    {
        $current = ee('Memory')->getMemoryLimitBytes();

        // There was a bug/behavioural change in PHP 5.2, where numbers over
        // one million get output into scientific notation.  number_format()
        // ensures this number is an integer
        // http://bugs.php.net/bug.php?id=43053

        $new_memory = number_format(ceil($size + $current), 0, '.', '');

        // When an integer is used, the value is measured in bytes.
        ini_set('memory_limit', $new_memory);
    }

    /**
     * Prep Filename
     *
     * Prevents possible script execution from Apache's handling of files multiple extensions
     * http://httpd.apache.org/docs/1.3/mod/mod_mime.html#multipleext
     *
     * @param   string
     * @return  string
     */
    protected function _prep_filename($filename)
    {
        if (strpos($filename, '.') === false || $this->allowed_types == '*') {
            return $filename;
        }

        $parts = explode('.', $filename);
        $ext = array_pop($parts);
        $filename = array_shift($parts);

        foreach ($parts as $part) {
            $filename .= '.' . $part;

            if (! $this->allowedType($part)) {
                $filename .= '_';
            }
        }

        $filename .= '.' . $ext;

        return $filename;
    }

    /**
     * Checks if the file's mime_type is allowed.
     *
     * @param string $extension
     * @return bool
     */
    private function allowedType($extension)
    {
        // numbers by themselves are safe, e.g. file_3.2.5.txt, as are concurrent...dots
        if (ctype_digit($extension) || $extension == '') {
            return true;
        }

        $by_legacy = false;
        $extension = strtolower($extension);

        if (is_array($this->allowed_types)) {
            $by_legacy = in_array($extension, $this->allowed_types);
        }

        return ($by_legacy || $this->mimes_types($extension));
    }
}
// END CLASS

// EOF
