e107inc/e107

View on GitHub
e107_handlers/file_class.php

Summary

Maintainability
B
6 hrs
Test Coverage
D
62%
<?php
/*
 * e107 website system
 *
 * Copyright (C) 2008-2010 e107 Inc (e107.org)
 * Released under the terms and conditions of the
 * GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
 *
 * $URL$
 * $Id$
 */

/**
 * File/folder manipulation handler
 *
 * @package       e107
 * @subpackage    e107_handlers
 * @version       $Id$
 * @author        e107 Inc.
 */

if(!defined('e107_INIT'))
{
    exit;
}
if(defined('SAFE_MODE') && SAFE_MODE === false)
{
    @set_time_limit(10 * 60);    // throws error in safe-mode.
}

//session_write_close();
@ini_set("max_execution_time", 10 * 60);
//while (@ob_end_clean()); // kill all output buffering else it eats server resources
//ob_implicit_flush(TRUE);


/*
Class to return a list of files, with options to specify a filename matching string and exclude specified directories.
get_files() is the usual entry point.
    $path - start directory (doesn't matter whether it has a trailing '/' or not - its stripped)
    $fmask - regex expression of file names to match (empty string matches all). Omit the start and end delimiters - '#' is added here.
                If the first character is '~', this becomes a list of files to exclude (the '~' is stripped)
                Note that 'special' characters such as '.' must be escaped by the caller
                There is a standard list of files which are always excluded (not affected by the leading '~')
                The regex is case-sensitive.
    $omit - specifies directories to exclude, in addition to the standard list. Does an exact, case-sensitive match.
                'standard' or empty string - uses the standard exclude list
                Otherwise a single directory name, or an array of names.
    $recurse_level - number of directory levels to search.

    If the standard file or directory filter is unacceptable in a special application, the relevant variable can be set to an empty array (emphasis - ARRAY).

setDefaults() restores the defaults - preferable to setting using a 'fixed' string. Can be called prior to using the class without knowledge of what went before.

get_dirs() returns a list of the directories in a specified directory (no recursion) - similar critera to get_files()

rmtree() attempts to remove a complete directory tree, including the files it contains


Note:
    Directory filters look for an exact match (i.e. regex not supported)
    Behaviour is slightly different to previous version:
        $omit used to be applied to just files (so would recurse down a tree even if no files match) - now used for directories
        The default file and directory filters are always applied (unless modified between instantiation/set defaults and call)

*/

/**
 * Flag used by prepareDirectory() method -- create directory if not present.
 */
define('FILE_CREATE_DIRECTORY', 1);

/**
 * Flag used by prepareDirectory() method -- file permissions may be changed.
 */
define('FILE_MODIFY_PERMISSIONS', 2);


/**
 *
 */
class e_file
{

    /**
     * Array of directory names to ignore (in addition to any set by caller)
     *
     * @var array
     */
    public $dirFilter = array();

    /**
     * Array of file names to ignore (in addition to any set by caller)
     *
     * @var array
     */
    public $fileFilter;

    public $filesRejected = array();

    /**
     * Defines what array format should return get_files() method
     * If one of 'fname', 'path', 'full' - numerical array.
     * If default - associative array (depends on $finfo value).
     *
     * @see get_files()
     * @var string one of the following: default (BC) | fname | path | full
     */
    public $mode = 'default';

    /**
     * Defines what info should gatter get_files method.
     * Works only in 'default' mode.
     *
     * @var string default (BC) | image | file | all
     */
    public $finfo = 'default';


//    private $authKey = false; // Used when retrieving files from e107.org.


    private $error = null;

    private $errornum = null;

    /**
     * Constructor
     */
    function __construct()
    {

        $this->setDefaults();
    }

    /**
     * Set default parameters
     *
     * @return e_file
     */
    function setDefaults()
    {

        $this->dirFilter = array('/', 'CVS', '.svn'); // Default directory filter (exact matches only)
        $this->fileFilter = array('^thumbs\.db$', '^Thumbs\.db$', '.*\._$', '^\.htaccess$', '^\.cvsignore$', '^\.ftpquota$', '^index\.html$', '^null\.txt$', '\.bak$', '^.tmp'); // Default file filter (regex format)

        return $this;
    }

    /**
     * Set fileinfo mode
     *
     * @param string $val
     * @return e_file
     */
    public function setFileInfo($val = 'default')
    {

        $this->finfo = $val;

        return $this;
    }


    /**
     * @param $filter
     * @return $this
     */
    public function setFileFilter($filter)
    {

        $this->fileFilter = $filter;

        return $this;
    }

    /**
     * Clean and rename file name
     *
     * @param $f      array as returned by get_files();
     * @param $rename boolean  - set to true to rename file.
     * @return array
     */
    public function cleanFileName($f, $rename = false)
    {

        $fullpath = $f['path'] . $f['fname'];
        $newfile = preg_replace("/[^a-z0-9-\._]/", "-", strtolower($f['fname']));
        $newpath = $f['path'] . $newfile;

        if($rename == true)
        {

            if(!rename($fullpath, $newpath))
            {
                $f['error'] = "Couldn't rename $fullpath to $newpath";
            }
        }

        $f['fname'] = $newfile;

        return $f;
    }

    /**
     * @param $mode
     * @return void
     */
    function setMode($mode)
    {

        $this->mode = $mode;
    }


    /**
     * @return null
     */
    public function getErrorMessage()
    {

        return $this->error;
    }


    /**
     * @return null
     */
    public function getErrorCode()
    {

        return $this->errornum;
    }


    /**
     * Read files from given path
     *
     * @param string  $path
     * @param string  $fmask         [optional]
     * @param string  $omit          [optional]
     * @param integer $recurse_level [optional]
     * @return array of file names/paths
     */
    function get_files($path, $fmask = '', $omit = 'standard', $recurse_level = 0)
    {

        $ret = array();
        $invert = false;
        if(!empty($fmask) && strpos($fmask, '~') === 0)
        {
            $invert = true;                        // Invert selection - exclude files which match selection
            $fmask = substr($fmask, 1);
        }

        if($recurse_level < 0)
        {
            return $ret;
        }


        if(substr($path, -1) == '/')
        {
            $path = substr($path, 0, -1);
        }


        if(!is_dir($path) || !$handle = opendir($path))
        {
            return $ret;
        }
        if(($omit == 'standard') || ($omit == ''))
        {
            $omit = $this->fileFilter;
        }
        else
        {
            if(!is_array($omit))
            {
                $omit = array($omit);
            }
        }

        while(false !== ($file = readdir($handle)))
        {
            if($file === '.' || $file === '..')
            {
                continue;
            }

            if(is_dir($path . '/' . $file))
            {    // Its a directory - recurse into it unless a filtered directory or required depth achieved
                // Must always check for '.' and '..'
                if(($recurse_level > 0) && !in_array($file, $this->dirFilter) && !in_array($file, $omit))
                {
                    $xx = $this->get_files($path . '/' . $file, $fmask, $omit, $recurse_level - 1);
                    $ret = array_merge($ret, $xx);
                }
            }
            else
            {
                // Now check against standard reject list and caller-specified list
                if(($fmask == '') || ($invert != preg_match("#" . $fmask . "#", $file)))
                {    // File passes caller's filter here
                    $rejected = false;

                    // Check against the generic file reject filter
                    foreach($omit as $rmask)
                    {
                        if(preg_match("#" . $rmask . "#", $file))
                        {
                            $rejected = true;
                            $this->filesRejected[] = $file;
                            break;            // continue 2 may well work
                        }
                    }
                    if($rejected == false)
                    {
                        switch($this->mode)
                        {
                            case 'fname':
                                $ret[] = $file;
                                break;

                            case 'path':
                                $ret[] = $path . "/";
                                break;

                            case 'full':
                                $ret[] = $path . "/" . $file;
                                break;

                            case 'all':
                            default:
                                if('default' != $this->finfo)
                                {
                                    $finfo = $this->getFileInfo($path . "/" . $file, ('file' != $this->finfo)); // -> 'all' & 'image'
                                }
                                else
                                {
                                    $finfo['path'] = $path . '/';  // important: leave this slash here and update other file instead.
                                    $finfo['fname'] = $file;
                                }
                                //    $finfo['path'] = $path.'/';  // important: leave this slash here and update other file instead.
                                //    $finfo['fname'] = $file;

                                $ret[] = $finfo;
                                break;
                        }
                    }
                }
            }
        }

        return $ret;
    }


    /**
     * Return an extension for a specific mime-type.
     *
     * @param $mimeType
     * @return string|null
     */
    function getFileExtension($mimeType)
    {

        $extensions = array(
            'application/ecmascript'                                                    => '.es',
            'application/epub+zip'                                                      => '.epub',
            'application/java-archive'                                                  => '.jar',
            'application/javascript'                                                    => '.js',
            'application/json'                                                          => '.json',
            'application/msword'                                                        => '.doc',
            'application/octet-stream'                                                  => '.bin',
            'application/ogg'                                                           => '.ogx',
            'application/pdf'                                                           => '.pdf',
            'application/rtf'                                                           => '.rtf',
            'application/typescript'                                                    => '.ts',
            'application/vnd.amazon.ebook'                                              => '.azw',
            'application/vnd.apple.installer+xml'                                       => '.mpkg',
            'application/vnd.mozilla.xul+xml'                                           => '.xul',
            'application/vnd.ms-excel'                                                  => '.xls',
            'application/vnd.ms-fontobject'                                             => '.eot',
            'application/vnd.ms-powerpoint'                                             => '.ppt',
            'application/vnd.oasis.opendocument.presentation'                           => '.odp',
            'application/vnd.oasis.opendocument.spreadsheet'                            => '.ods',
            'application/vnd.oasis.opendocument.text'                                   => '.odt',
            'application/vnd.openxmlformats-officedocument.presentationml.presentation' => '.pptx',
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'         => '.xlsx',
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document'   => '.docx',
            'application/vnd.visio'                                                     => '.vsd',
            'application/x-7z-compressed'                                               => '.7z',
            'application/x-abiword'                                                     => '.abw',
            'application/x-bzip'                                                        => '.bz',
            'application/x-bzip2'                                                       => '.bz2',
            'application/x-csh'                                                         => '.csh',
            'application/x-rar-compressed'                                              => '.rar',
            'application/x-sh'                                                          => '.sh',
            'application/x-shockwave-flash'                                             => '.swf',
            'application/x-tar'                                                         => '.tar',
            'application/xhtml+xml'                                                     => '.xhtml',
            'application/xml'                                                           => '.xml',
            'application/zip'                                                           => '.zip',
            'audio/aac'                                                                 => '.aac',
            'audio/midi'                                                                => '.midi',
            'audio/mpeg'                                                                => '.mp3',
            'audio/ogg'                                                                 => '.oga',
            'audio/wav'                                                                 => '.wav',
            'audio/webm'                                                                => '.weba',
            'font/otf'                                                                  => '.otf',
            'font/ttf'                                                                  => '.ttf',
            'font/woff'                                                                 => '.woff',
            'font/woff2'                                                                => '.woff2',
            'image/bmp'                                                                 => '.bmp',
            'image/gif'                                                                 => '.gif',
            'image/jpeg'                                                                => '.jpg',
            'image/png'                                                                 => '.png',
            'image/svg+xml'                                                             => '.svg',
            'image/tiff'                                                                => '.tiff',
            'image/webp'                                                                => '.webp',
            'image/x-icon'                                                              => '.ico',
            'text/calendar'                                                             => '.ics',
            'text/css'                                                                  => '.css',
            'text/csv'                                                                  => '.csv',
            'text/html'                                                                 => '.html',
            'text/plain'                                                                => '.txt',
            'video/mp4'                                                                 => '.mp4',
            'video/mpeg'                                                                => '.mpeg',
            'video/ogg'                                                                 => '.ogv',
            'video/webm'                                                                => '.webm',
            'video/x-msvideo'                                                           => '.avi',
        );

        if(isset($extensions[$mimeType]))
        {
            return $extensions[$mimeType];
        }

        return null;
    }

    /**
     * Return information about a file, including mime-type
     *
     * @param string $path_to_file
     * @param bool   $imgcheck
     * @param bool   $auto_fix_ext
     * @return array|bool
     * @deprecated - use getFileInfo() instead.
     */
    public function get_file_info($path_to_file, $imgcheck = true, $auto_fix_ext = true)
    {

        return $this->getFileInfo($path_to_file, $imgcheck, $auto_fix_ext);
    }

    /**
     * Collect file information
     *
     * @param string  $path_to_file
     * @param boolean $imgcheck
     * @param boolean $auto_fix_ext
     * @return array|bool
     */
    public function getFileInfo($path_to_file, $imgcheck = true, $auto_fix_ext = true)
    {

        $finfo = array();

        if(!file_exists($path_to_file) || filesize($path_to_file) < 2) // Don't try and read 0 byte files.
        {
            return false;
        }

        $finfo['pathinfo'] = pathinfo($path_to_file);
        $finfo['mime'] = $this->getMime($path_to_file);

        if($auto_fix_ext && $finfo['mime'] === false)
        {

            if(class_exists('finfo')) // Best Mime detection method.
            {
                $fin = new finfo(FILEINFO_MIME);
                list($mime, $other) = explode(";", $fin->file($path_to_file));

                if(!empty($mime))
                {
                    $finfo['mime'] = $mime;
                }

                unset($other);

            }

            // Auto-Fix Files without an extensions using known mime-type.
            if(empty($finfo['pathinfo']['extension']) && !empty($finfo['mime']) && !is_dir($path_to_file))
            {
                if($ext = $this->getFileExtension($finfo['mime']))
                {
                    $finfo['pathinfo']['extension'] = $ext;


                    $newFile = $path_to_file . $ext;
                    if(!file_exists($newFile))
                    {
                        if(rename($path_to_file, $newFile) === true)
                        {
                            $finfo['pathinfo'] = pathinfo($newFile);
                            $path_to_file = $newFile;
                        }
                    }
                }
            }
        }


        if($imgcheck && ($tmp = getimagesize($path_to_file)))
        {
            $finfo['img-width'] = $tmp[0];
            $finfo['img-height'] = $tmp[1];

            if(empty($finfo['mime']))
            {
                $finfo['mime'] = $tmp['mime'];
            }

        }

        if($tmp = stat($path_to_file))
        {

            $finfo['fsize'] = $tmp['size'];
            $finfo['modified'] = $tmp['mtime'];
        }

        $finfo['fullpath'] = $path_to_file;
        $finfo['fname'] = basename($path_to_file);
        $finfo['path'] = dirname($path_to_file) . '/';

        return $finfo;
    }


    /**
     *     Grab a remote file and save it in the /temp directory. requires CURL
     *
     * @param string $remote_url
     * @param string $local_file string filename to save as
     * @param string $type       media, temp, or import
     * @return boolean TRUE on success, FALSE on failure (which includes absence of CURL functions)
     */
    function getRemoteFile($remote_url, $local_file, $type = 'temp', $timeout = 40)
    {

        // check for cURL
        if(!function_exists('curl_init'))
        {
            $msg = 'e_file::getRemoteFile() requires cURL to be installed.';
            if(E107_DEBUG_LEVEL > 0)
            {
                e107::getLog()->addDebug($msg);
            }

            error_log($msg);

            return false;            // May not be installed
        }

        $path = ($type === 'media') ? e_MEDIA : e_TEMP;

        if($type === 'import')
        {
            $path = e_IMPORT;
        }

        $fp = fopen($path . $local_file, 'w'); // media-directory or temp directory is the root.

        $cp = $this->initCurl($remote_url);
        curl_setopt($cp, CURLOPT_FILE, $fp);
        curl_setopt($cp, CURLOPT_TIMEOUT, $timeout);
        set_time_limit($timeout);

        $buffer = curl_exec($cp);

        if(curl_errno($cp)) // Fixes curl_error output - here see #1936
        {
            error_log('cURL error: ' . curl_error($cp));
        }

        curl_close($cp);
        fclose($fp);

        return (bool) $buffer;
    }

    /**
     * @param string     $address
     * @param array|null $options
     *
     * @return CurlHandle|false
     */
    public function initCurl($address, $options = null)
    {

        $cu = curl_init();

        $timeout = (integer) vartrue($options['timeout'], 10);
        $timeout = min($timeout, 120);
        $timeout = max($timeout, 3);

        $urlData = parse_url($address);
        $referer = $urlData['scheme'] . "://" . $urlData['host'];

        if(empty($referer))
        {
            $referer = e_REQUEST_HTTP;
        }

        curl_setopt($cu, CURLOPT_URL, $address);
        curl_setopt($cu, CURLOPT_TIMEOUT, $timeout);
        curl_setopt($cu, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($cu, CURLOPT_HEADER, 0);
        curl_setopt($cu, CURLOPT_REFERER, $referer);
        curl_setopt($cu, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($cu, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($cu, CURLOPT_USERAGENT, "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0");
        curl_setopt($cu, CURLOPT_COOKIEFILE, e_SYSTEM . 'cookies.txt');
        curl_setopt($cu, CURLOPT_COOKIEJAR, e_SYSTEM . 'cookies.txt');

        if(defined('e_CURL_PROXY'))
        {
            curl_setopt($cu, CURLOPT_PROXY, e_CURL_PROXY);     // PROXY details with port
        }

        if(defined('e_CURL_PROXYUSERPWD'))
        {
            curl_setopt($cu, CURLOPT_PROXYUSERPWD, e_CURL_PROXYUSERPWD);   // Use if proxy have username and password
        }

        if(defined('e_CURL_PROXYTYPE'))
        {
            curl_setopt($cu, CURLOPT_PROXYTYPE, e_CURL_PROXYTYPE); // If expected to cal
        }

        if(!empty($options['post']))
        {
            curl_setopt($cu, CURLOPT_POST, true);
            // if array -> will encode the data as multipart/form-data, if URL-encoded string - application/x-www-form-urlencoded
            curl_setopt($cu, CURLOPT_POSTFIELDS, $options['post']);
        }

        if(!empty($options['postfields']))
        {
            curl_setopt($cu, CURLOPT_POSTFIELDS, $options['postfields']);
        }

        if(!empty($options['customrequest'])) // ie. GET, PUT, POST
        {
            curl_setopt($cu, CURLOPT_CUSTOMREQUEST, $options['customrequest']);
        }

        if(isset($options['header']) && is_array($options['header']))
        {
            curl_setopt($cu, CURLOPT_HTTPHEADER, $options['header']);
        }

        if(!file_exists(e_SYSTEM . 'cookies.txt'))
        {
            file_put_contents(e_SYSTEM . 'cookies.txt', '');
        }

        return $cu;

    }


    /**
     * FIXME add POST support
     * Get Remote contents
     * $options array:
     * - 'timeout' (integer): timeout in seconds
     * - 'post' (array|urlencoded string): POST data
     * - 'header' (array) headers, example: array('Content-Type: text/xml', 'X-Custom-Header: SomeValue');
     *
     * @param string $address
     * @param array  $options [optional]
     * @return string
     */
    public function getRemoteContent($address, $options = array())
    {

        // Could do something like: if ($timeout <= 0) $timeout = $pref['get_remote_timeout'];  here

        //    $fileContents = '';
        $this->error = '';
        $this->setErrorNum(null);

        //    $mes = e107::getMessage();

        // May be paranoia, but streaky thought it might be a good idea

        $address = str_replace(array("\r", "\n", "\t", '&amp;'), array('', '', '', '&'), $address);

        // ... and there shouldn't be unprintable characters in the URL anyway
        $requireCurl = false;

        if(!empty($options['decode']))
        {
            $address = urldecode($address);
        }

        // Keep this in first position.
        if(function_exists("curl_init")) // Preferred.
        {

            $cu = $this->initCurl($address, $options);

            $fileContents = curl_exec($cu);
            if(curl_error($cu))
            {
                $errorCode = curl_errno($cu);
                $this->setErrorNum($errorCode);
                $this->error = "Curl error: " . $errorCode . ", " . curl_error($cu);

                return false;
            }
            curl_close($cu);

            return $fileContents;
        }

        // CURL is required, abort...
        if($requireCurl == true)
        {
            return false;
        }

        $timeout = 5;

        if(function_exists('file_get_contents') && ini_get('allow_url_fopen'))
        {
            $old_timeout = ini_set('default_socket_timeout', $timeout);

            $context = array(
                'ssl' => array(
                    'verify_peer'      => false,
                    'verify_peer_name' => false,
                ),
            );

            $data = file_get_contents($address, false, stream_context_create($context));

            //          $data = file_get_contents(htmlspecialchars($address));    // buggy - sometimes fails.
            if($old_timeout !== false)
            {
                @ini_set('default_socket_timeout', $old_timeout);
            }
            if($data !== false)
            {
                //    $fileContents = $data;
                return $data;
            }
            $this->error = "File_get_contents(XML) error";        // Fill in more info later

            return false;
        }

        if(ini_get("allow_url_fopen"))
        {
            $old_timeout = ini_set('default_socket_timeout', $timeout);
            $remote = @fopen($address, "r");
            if(!$remote)
            {
                $this->error = "fopen: Unable to open remote XML file: " . $address;

                return false;
            }
        }
        else
        {
            $old_timeout = $timeout;
            $tmp = parse_url($address);
            if(!$remote = fsockopen($tmp['host'], 80, $errno, $errstr, $timeout))
            {
                $this->error = "Sockets: Unable to open remote XML file: " . $address;

                return false;
            }
            else
            {
                stream_set_timeout($remote, $timeout);
                fwrite($remote, "GET " . urlencode($address) . " HTTP/1.0\r\n\r\n");
            }
        }
        $fileContents = "";
        while(!feof($remote))
        {
            $fileContents .= fgets($remote, 4096);
        }
        fclose($remote);
        if($old_timeout != $timeout)
        {
            if($old_timeout !== false)
            {
                ini_set('default_socket_timeout', $old_timeout);
            }
        }

        return $fileContents;
    }


    /**
     * Get a list of directories matching $fmask, omitting any in the $omit array - same calling syntax as get_files()
     * N.B. - no recursion - just looks in the specified directory.
     *
     * @param string $path
     * @param string $fmask
     * @param string $omit
     * @return array
     */
    function get_dirs($path, $fmask = '', $omit = 'standard')
    {

        $ret = array();
        $path = rtrim($path, '/');
        if($path[strlen($path) - 1] === '/')
            //    if(substr($path, -1) == '/')
        {
            $path = substr($path, 0, -1);
        }

        if(!$handle = opendir($path))
        {
            return $ret;
        }

        if($omit == 'standard')
        {
            $omit = array();
        }
        else
        {
            if(!is_array($omit))
            {
                $omit = array($omit);
            }
        }

        while(false !== ($file = readdir($handle)))
        {
            if(($file != '.') && ($file != '..') && !in_array($file, $this->dirFilter) && !in_array($file, $omit) && is_dir($path . '/' . $file) && ($fmask == '' || preg_match("#" . $fmask . "#", $file)))
            {
                $ret[] = $file;
            }
        }

        return $ret;
    }

    /**
     * Delete a complete directory tree
     *
     * @param string $dir
     * @return boolean success
     */
    function rmtree($dir)
    {

        if(substr($dir, -1) != '/')
        {
            $dir .= '/';
        }
        if($handle = opendir($dir))
        {
            while($obj = readdir($handle))
            {
                if($obj != '.' && $obj != '..')
                {
                    if(is_dir($dir . $obj))
                    {
                        if(!$this->rmtree($dir . $obj))
                        {
                            return false;
                        }
                    }
                    elseif(is_file($dir . $obj))
                    {
                        if(!unlink($dir . $obj))
                        {
                            return false;
                        }
                    }
                }
            }

            closedir($handle);

            if(!@rmdir($dir))
            {
                return false;
            }

            return true;
        }

        return false;
    }

    /**
     *    Parse a file size string (e.g. 16M) and compute the simple numeric value.
     *
     * @param string $source  - input string which may include 'multiplier' characters such as 'M' or 'G'. Converted to 'decoded value'
     * @param int    $compare - a 'compare' value
     * @param string $action  - values (gt|lt)
     *
     * @return int file size value in bytes.
     *        If the decoded value evaluates to zero, returns the value of $compare
     *        If $action == 'gt', return the larger of the decoded value and $compare
     *        If $action == 'lt', return the smaller of the decoded value and $compare
     */
    function file_size_decode($source, $compare = 0, $action = '')
    {

        $source = trim($source);
        $source = strtoupper($source);

        list($val, $unit) = array_pad(preg_split('#(?<=\d)(?=[a-z])#i', $source), 2, '');

        $val = (int) $val;

        if(!$source || is_numeric($source))
        {
            $val = (int) $source;
        }
        else
        {
            switch($unit)
            {
                case 'T':
                case 'TB':
                    $val = $val * 1024 * 1024 * 1024 * 1024;
                    break;
                case 'G':
                case 'GB':
                    $val = $val * 1024 * 1024 * 1024;
                    break;
                case 'M':
                case 'MB':
                    $val = $val * 1024 * 1024;
                    break;
                case 'K':
                case 'KB':
                    $val = $val * 1024;
                    break;
            }
        }
        if($val == 0)
        {
            return $compare;
        }

        switch($action)
        {
            case 'lt':
                return min($val, $compare);
            case 'gt':
                return max($val, $compare);
            default:
                return $val;
        }
        //    return 0;
    }

    /**
     * Parse bytes to human readable format
     * Former Download page function
     *
     * @param mixed   $size     file size in bytes or file path if $retrieve is true
     * @param boolean $retrieve defines the type of $size
     * @param integer $decimal
     * @return string formatted size
     */
    function file_size_encode($size, $retrieve = false, $decimal = 2)
    {

        if($retrieve)
        {
            $size = filesize($size);
        }
        $kb = 1024;
        $mb = 1024 * $kb;
        $gb = 1024 * $mb;
        $tb = 1024 * $gb;
        if(!$size)
        {
            return '0&nbsp;' . CORE_LAN_B;
        }
        if($size < $kb)
        {
            return $size . "&nbsp;" . CORE_LAN_B;
        }
        elseif($size < $mb)
        {
            return round($size / $kb, $decimal) . "&nbsp;" . CORE_LAN_KB;
        }
        elseif($size < $gb)
        {
            return round($size / $mb, $decimal) . "&nbsp;" . CORE_LAN_MB;
        }
        elseif($size < $tb)
        {
            return round($size / $gb, $decimal) . "&nbsp;" . CORE_LAN_GB;
        }
        else
        {
            return round($size / $tb, 2) . "&nbsp;" . CORE_LAN_TB;
        }
    }


    /** Recursive Chmod function.
     *
     * @param string  $path     to folder
     * @param integer $filemode perms for files
     * @param integer $dirmode  perms for directories
     * @example chmod_R('mydir', 0644, 0755);
     */
    function chmod($path, $filemode = 0644, $dirmode = 0755)
    {

        if(is_dir($path))
        {
            if(!chmod($path, $dirmode))
            {
                $dirmode_str = decoct($dirmode);
                print "Failed applying filemode '$dirmode_str' on directory '$path'\n";
                print "  `-> the directory '$path' will be skipped from recursive chmod\n";

                return;
            }
            $dh = opendir($path);
            while(($file = readdir($dh)) !== false)
            {
                if($file != '.' && $file != '..')   // skip self and parent pointing directories
                {
                    $fullpath = $path . '/' . $file;
                    $this->chmod($fullpath, $filemode, $dirmode);
                }
            }
            closedir($dh);
        }
        else
        {
            if(is_link($path))
            {
                print "link '$path' is skipped\n";

                return;
            }

            if(!chmod($path, $filemode))
            {
                $filemode_str = decoct($filemode);
                print "Failed applying filemode '$filemode_str' on file '$path'\n";

                return;
            }
        }
    }


    /**
     * Copy a file, or copy the contents of a folder.
     *
     * @param string $source Source path
     * @param string $dest   Destination path
     * @param array  $options
     * @return  bool     Returns true on success, false on error
     */
    function copy($source, $dest, $options = array())
    {

        $perm = !empty($options['perm']) ? $options['perm'] : 0755;
        $filter = !empty($options['git']) ? "" : ".git"; // filter out .git by default.

        // Simple copy for a file
        if(is_file($source))
        {
            return copy($source, $dest);
        }

        // Make destination directory
        if(!is_dir($dest))
        {
            mkdir($dest, $perm);
        }

        // Directory - so copy it.
        $dir = scandir($source);
        foreach($dir as $folder)
        {
            // Skip pointers
            if($folder === '.' || $folder == '..' || $folder === $filter)
            {
                continue;
            }

            $this->copy("$source/$folder", "$dest/$folder", $perm);
        }

        return true;
    }


    /**
     * File retrieval function. by Cam.
     *
     * @param string $file actual path or {e_xxxx} path to file.
     * @param string $opts (optional) type | disposition | encoding values.
     *
     */
    function send($file, $opts = array())
    {

        global $e107;

        //    $pref                     = e107::getPref();
        $tp = e107::getParser();

        $DOWNLOADS_DIR = e107::getFolder('DOWNLOADS');
        $DOWNLOADS_DIRECTORY = ($DOWNLOADS_DIR[0] == DIRECTORY_SEPARATOR) ? $DOWNLOADS_DIR : e_BASE . $DOWNLOADS_DIR; // support for full path eg. /home/account/folder.
        $FILES_DIRECTORY = e_BASE . e107::getFolder('FILES');
        $MEDIA_DIRECTORY = realpath(e_MEDIA); //  could be image, file or other type.
        $SYSTEM_DIRECTORY = realpath(e_SYSTEM); // downloading of logs or hidden files etc. via browser if required.

        $file = $tp->replaceConstants($file);


        @set_time_limit(10 * 60);
        @session_write_close();
        @ini_set("max_execution_time", 10 * 60);
        while(ob_get_length() !== false)  // destroy all ouput buffering
        {
            ob_end_clean();
        }
        @ob_implicit_flush();


        $filename = $file;
        $file = basename($file);
        $path = realpath($filename);
        $path_downloads = realpath($DOWNLOADS_DIRECTORY);
        $path_public = realpath($FILES_DIRECTORY . "public/");


        if(strpos($path, $path_downloads) === false && strpos($path, $path_public) === false && strpos($path, $MEDIA_DIRECTORY) === false && strpos($path, $SYSTEM_DIRECTORY) === false)
        {
            if(E107_DEBUG_LEVEL > 0 && ADMIN)
            {
                echo "Failed to Download <b>" . $file . "</b><br />";
                echo "The file-path <b>" . $path . "<b> didn't match with either of 
                <ul><li><b>{$path_downloads}</b></li>
                <li><b>{$path_public}</b></li></ul><br />";
                echo "Downloads Path: " . $path_downloads . " (" . $DOWNLOADS_DIRECTORY . ")";
                exit();
            }
            else
            {
                header("location: {$e107->base_path}");
                exit();
            }
        }
        else
        {
            if(is_file($filename) && is_readable($filename) && connection_status() == 0)
            {
                $seek = 0;
                if(strpos($_SERVER['HTTP_USER_AGENT'], "MSIE") !== false)
                {
                    $file = preg_replace('/\./', '%2e', $file, substr_count($file, '.') - 1);
                }
                if(isset($_SERVER['HTTP_RANGE']))
                {
                    $seek = intval(substr($_SERVER['HTTP_RANGE'], strlen('bytes=')));
                }
                $bufsize = 2048;
                ignore_user_abort(true);
                $data_len = filesize($filename);
                if($seek > ($data_len - 1))
                {
                    $seek = 0;
                }
                //    if ($filename == null) { $filename = basename($this->data); }
                $res = fopen($filename, 'rb');
                if($seek)
                {
                    fseek($res, $seek);
                }
                $data_len -= $seek;

                $contentType = vartrue($opts['type'], 'application/force-download');
                $contentDisp = vartrue($opts['disposition'], 'attachment');

                header('Expires: 0');
                header("Cache-Control: max-age=30");
                header('Content-Type: ' . $contentType);
                header('Content-Disposition: ' . $contentDisp . '; filename="' . $file . '"');
                header("Content-Length: {$data_len}");
                header("Pragma: public");

                if(!empty($opts['encoding']))
                {
                    header('Content-Transfer-Encoding: ' . $opts['encoding']);
                }

                if($seek)
                {
                    header("Accept-Ranges: bytes");
                    header("HTTP/1.0 206 Partial Content");
                    header("status: 206 Partial Content");
                    header("Content-Range: bytes {$seek}-" . ($data_len - 1) . "/{$data_len}");
                }
                while(!connection_aborted() && $data_len > 0)
                {
                    echo fread($res, $bufsize);
                    $data_len -= $bufsize;
                }
                fclose($res);
            }
            else
            {
                if(E107_DEBUG_LEVEL > 0 && ADMIN)
                {
                    $mes = __METHOD__ . " -- File failed: " . $file . "\n";
                    $mes .= "Path: " . $path . "\n";
                    $mes .= "Backtrace: ";
                    $mes .= print_r(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), true);
                    trigger_error($mes);
                    exit();
                }
                else
                {
                    header("location: " . e_BASE . "index.php");
                    exit();
                }
            }
        }
    }


    /**
     * Return a user specific file directory for the current plugin with the option to create one if it does not exist.
     *
     * @param int         $user userid
     * @param boolean     $create
     * @param null|string $subDir
     * @return string
     */
    public function getUserDir($user, $create = false, $subDir = null)
    {

        $tp = e107::getParser();

        $baseDir = e_MEDIA . 'plugins/' . e_CURRENT_PLUGIN . '/';

        if(!empty($subDir))
        {
            $subDir = e107::getParser()->filter($subDir, 'w');
            $baseDir .= rtrim($subDir, '/') . '/';
        }

        if(is_numeric($user))
        {
            $baseDir .= ($user > 0) ? "user_" . $tp->leadingZeros($user, 6) : "anon";
        }

        if($create == true && !is_dir($baseDir))
        {
            mkdir($baseDir, 0755, true); // recursively
        }

        $baseDir = rtrim($baseDir, '/') . "/";

        return $baseDir;
    }


    /**
     * Runs through the zip archive array and finds the root directory.
     *
     * @param $unarc
     * @return bool|string
     */
    public function getRootFolder($unarc)
    {

        foreach($unarc as $d)
        {
            $target = trim($d['stored_filename'], '/');

            $test = basename(str_replace(e_TEMP, "", $d['stored_filename']), '/');

            if($d['folder'] == 1 && $target == $test)  //
            {
                //    $text .= "\\n test = ".$test;
                $text = "getRootDirectory: " . $d['stored_filename'];
                $text .= "<br />test=" . $test;
                $text .= "<br />target=" . $target;

                if(E107_DEBUG_LEVEL > 0)
                {
                    e107::getMessage()->addDebug($text);
                    //     echo "<script>alert('".$text."')</script>";
                }

                return $target;

            }
        }

        return false;

    }


    private function addToZip(ZipArchive $zip, $src, $localname)
    {

        if(is_dir($src))
        {
            $dir = opendir($src);

            // add empty directories
            $zip->addEmptyDir($localname);

            while(false !== ($file = readdir($dir)))
            {
                if(($file != '.') && ($file != '..'))
                {
                    $this->addToZip($zip, $src . '/' . $file, $localname . '/' . $file);
                }
            }

            closedir($dir);
        }
        elseif(is_file($src))
        {
            if(!$zip->addFile($src, $localname))
            {
                $this->error = "Could not add file: $src";
                e107::getLog()->addError($this->error)->save('FILE', E_LOG_NOTICE);
            }
        }
    }


    /**
     * Zip up folders and files
     *
     * @param array  $filePaths
     * @param string $newFile
     * @param array  $options
     * @return bool|string
     */
     public function zip($filePaths = null, $newFile = '', $options = array())
    {
        if(empty($newFile))
        {
            $newFile = e_BACKUP . eHelper::title2sef(SITENAME) . "_" . date("Y-m-d-H-i-s") . ".zip";
        }

        if($filePaths === null)
        {
            return "No file-paths set!";
        }

        $zip = new ZipArchive();

        if($zip->open($newFile, ZipArchive::CREATE) !== true)
        {
            $this->error = "Cannot open <$newFile>\n";
            e107::getLog()->addError($this->error)->save('FILE', E_LOG_NOTICE);

            return false;
        }

        $removePath = (!empty($options['remove_path'])) ? $options['remove_path'] : e_BASE;

        foreach($filePaths as $file)
        {
            $localname = str_replace($removePath, '', $file);
            $this->addToZip($zip, $file, rtrim($localname, '/'));
        }

        $zip->close();

        return $newFile;
    }


    /**
     * Delete a file.
     *
     * @param $file
     * @return bool
     */
    public function delete($file)
    {

        if(empty($file))
        {
            return false;
        }

        $file = e107::getParser()->replaceConstants($file);

        if(file_exists($file))
        {
            return unlink($file);
        }

        return false;

    }


    /**
     * Recursive Directory removal .
     *
     * @param $dir
     */
    public function removeDir($dir)
    {

        if(is_dir($dir))
        {
            $objects = scandir($dir);
            foreach($objects as $object)
            {
                if($object != "." && $object != "..")
                {
                    if(filetype($dir . "/" . $object) == "dir")
                    {
                        $this->removeDir($dir . "/" . $object);
                    }
                    else
                    {
                        @unlink($dir . "/" . $object);
                    }
                }
            }

            reset($objects);
            @rmdir($dir);
        }
    }


    /**
     * File-class wrapper for upload handler. (Preferred for v2.x)
     * Process files uploaded in a form post. ie. $_FILES.
     * Routine processes the array of uploaded files according to both specific options set by the caller,
     * and    system options configured by the main admin.
     *
     * @param string $uploaddir  Target directory (checked that it exists, but path not otherwise changed)
     *
     * @param string $fileinfo   Determines any special handling of file name (combines previous $fileinfo and $avatar parameters):
     *                           FALSE - default option; no processing
     *                           = 'attachment+extra_text' Indicates an attachment (related to forum post or PM), and specifies some optional text which is
     *                           incorporated into the final file name (the original $fileinfo parameter).
     *                           = 'prefix+extra_text' - indicates an attachment or file, and specifies some optional text which is prefixed to the file name
     *                           = 'unique'
     *                           - if the proposed destination file doesn't exist, saved under given name
     *                           - if the proposed destination file does exist, prepends time() to the file name to make it unique
     *                           =  'avatar'
     *                           - indicates an avatar is being uploaded (not used - options must be set elsewhere)
     *
     * @param array  $options    = [  An array of supplementary options, all of which will be given appropriate defaults if not defined:
     *                           'filetypes'            => (string)         Name of file containing list of valid file types
     *                           - Always looks in the admin directory
     *                           - defaults to e_ADMIN.filetypes.xml, else e_ADMIN.admin_filetypes.php for admins (if file exists), otherwise e_ADMIN.filetypes.php for users.
     *                           - FALSE disables this option (which implies that 'extra_file_types' is used)
     *                           'file_mask'            => (string)         Comma-separated list of file types which if defined limits the allowed file types to those which are in both this list and the
     *                           file specified by the 'filetypes' option. Enables restriction to, for example, image files.
     *                           'filetypes'            => (bool)         file).
     *                           if TRUE, accepts totally unknown file extensions which are in $options['filetypes'] file.
     *                           otherwise specifies a comma-separated list of additional permitted file extensions
     *                           'final_chmod'        => (int)         - chmod() to be applied to uploaded files (0644 default)  (This routine expects an integer value, so watch formatting/decoding - its normally
     *                           specified in octal. Typically use intval($permissions,8) to convert)
     *                           'max_upload_size'        => (int)         - maximum size of uploaded files in bytes, or as a string with a 'multiplier' letter (e.g. 16M) at the end.
     *                           - otherwise uses $pref['upload_maxfilesize'] if set
     *                           - overriding limit of the smaller of 'post_max_size' and 'upload_max_size' if set in php.ini
     *                           (Note: other parts of E107 don't understand strings with a multiplier letter yet)
     *                           'file_array_name'    => (string)         - the name of the 'input' array - defaults to file_userfile[] - otherwise as set.
     *                           'max_file_count'    => (int)         - maximum number of files which can be uploaded - default is 'unlimited' if this is zero or not set.
     *                           'overwrite'            => (bool)         - if TRUE, existing file of the same name is overwritten; otherwise returns 'duplicate file' error (default FALSE)
     *                           'save_to_db'        => (int)         - [obsolete] storage type - if set and TRUE, uploaded files were saved in the database (rather than as flat files)
     *                           ]
     * @return boolean|array
     *                           Returns FALSE if the upload directory doesn't exist, or various other errors occurred which restrict the amount of meaningful information.
     *                           Returns an array, with one set of entries per uploaded file, regardless of whether saved or
     *                           discarded (not all fields always present) - $c is array index:
     *                           $uploaded[$c]['name'] - file name - as saved to disc
     *                           $uploaded[$c]['rawname'] - original file name, prior to any addition of identifiers etc (useful for display purposes)
     *                           $uploaded[$c]['type'] - mime type (if set - as sent by browser)
     *                           $uploaded[$c]['size'] - size in bytes (should be zero if error)
     *                           $uploaded[$c]['error'] - numeric error code (zero = OK)
     *                           $uploaded[$c]['index'] - if upload successful, the index position from the file_userfile[] array - usually numeric, but may be alphanumeric if coded
     *                           $uploaded[$c]['message'] - text of displayed message relating to file
     *                           $uploaded[$c]['line'] - only if an error occurred, has line number (from __LINE__)
     *                           $uploaded[$c]['file'] - only if an error occurred, has file name (from __FILE__)
     *
     *    On exit, uploaded files should all have been removed from the temporary directory.
     *    No messages displayed - its caller's responsibility to handle errors and display info to
     *    user (or can use handle_upload_messages() from this module)
     *
     *    Details of uploaded files are in $_FILES['file_userfile'] (or other array name as set) on entry.
     *    Elements passed (from PHP) relating to each file:
     *        ['name']    - the original name
     *        ['type']    - mime type (if provided - not checked by PHP)
     *        ['size']    - file size in bytes
     *        ['tmp_name'] - temporary file name on server
     *        ['error']    - error code. 0 = 'good'. 1..4 main others, although up to 8 defined for later PHP versions
     *    Files stored in server's temporary directory, unless another set
     */
    public function getUploaded($uploaddir, $fileinfo = false, $options = array())
    {

        require_once(e_HANDLER . "upload_handler.php");

        if($uploaddir == e_UPLOAD || $uploaddir == e_TEMP || $uploaddir == e_AVATAR_UPLOAD)
        {
            $path = $uploaddir;
        }
        elseif(defined('e_CURRENT_PLUGIN'))
        {
            $path = $this->getUserDir(USERID, true, str_replace("../", '', $uploaddir)); // .$this->get;
        }
        else
        {
            return false;
        }

        return process_uploaded_files($path, $fileinfo, $options);

    }


    /**
     * Quickly scan and return a list of files in a directory.
     *
     * @param string $dir
     * @param null   $extensions
     * @return array
     */
    public function scandir($dir, $extensions = null)
    {

        $list = array();

        $ext = str_replace(",", "|", $extensions);

        $tmp = scandir($dir);
        foreach($tmp as $v)
        {
            if($v == '.' || $v == '..')
            {
                continue;
            }

            if(!empty($ext) && !preg_match("/\.(" . $ext . ")$/i", $v))
            {

                continue;
            }

            $list[] = $v;
        }

        return $list;
    }


    /**
     * @param string $folder
     * @param null   $type
     * @return bool|string
     */
    public function gitPull($folder = '', $type = null)
    {

        $gitPath = defset('e_GIT', 'git'); // addo to e107_config.php to
        $mes = e107::getMessage();


        //    $text = 'umask 0022'; //Could correct permissions issue with 0664 files.
        // Change Dir.
        $folder = e107::getParser()->filter($folder, 'file'); // extra filter to keep RIPS happy.

        switch($type)
        {
            case "plugin":
                $dir = realpath(e_PLUGIN . basename($folder));
                break;

            case "theme":
                $dir = realpath(e_THEME . basename($folder));
                break;

            default:
                $dir = e_ROOT;
        }


        //    $cmd1 = 'cd '.$dir;
        $cmd2 = 'cd ' . $dir . '; ' . $gitPath . ' reset --hard'; // Remove any local changes.
        $cmd3 = 'cd ' . $dir . '; ' . $gitPath . ' pull';    // Run Pull request


        $text = '';


        $mes->addDebug($cmd2);
        $mes->addDebug($cmd3);

        //    $text = `$cmd1 2>&1`;
        $text .= `$cmd2 2>&1`;
        $text .= `$cmd3 2>&1`;


        if(deftrue('e_DEBUG') || deftrue('e_GIT_DEBUG'))
        {
            $message = date('r') . "\t\tgitPull()\t\t" . $text;
            file_put_contents(e_LOG . "fileClass.log", $message, FILE_APPEND);
        }

        //    $text .= `$cmd4 2>&1`;

        //    $text .= `$cmd5 2>&1`;

        return print_a($text, true);

    }


    /**
     * Returns true is the URL is valid and false if it is not.
     *
     * @param $url
     * @return bool
     */
    public function isValidURL($url)
    {

        ini_set('default_socket_timeout', 1);
        $headers = get_headers($url);

        //   print_a($headers);

        return (stripos($headers[0], "200 OK") || strpos($headers[0], "302"));
    }


    /**
     * Unzip Plugin or Theme zip file and move to plugin or theme folder.
     *
     * @param string $localfile - filename located in e_TEMP
     * @param string $type      - addon type, either 'plugin' or 'theme', (possibly 'language' in future).
     * @param bool   $overwrite
     * @return string unzipped folder name on success or false.
     */
    public function unzipArchive($localfile, $type, $overwrite = false)
    {

        $mes = e107::getMessage();

        chmod(e_TEMP . $localfile, 0755);

        $fileinfo = array();

        $dir = false;

        if(class_exists('ZipArchive')) // PHP7 compat. method.
        {
            $zip = new ZipArchive;

            if($zip->open(e_TEMP . $localfile) === true)
            {
                for($i = 0; $i < $zip->numFiles; $i++)
                {
                    $filename = $zip->getNameIndex($i);

                    $fileinfo = pathinfo($filename);

                    if($fileinfo['dirname'] === '.')
                    {
                        $dir = $fileinfo['basename'];
                        break;
                    }
                    elseif($fileinfo['basename'] === 'plugin.php' || $fileinfo['basename'] === 'theme.php')
                    {
                        $dir = $fileinfo['dirname'];
                    }

                    //   $stat = $zip->statIndex( $i );
                    //    print_a( $stat['name']  );
                }


                $zip->extractTo(e_TEMP);
                chmod(e_TEMP . $dir, 0755);

                if(empty($dir) && deftrue('e_DEBUG'))
                {
                    print_a($fileinfo);
                }


                $zip->close();
            }


        }
    /*    else // Legacy Method.
        {
            require_once(e_HANDLER . "pclzip.lib.php");

            $archive = new PclZip(e_TEMP . $localfile);
            $unarc = ($fileList = $archive->extract(PCLZIP_OPT_PATH, e_TEMP, PCLZIP_OPT_SET_CHMOD, 0755)); // Store in TEMP first.
            $dir = $this->getRootFolder($unarc);
        }*/


        $destpath = ($type == 'theme') ? e_THEME : e_PLUGIN;
        //    $typeDiz     = ucfirst($type);

        @copy(e_TEMP . $localfile, e_BACKUP . $dir . ".zip"); // Make a Backup in the system folder.

        if($dir && is_dir($destpath . $dir))
        {
            if($overwrite === true)
            {
                if(file_exists(e_TEMP . $localfile))
                {
                    $time = date("YmdHi");
                    if(rename($destpath . $dir, e_BACKUP . $dir . "_" . $time))
                    {
                        $mes->addSuccess(ADLAN_195);
                    }
                }
            }
            else
            {

                $mes->addError("(" . ucfirst($type) . ") Already Downloaded - " . basename($destpath) . '/' . $dir);

                if(file_exists(e_TEMP . $localfile))
                {
                    @unlink(e_TEMP . $localfile);
                }

                $this->removeDir(e_TEMP . $dir);

                return false;
            }
        }

        if(empty($dir))
        {
            $mes->addError("Couldn't detect the root folder in the zip."); //  flush();
            @unlink(e_TEMP . $localfile);

            return false;
        }

        if(is_dir(e_TEMP . $dir))
        {
            $res = rename(e_TEMP . $dir, $destpath . $dir);
            if($res === false)
            {
                $mes->addError("Couldn't Move " . e_TEMP . $dir . " to " . $destpath . $dir . " Folder"); //  flush(); usleep(50000);
                @unlink(e_TEMP . $localfile);

                return false;
            }


            //    $dir         = basename($unarc[0]['filename']);
            //    $plugPath    = preg_replace("/[^a-z0-9-\._]/", "-", strtolower($dir));
            //$status = "Done"; // ADMIN_TRUE_ICON;
            @unlink(e_TEMP . $localfile);

            return $dir;
        }

        return false;
    }


    /**
     * @param string|boolean $file_mask - comma-separated list of allowed file types
     * @param string         $filename  - optional override file name - defaults ignored
     *
     * @return array of filetypes
     * @deprecated Use getAllowedFileTypes()
     *             Get an array of permitted filetypes according to a set hierarchy.
     *             If a specific file name given, that's used. Otherwise the default hierarchy is used
     *
     */
    function getFiletypeLimits($file_mask = false, $filename = '') // Wrapper only for now.
    {

        require_once(e_HANDLER . "upload_handler.php");
        $limits = get_filetypes($file_mask, $filename);
        ksort($limits);

        return $limits;
    }


    /**
     * Download and extract a zipped copy of e107
     *
     * @param string $url              "core" to download the e107 core from Git master or
     *                                 a custom download URL
     * @param string $destination_path The e107 root where the downloaded archive should be extracted,
     *                                 with a directory separator at the end
     * @return array|bool FALSE on failure;
     *                                 An array of successful and failed path extractions
     */
    public function unzipGithubArchive($url = 'core', $destination_path = e_BASE)
    {

        switch($url)
        {
            case "core":
                $localfile = 'e107-master.zip';
                $remotefile = 'https://codeload.github.com/e107inc/e107/zip/master';
                $excludes = array(
                    'e107-master/.codeclimate.yml',
                    'e107-master/.editorconfig',
                    'e107-master/.gitignore',
                    'e107-master/.gitmodules',
                    'e107-master/CONTRIBUTING.md', # moved to ./.github/CONTRIBUTING.md
                    'e107-master/LICENSE',
                    'e107-master/README.md',
                    'e107-master/composer.json',
                    'e107-master/composer.lock',
                    'e107-master/install.php',
                    'e107-master/favicon.ico',
                );
                $excludeMatch = array(
                    '/.github/',
                    '/e107_tests/',
                );
                break;

            // language.
            // eg. https://github.com/e107translations/Spanish/archive/v2.1.5.zip
            default:
                // 'e107-master.zip';
                $localfile = str_replace(array('https://github.com/e107translations/', '/archive/v'), array('', '-'), $url); //remove dirs.
                $remotefile = $url;
                $excludes = array();
                $excludeMatch = array('alt_auth', 'tagwords', 'faqs');

        }

        // Delete any existing file.
        if(file_exists(e_TEMP . $localfile))
        {
            unlink(e_TEMP . $localfile);
        }

        $result = $this->getRemoteFile($remotefile, $localfile);

        if($result === false)
        {
            return false;
        }


        chmod(e_TEMP . $localfile, 0755);
        require_once(e_HANDLER . "pclzip.lib.php");

        $zipBase = str_replace('.zip', '', $localfile); // eg. e107-master
        $excludes[] = $zipBase;

        $newFolders = array(
            $zipBase . '/e107_admin/'     => $destination_path . e107::getFolder('ADMIN'),
            $zipBase . '/e107_core/'      => $destination_path . e107::getFolder('CORE'),
            $zipBase . '/e107_docs/'      => $destination_path . e107::getFolder('DOCS'),
            $zipBase . '/e107_handlers/'  => $destination_path . e107::getFolder('HANDLERS'),
            $zipBase . '/e107_images/'    => $destination_path . e107::getFolder('IMAGES'),
            $zipBase . '/e107_languages/' => $destination_path . e107::getFolder('LANGUAGES'),
            $zipBase . '/e107_media/'     => $destination_path . e107::getFolder('MEDIA'),
            $zipBase . '/e107_plugins/'   => $destination_path . e107::getFolder('PLUGINS'),
            $zipBase . '/e107_system/'    => $destination_path . e107::getFolder('SYSTEM'),
            $zipBase . '/e107_themes/'    => $destination_path . e107::getFolder('THEMES'),
            $zipBase . '/e107_web/'       => $destination_path . e107::getFolder('WEB'),
            $zipBase . '/'                => $destination_path
        );

        $srch = array_keys($newFolders);
        $repl = array_values($newFolders);

        $archive = new PclZip(e_TEMP . $localfile);
        $unarc = ($fileList = $archive->extract(PCLZIP_OPT_PATH, e_TEMP, PCLZIP_OPT_SET_CHMOD, 0755)); // Store in TEMP first.

        $error = array();
        $success = array();
        $skipped = array();


        foreach($unarc as $k => $v)
        {
            if(
                $this->matchFound($v['stored_filename'], $excludeMatch) ||
                in_array($v['stored_filename'], $excludes)
            )
            {
                $skipped[] = $v['stored_filename'];
                continue;
            }

            $oldPath = $v['filename'];
            $newPath = str_replace($srch, $repl, $v['stored_filename']);

            if($v['folder'] == 1 && is_dir($newPath))
            {
                // $skipped[] =  $newPath. " (already exists)";
                continue;
            }
            @mkdir(dirname($newPath), 0755, true);
            if(!rename($oldPath, $newPath))
            {
                $error[] = $newPath;
            }
            else
            {
                $success[] = $newPath;
            }

        }

        return array('success' => $success, 'error' => $error, 'skipped' => $skipped);
    }


    /**
     * @param $file
     * @param $array
     * @return bool
     */
    private function matchFound($file, $array)
    {

        if(empty($array))
        {
            return false;
        }

        foreach($array as $term)
        {
            if(strpos($file, $term) !== false)
            {
                return true;
            }

        }

        return false;

    }

    /**
     * Checks that the directory exists and is writable.
     *
     * @param string $directory
     *   A string containing the name of a directory path. A trailing slash will be trimmed from a path.
     * @param int    $options
     *   A bitmask to indicate if the directory should be created if it does not exist (FILE_CREATE_DIRECTORY) or
     *   made writable if it is read-only (FILE_MODIFY_PERMISSIONS).
     *
     * @return bool
     *   TRUE if the directory exists (or was created) and is writable. FALSE otherwise.
     */
    public function prepareDirectory($directory, $options = FILE_MODIFY_PERMISSIONS)
    {

        $directory = e107::getParser()->replaceConstants($directory);
        $directory = rtrim($directory, '/\\');

        // Check if directory exists.
        if(!is_dir($directory))
        {
            // Let mkdir() recursively create directories and use the default directory permissions.
            if(($options & FILE_CREATE_DIRECTORY) && @$this->mkDir($directory, null, true))
            {
                return $this->_chMod($directory);
            }

            return false;
        }

        // The directory exists, so check to see if it is writable.
        $writable = is_writable($directory);

        if(!$writable && ($options & FILE_MODIFY_PERMISSIONS))
        {
            return $this->_chMod($directory);
        }

        return $writable;
    }

    /**
     * (Non-Recursive) Sets the permissions on a file or directory.
     *
     * @param string $path
     *   A string containing a file, or directory path.
     * @param int    $mode
     *   Integer value for the permissions. Consult PHP chmod() documentation for more information.
     *
     * @return bool
     *   TRUE for success, FALSE in the event of an error.
     */
    private function _chMod($path, $mode = null)
    {

        if(!isset($mode))
        {
            if(is_dir($path))
            {
                $mode = 0775;
            }
            else
            {
                $mode = 0664;
            }
        }

        if(@chmod($path, $mode))
        {
            return true;
        }

        return false;
    }

    /**
     * Creates a directory.
     *
     * @param string $path
     *   A string containing a file path.
     * @param int    $mode
     *   Mode is used.
     * @param bool   $recursive
     *   Default to FALSE.
     * @param null   $context
     *   Refer to http://php.net/manual/ref.stream.php
     *
     * @return bool
     *   Boolean TRUE on success, or FALSE on failure.
     */
    public function mkDir($path, $mode = null, $recursive = false, $context = null)
    {

        if(!isset($mode))
        {
            $mode = 0775;
        }

        if(!isset($context))
        {
            return mkdir($path, $mode, $recursive);
        }
        else
        {
            return mkdir($path, $mode, $recursive, $context);
        }
    }


    /**
     * @param int|null $int
     */
    private function setErrorNum($int)
    {

        $this->errornum = $int;
    }


    /**
     * New in v2.1.9
     * Check uploaded file to try and identify dodgy content.
     *
     * @param string         $filename          is the full path+name to the uploaded file on the server
     * @param string         $target_name       is the intended name of the file once transferred
     * @param array          $allowed_filetypes is an array of permitted file extensions, in lower case, no leading '.'
     *                                          (usually generated from filetypes.xml/filetypes.php)
     * @param boolean|string $unknown           - handling of file types unknown to us/define additional types
     *                                          if FALSE, rejects totally unknown file extensions (even if in $allowed_filetypes).
     *                                          if $unknown is TRUE, accepts totally unknown file extensions.
     *                                          otherwise $unknown is a comma-separated list of additional permitted file extensions
     * @return boolean - TRUE if file acceptable, FALSE if unacceptable. Use getErrorCode() immediately after to retrieve error code:
     *                                          1 - file type not allowed
     *                                          2 - can't read file contents
     *                                          3 - illegal file contents (usually '<?php')
     *                                          4 - not an image file
     *                                          5 - bad image parameters - REMOVED
     *                                          6 - not in supplementary list
     *                                          7 - suspicious file contents
     *                                          8 - unknown file type
     *                                          9 - unacceptable file type (prone to exploits)
     */
    function isClean($filename, $target_name = '', $allowed_filetypes = array(), $unknown = false)
    {

        if(empty($target_name)) // no temp file, just use the filename.
        {
            $target_name = $filename;
        }

        $this->setErrorNum(null);
        // 1. Start by checking against filetypes - that's the easy one!
        $file_ext = pathinfo($target_name, PATHINFO_EXTENSION);

        $file_ext = strtolower($file_ext);

        // 2. For all files, read the first little bit to check for any flags etc
        $res = fopen($filename, 'rb');
        $tstr = fread($res, 2048);
        fclose($res);

        if($tstr === false)
        {
            $this->setErrorNum(2); // If can't read file, not much use carrying on!

            return false;
        }

        $archives = array('zip', 'gzip', 'gz', 'tar', 'bzip', '7z', 'rar');

        if(!in_array($file_ext, $archives) && stripos($tstr, '<?php') !== false)
        {
            $this->setErrorNum(3); // Pretty certain exploit

            return false;
        }

        if(!in_array($file_ext, $archives) && strpos($tstr, '<?') !== false)                // Bit more tricky - can sometimes be OK
        {
            if(stripos($tstr, '<?xpacket') === false && stripos($tstr, '<?xml ') === false)    // Allow the XMP header produced by CS4 and xml files.
            {
                $this->setErrorNum(7);

                return false;
            }
        }

        // 3. Now do what we can based on file extension
        switch($file_ext)
        {

            case 'jpg':
            case 'gif':
            case 'png':
            case 'jpeg':
            case 'pjpeg':
            case 'bmp':
            case 'swf':
            case 'fla':
                //        case 'flv':
            case 'swc':
            case 'psd':
            case 'ai':
            case 'eps':
            case 'svg':
            case 'tiff':
            case 'jpc': // http://fileinfo.com/extension/jpc
            case 'jpx': // http://fileinfo.com/extension/jpx
            case 'jb2': // http://fileinfo.com/extension/jb2
            case 'jp2': // http://fileinfo.com/extension/jp2
            case 'iff':
            case 'wbmp':
            case 'xbm':
            case 'ico':
            case 'webp':

                $ret = $this->getImageMime($filename);

                if($ret === false)
                {
                    $this->setErrorNum(4);  // exif_imagetype didn't recognize the image mime

                    return false;
                }

                // getimagesize() is extremely slow + it can't handle all required media!!! Abandon this check!
                //    return 5; // Zero size picture or bad file format
                break;

            case 'zip':
            case 'gzip':
            case 'gz':
            case 'tar':
            case 'bzip':
            case 'pdf':
            case 'doc':
            case 'docx':
            case 'xls':
            case 'xlsx':
            case 'rar':
            case '7z':
            case 'csv':
            case 'mp3':
            case 'wav':
            case 'mp4':
            case 'mpg':
            case 'mpa':
            case 'wma':
            case 'wmv':
            case 'flv': //Flash stream
            case 'f4v': //Flash stream
            case 'mov': //media
            case 'avi': //media
            case 'xml':
            case 'webm':
            case 'ppt':
            case 'pptx':

                break; // Just accept these

            case 'php':
            case 'php5':
            case 'php7':
            case 'htm':
            case 'html':
            case 'cgi':
            case 'pl':

                $this->setErrorNum(9); // Never accept these! Whatever the user thinks!

                return false;

            default: // Unknown file type.

                $this->setErrorNum(8);

                return false;
        }

        return true; // Accepted here
    }


    /**
     * New in v2.1.9
     * Check filename, path or URL against filetypes.xml
     *
     * @param        $file - real path to file.
     * @param string $targetFile
     * @return boolean
     */
    public function isAllowedType($file, $targetFile = '')
    {

        if(empty($targetFile))
        {
            $targetFile = $file;
        }

        $remote = false;

        if(strpos($targetFile, 'http') === 0) // remote file.
        {
            $tmp = parse_url($targetFile);
            $targetFile = $tmp['path'];
            $remote = true;
            if(!empty($tmp['host']) && ($tmp['host'] === 'localhost' || $tmp['host'] === '127.0.0.1'))
            {
                return false;
            }
        }

        $ext = pathinfo($targetFile, PATHINFO_EXTENSION);

        $types = $this->getAllowedFileTypes();

        if(isset($types[$ext]))
        {
            if($remote)
            {
                return true;
            }

            $maxSize = $types[$ext] * 1024;
            $fileSize = filesize($file);

            //    echo "\nisAllowedType(".basename($file).") ".$fileSize ." / ".$maxSize;

            if($fileSize <= $maxSize)
            {
                return true;
            }

        }

        return false;

    }

    /**
     * @return string[]
     */
    private function getMimeTypes()
    {

        return array(
            'asc'  => 'text/plain',
            'css'  => 'text/css',
            'csv'  => 'text/csv',
            'etx'  => 'text/x-setext',
            'htm'  => 'text/html',
            'html' => 'text/html',
            'ics'  => 'text/calendar',
            'ini'  => 'text/plain',
            'log'  => 'text/plain',
            'php'  => 'text/html',
            'sgm'  => 'text/sgml',
            'sgml' => 'text/sgml',
            'txt'  => 'text/plain',
            'yaml' => 'text/yaml',
            'yml'  => 'text/yaml',


            // images
            'bmp'  => 'image/bmp',
            'gif'  => 'image/gif',
            'ico'  => 'image/vnd.microsoft.icon',
            'jpe'  => 'image/jpeg',
            'jpeg' => 'image/jpeg',
            'jpg'  => 'image/jpeg',
            'pbm'  => 'image/x-portable-bitmap',
            'pgm'  => 'image/x-portable-graymap',
            'png'  => 'image/png',
            'pnm'  => 'image/x-portable-anymap',
            'ppm'  => 'image/x-portable-pixmap',
            'ras'  => 'image/x-cmu-raster',
            'svg'  => 'image/svg+xml',
            'svgz' => 'image/svg+xml',
            'tif'  => 'image/tiff',
            'tiff' => 'image/tiff',
            'webp' => 'image/webp',
            'xbm'  => 'image/x-xbitmap',
            'xpm'  => 'image/x-xpixmap',
            'xwd'  => 'image/x-xwindowdump',

            // archives
            '7z'   => 'application/x-7z-compressed',
            'cab'  => 'application/vnd.ms-cab-compressed',
            'exe'  => 'application/x-msdownload',
            'gz'   => 'application/gzip',
            'msi'  => 'application/x-msdownload',
            'rar'  => 'application/x-rar-compressed',
            'zip'  => 'application/zip',


            // video

            '3gp'     => 'video/3gpp',
            'asf'     => 'video/x-ms-asf',
            'avi'     => 'video/x-msvideo',
            'flv'     => 'video/x-flv',
            'm4v'     => 'video/mp4',
            'mkv'     => 'video/x-matroska',
            'mov'     => 'video/quicktime',
            'mp4'     => 'video/mp4',
            'mp4v'    => 'video/mp4',
            'mpe'     => 'video/mpeg',
            'mpeg'    => 'video/mpeg',
            'mpg'     => 'video/mpeg',
            'mpg4'    => 'video/mp4',
            'ogv'     => 'video/ogg',
            'qt'      => 'video/quicktime',
            'webm'    => 'video/webm',
            'wmv'     => 'video/x-ms-wmv',

            // audio
            'aac'     => 'audio/x-aac',
            'aif'     => 'audio/x-aiff',
            'flac'    => 'audio/flac',
            'm4a'     => 'audio/mp4',
            'mid'     => 'audio/midi',
            'midi'    => 'audio/midi',
            'mp3'     => 'audio/mpeg',
            'mp4a'    => 'audio/mp4',
            'oga'     => 'audio/ogg',
            'ogg'     => 'audio/ogg',
            'wav'     => 'audio/x-wav',
            'wma'     => 'audio/x-ms-wma',

            // adobe
            'ai'      => 'application/postscript',
            'eps'     => 'application/postscript',
            'pdf'     => 'application/pdf',
            'ps'      => 'application/postscript',
            'psd'     => 'image/vnd.adobe.photoshop',

            // ms office
            'doc'     => 'application/msword',
            'docx'    => 'application/msword',
            'ppt'     => 'application/vnd.ms-powerpoint',
            'pptx'    => 'application/vnd.ms-powerpoint',
            'rtf'     => 'application/rtf',
            'xls'     => 'application/vnd.ms-excel',
            'xlsx'    => 'application/vnd.ms-excel',

            // open office
            'odt'     => 'application/vnd.oasis.opendocument.text',
            'ods'     => 'application/vnd.oasis.opendocument.spreadsheet',

            // other
            'atom'    => 'application/atom+xml',
            'bz2'     => 'application/x-bzip2',
            'cer'     => 'application/pkix-cert',
            'crl'     => 'application/pkix-crl',
            'crt'     => 'application/x-x509-ca-cert',
            'cu'      => 'application/cu-seeme',
            'deb'     => 'application/x-debian-package',
            'dvi'     => 'application/x-dvi',
            'eot'     => 'application/vnd.ms-fontobject',
            'epub'    => 'application/epub+zip',
            'iso'     => 'application/x-iso9660-image',
            'jar'     => 'application/java-archive',
            'js'      => 'application/javascript',
            'json'    => 'application/json',
            'latex'   => 'application/x-latex',
            'ogx'     => 'application/ogg',
            'rss'     => 'application/rss+xml',
            'swf'     => 'application/x-shockwave-flash',
            'tar'     => 'application/x-tar',
            'torrent' => 'application/x-bittorrent',
            'ttf'     => 'application/x-font-ttf',
            'woff'    => 'application/x-font-woff',
            'wsdl'    => 'application/wsdl+xml',
            'xml'     => 'application/xml',

        );

    }


    /**
     * Return the mime-type based on the file's extension.
     *
     * @param string $filename
     * @return string
     */
    public function getMime($filename)
    {

        $filename = basename($filename);

        $tmp = explode('.', $filename);

        if(count($tmp) < 2) // no extension.
        {
            return false;
        }

        $ext = strtolower(end($tmp));

        $types = $this->getMimeTypes();

        if(isset($types[$ext]))
        {
            return $types[$ext];
        }
        else
        {
            return 'application/octet-stream';
        }


    }


    /**
     * New in v2.1.9
     * Get image (string) mime type
     * or when extended - array [(string) mime-type, (array) associated extensions)].
     * A much faster way to retrieve mimes than getimagesize()
     *
     * @param            $filename
     * @param bool|false $extended
     * @return array|string
     */
    function getImageMime($filename, $extended = false)
    {

        // mime types as returned from image_type_to_mime_type()
        // and associated file extensions
        $imageExtensions = array(
            'image/gif'                     => array('gif'),
            'image/jpeg'                    => array('jpg'),
            'image/png'                     => array('png'),
            'application/x-shockwave-flash' => array('swf', 'swc'),
            'image/psd'                     => array('psd'),
            'image/bmp'                     => array('bmp'),
            'image/tiff'                    => array('tiff'),
            'application/octet-stream'      => array('jpc', 'jpx', 'jb2'),
            'image/jp2'                     => array('jp2'),
            'image/iff'                     => array('iff'),
            'image/vnd.wap.wbmp'            => array('wbmp'),
            'image/xbm'                     => array('xbm'),
            'image/vnd.microsoft.icon'      => array('ico'),
            'image/webp'                    => array('webp'),
        );

        $ret = image_type_to_mime_type(exif_imagetype($filename));

        if($extended)
        {
            return array(
                $ret,
                $ret && isset($imageExtensions[$ret]) ? $imageExtensions[$ret] : array()
            );
        }

        return $ret;

    }


    /**
     *    New in v2.1.9
     *  Get array of file types (file extensions) which are permitted - reads an XML-formatted definition file.
     *    (Similar to @See{get_allowed_filetypes()}, but expects an XML file)
     *
     * @param string $class - e_UC_MEMBER etc if a specific class of file-types is required. Otherwise, it defaults to the perms of the current user.
     * @return array - where key is the file type (extension); value is max upload size
     */
    public function getAllowedFileTypes($class = null)
    {

        $ret = array();
        $file_array = array();

        /*        if($file_mask)
                {
                    $file_array = explode(',', $file_mask);
                    foreach($file_array as $k => $f)
                    {
                        $file_array[$k] = trim($f);
                    }
                }*/

        if(!is_readable(e_SYSTEM . "filetypes.xml"))
        {
            return array();
        }

        $xml = e107::getXml();
        $xml->setOptArrayTags('class'); // class tag should be always array
        $temp_vars = $xml->loadXMLfile(e_SYSTEM . "filetypes.xml", 'filetypes');

        if($temp_vars === false)
        {
            echo "Error reading filetypes.xml<br />";

            return $ret;
        }

        foreach($temp_vars['class'] as $v1)
        {
            $v = $v1['@attributes'];

            if(!is_numeric($v['name']))
            {
                $v['name'] = e107::getUserClass()->getClassFromKey($v['name'], $v['name']); // convert 'admin' etc to numeric equivalent.
            }

            if(($class === null && check_class($v['name'])) || (int) $class === (int) $v['name'])
            {
                $current_perms[$v['name']] = array('type' => $v['type'], 'maxupload' => $v['maxupload']);
                $a_filetypes = explode(',', $v['type']);
                foreach($a_filetypes as $ftype)
                {
                    $ftype = strtolower(trim(str_replace('.', '', $ftype))); // File extension

                    //    if(!$file_mask || in_array($ftype, $file_array)) // We can load this extension
                    {
                        if(isset($ret[$ftype]))
                        {
                            $ret[$ftype] = $this->file_size_decode($v['maxupload'], $ret[$ftype], 'gt'); // Use largest value
                        }
                        else
                        {
                            $ret[$ftype] = $this->file_size_decode($v['maxupload']);
                        }
                    }
                }
            }
        }

        return $ret;
    }


}