<?php
/**
 * The Horde_Util:: class provides generally useful methods.
 *
 * Copyright 1999-2016 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
 *
 * @author   Chuck Hagenbuch <chuck@horde.org>
 * @author   Jon Parise <jon@horde.org>
 * @category Horde
 * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
 * @package  Util
 */
class Horde_Util
{
    /**
     * A list of random patterns to use for overwriting purposes.
     * See http://www.cs.auckland.ac.nz/~pgut001/pubs/secure_del.html.
     * We save the random overwrites for efficiency reasons.
     *
     * @var array
     */
    public static $patterns = array(
        "\x55", "\xaa", "\x92\x49\x24", "\x49\x24\x92", "\x24\x92\x49",
        "\x00", "\x11", "\x22", "\x33", "\x44", "\x55", "\x66", "\x77",
        "\x88", "\x99", "\xaa", "\xbb", "\xcc", "\xdd", "\xee", "\xff",
        "\x92\x49\x24", "\x49\x24\x92", "\x24\x92\x49", "\x6d\xb6\xdb",
        "\xb6\xdb\x6d", "\xdb\x6d\xb6"
    );

    /**
     * Are magic quotes in use?
     *
     * @var boolean
     */
    protected static $_magicquotes = null;

    /**
     * Data used to determine shutdown deletion.
     *
     * @var array
     */
    protected static $_shutdowndata = array(
        'paths' => array(),
        'secure' => array()
    );

    /**
     * Has the shutdown method been registered?
     *
     * @var boolean
     */
    protected static $_shutdownreg = false;

    /**
     * Cache for extensionExists().
     *
     * @var array
     */
    protected static $_cache = array();

    /**
     * Checks to see if a value has been set by the script and not by GET,
     * POST, or cookie input. The value being checked MUST be in the global
     * scope.
     *
     * @param string $varname  The variable name to check.
     * @param mixed $default   Default value if the variable isn't present
     *                         or was specified by the user. Defaults to null.
     *
     * @return mixed  $default if the var is in user input or not present,
     *                the variable value otherwise.
     */
    public static function nonInputVar($varname, $default = null)
    {
        return (isset($_GET[$varname]) || isset($_POST[$varname]) || isset($_COOKIE[$varname]))
            ? $default
            : (isset($GLOBALS[$varname]) ? $GLOBALS[$varname] : $default);
    }

    /**
     * Returns a hidden form input containing the session name and id.
     *
     * @param boolean $append_session  0 = only if needed, 1 = always.
     *
     * @return string  The hidden form input, if needed/requested.
     */
    public static function formInput($append_session = 0)
    {
        return (($append_session == 1) || !isset($_COOKIE[session_name()]))
            ? '<input type="hidden" name="' . htmlspecialchars(session_name()) . '" value="' . htmlspecialchars(session_id()) . "\" />\n"
            : '';
    }

    /**
     * Prints a hidden form input containing the session name and id.
     *
     * @param boolean $append_session  0 = only if needed, 1 = always.
     */
    public static function pformInput($append_session = 0)
    {
        echo self::formInput($append_session);
    }

    /**
     * If magic_quotes_gpc is in use, run stripslashes() on $var.
     *
     * @param mixed $var  The string, or an array of strings, to un-quote.
     *
     * @return mixed  $var, minus any magic quotes.
     */
    public static function dispelMagicQuotes($var)
    {
        if (is_null(self::$_magicquotes)) {
            self::$_magicquotes = get_magic_quotes_gpc();
        }

        if (self::$_magicquotes) {
            $var = is_array($var)
                ? array_map(array(__CLASS__, 'dispelMagicQuotes'), $var)
                : stripslashes($var);
        }

        return $var;
    }

    /**
     * Gets a form variable from GET or POST data, stripped of magic quotes if
     * necessary. If the variable is somehow set in both the GET data and the
     * POST data, the value from the POST data will be returned and the GET
     * value will be ignored.
     *
     * @param string $var      The name of the form variable to look for.
     * @param string $default  The value to return if the variable is not
     *                         there.
     *
     * @return string  The cleaned form variable, or $default.
     */
    public static function getFormData($var, $default = null)
    {
        return (($val = self::getPost($var)) !== null)
            ? $val
            : self::getGet($var, $default);
    }

    /**
     * Gets a form variable from GET data, stripped of magic quotes if
     * necessary. This function will NOT return a POST variable.
     *
     * @param string $var      The name of the form variable to look for.
     * @param string $default  The value to return if the variable is not
     *                         there.
     *
     * @return string  The cleaned form variable, or $default.
     */
    public static function getGet($var, $default = null)
    {
        return (isset($_GET[$var]))
            ? self::dispelMagicQuotes($_GET[$var])
            : $default;
    }

    /**
     * Gets a form variable from POST data, stripped of magic quotes if
     * necessary. This function will NOT return a GET variable.
     *
     * @param string $var      The name of the form variable to look for.
     * @param string $default  The value to return if the variable is not
     *                         there.
     *
     * @return string  The cleaned form variable, or $default.
     */
    public static function getPost($var, $default = null)
    {
        return (isset($_POST[$var]))
            ? self::dispelMagicQuotes($_POST[$var])
            : $default;
    }

    /**
     * Creates a temporary filename for the lifetime of the script, and
     * (optionally) registers it to be deleted at request shutdown.
     *
     * @param string $prefix   Prefix to make the temporary name more
     *                         recognizable.
     * @param boolean $delete  Delete the file at the end of the request?
     * @param string $dir      Directory to create the temporary file in.
     * @param boolean $secure  If deleting the file, should we securely delete
     *                         the file by overwriting it with random data?
     *
     * @return string   Returns the full path-name to the temporary file.
     *                  Returns false if a temp file could not be created.
     */
    public static function getTempFile($prefix = '', $delete = true, $dir = '',
                                       $secure = false)
    {
        $tempDir = (empty($dir) || !is_dir($dir))
            ? sys_get_temp_dir()
            : $dir;

        $tempFile = tempnam($tempDir, $prefix);

        // If the file was created, then register it for deletion and return.
        if (empty($tempFile)) {
            return false;
        }

        if ($delete) {
            self::deleteAtShutdown($tempFile, true, $secure);
        }

        return $tempFile;
    }

    /**
     * Creates a temporary filename with a specific extension for the lifetime
     * of the script, and (optionally) registers it to be deleted at request
     * shutdown.
     *
     * @param string $extension  The file extension to use.
     * @param string $prefix     Prefix to make the temporary name more
     *                           recognizable.
     * @param boolean $delete    Delete the file at the end of the request?
     * @param string $dir        Directory to create the temporary file in.
     * @param boolean $secure    If deleting file, should we securely delete
     *                           the file by overwriting it with random data?
     *
     * @return string   Returns the full path-name to the temporary file.
     *                  Returns false if a temporary file could not be created.
     */
    public static function getTempFileWithExtension($extension = '.tmp',
                                                    $prefix = '',
                                                    $delete = true, $dir = '',
                                                    $secure = false)
    {
        $tempDir = (empty($dir) || !is_dir($dir))
            ? sys_get_temp_dir()
            : $dir;

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

        $windows = substr(PHP_OS, 0, 3) == 'WIN';
        $tries = 1;
        do {
            // Get a known, unique temporary file name.
            $sysFileName = tempnam($tempDir, $prefix);
            if ($sysFileName === false) {
                return false;
            }

            // tack on the extension
            $tmpFileName = $sysFileName . $extension;
            if ($sysFileName == $tmpFileName) {
                return $sysFileName;
            }

            // Move or point the created temporary file to the full filename
            // with extension. These calls fail if the new name already
            // exists.
            $fileCreated = ($windows ? @rename($sysFileName, $tmpFileName) : @link($sysFileName, $tmpFileName));
            if ($fileCreated) {
                if (!$windows) {
                    unlink($sysFileName);
                }

                if ($delete) {
                    self::deleteAtShutdown($tmpFileName, true, $secure);
                }

                return $tmpFileName;
            }

            unlink($sysFileName);
        } while (++$tries <= 5);

        return false;
    }

    /**
     * Creates a temporary directory in the system's temporary directory.
     *
     * @param boolean $delete   Delete the temporary directory at the end of
     *                          the request?
     * @param string $temp_dir  Use this temporary directory as the directory
     *                          where the temporary directory will be created.
     *
     * @return string  The pathname to the new temporary directory.
     *                 Returns false if directory not created.
     */
    public static function createTempDir($delete = true, $temp_dir = null)
    {
        if (is_null($temp_dir)) {
            $temp_dir = sys_get_temp_dir();
        }

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

        /* Get the first 8 characters of a random string to use as a temporary
           directory name. */
        do {
            $new_dir = $temp_dir . '/' . substr(base_convert(uniqid(mt_rand()), 10, 36), 0, 8);
        } while (file_exists($new_dir));

        $old_umask = umask(0000);
        if (!mkdir($new_dir, 0700)) {
            $new_dir = false;
        } elseif ($delete) {
            self::deleteAtShutdown($new_dir);
        }
        umask($old_umask);

        return $new_dir;
    }

    /**
     * Returns the canonical path of the string.  Like PHP's built-in
     * realpath() except the directory need not exist on the local server.
     *
     * Algorithim loosely based on code from the Perl File::Spec::Unix module
     * (version 1.5).
     *
     * @param string $path  A file path.
     *
     * @return string  The canonicalized file path.
     */
    public static function realPath($path)
    {
        /* Standardize on UNIX directory separators. */
        if (!strncasecmp(PHP_OS, 'WIN', 3)) {
            $path = str_replace('\\', '/', $path);
        }

        /* xx////xx -> xx/xx
         * xx/././xx -> xx/xx */
        $path = preg_replace(array("|/+|", "@(/\.)+(/|\Z(?!\n))@"), array('/', '/'), $path);

        /* ./xx -> xx */
        if ($path != './') {
            $path = preg_replace("|^(\./)+|", '', $path);
        }

        /* /../../xx -> xx */
        $path = preg_replace("|^/(\.\./?)+|", '/', $path);

        /* xx/ -> xx */
        if ($path != '/') {
            $path = preg_replace("|/\Z(?!\n)|", '', $path);
        }

        /* /xx/.. -> / */
        while (strpos($path, '/..') !== false) {
            $path = preg_replace("|/[^/]+/\.\.|", '', $path);
        }

        return empty($path) ? '/' : $path;
    }

    /**
     * Removes given elements at request shutdown.
     *
     * If called with a filename will delete that file at request shutdown; if
     * called with a directory will remove that directory and all files in that
     * directory at request shutdown.
     *
     * If called with no arguments, return all elements to be deleted (this
     * should only be done by Horde_Util::_deleteAtShutdown()).
     *
     * The first time it is called, it initializes the array and registers
     * Horde_Util::_deleteAtShutdown() as a shutdown function - no need to do
     * so manually.
     *
     * The second parameter allows the unregistering of previously registered
     * elements.
     *
     * @param string $filename   The filename to be deleted at the end of the
     *                           request.
     * @param boolean $register  If true, then register the element for
     *                           deletion, otherwise, unregister it.
     * @param boolean $secure    If deleting file, should we securely delete
     *                           the file?
     */
    public static function deleteAtShutdown($filename, $register = true,
                                            $secure = false)
    {
        /* Initialization of variables and shutdown functions. */
        if (!self::$_shutdownreg) {
            register_shutdown_function(array(__CLASS__, 'shutdown'));
            self::$_shutdownreg = true;
        }

        $ptr = &self::$_shutdowndata;
        if ($register) {
            $ptr['paths'][$filename] = true;
            if ($secure) {
                $ptr['secure'][$filename] = true;
            }
        } else {
            unset($ptr['paths'][$filename], $ptr['secure'][$filename]);
        }
    }

    /**
     * Deletes registered files at request shutdown.
     *
     * This function should never be called manually; it is registered as a
     * shutdown function by Horde_Util::deleteAtShutdown() and called
     * automatically at the end of the request.
     *
     * Contains code from gpg_functions.php.
     * Copyright 2002-2003 Braverock Ventures
     */
    public static function shutdown()
    {
        $ptr = &self::$_shutdowndata;

        foreach (array_keys($ptr['paths']) as $val) {
            if (@is_file($val)) {
                self::_secureDelete($val);
                continue;
            }

            try {
                $it = new RecursiveIteratorIterator(
                    new RecursiveDirectoryIterator($val),
                    RecursiveIteratorIterator::CHILD_FIRST
                );
            } catch (UnexpectedValueException $e) {
                continue;
            }

            while ($it->valid()) {
                if (!$it->isDot()) {
                    if ($it->isDir()) {
                        @rmdir($it->key());
                    } elseif ($it->isFile()) {
                        self::_secureDelete($it->key());
                    } else {
                        @unlink($it->key());
                    }
                }
                $it->next();
            }

            @rmdir($val);
        }
    }

    /**
     * Securely delete the file by overwriting the data with a random
     * string.
     *
     * @param string $file  Filename.
     */
    protected static function _secureDelete($file)
    {
        if (isset($ptr['secure'][$file])) {
            $filesize = filesize($file);
            $fp = fopen($file, 'r+');
            foreach (self::$patterns as $pattern) {
                $pattern = substr(str_repeat($pattern, floor($filesize / strlen($pattern)) + 1), 0, $filesize);
                fwrite($fp, $pattern);
                fseek($fp, 0);
            }
            fclose($fp);
        }

        @unlink($file);
    }

    /**
     * Caches the result of extension_loaded() calls.
     *
     * @param string $ext  The extension name.
     *
     * @return boolean  Is the extension loaded?
     */
    public static function extensionExists($ext)
    {
        if (!isset(self::$_cache[$ext])) {
            self::$_cache[$ext] = extension_loaded($ext);
        }

        return self::$_cache[$ext];
    }

    /**
     * Tries to load a PHP extension, behaving correctly for all operating
     * systems.
     *
     * @param string $ext  The extension to load.
     *
     * @return boolean  True if the extension is now loaded, false if not.
     *                  True can mean that the extension was already loaded,
     *                  OR was loaded dynamically.
     */
    public static function loadExtension($ext)
    {
        /* If $ext is already loaded, our work is done. */
        if (self::extensionExists($ext)) {
            return true;
        }

        /* See if we can call dl() at all, by the current ini settings.
         * dl() has been removed in some PHP 5.3 SAPIs. */
        if ((ini_get('enable_dl') != 1) ||
            (ini_get('safe_mode') == 1) ||
            !function_exists('dl')) {
            return false;
        }

        if (!strncasecmp(PHP_OS, 'WIN', 3)) {
            $suffix = 'dll';
        } else {
            switch (PHP_OS) {
            case 'HP-UX':
                $suffix = 'sl';
                break;

            case 'AIX':
                $suffix = 'a';
                break;

            case 'OSX':
                $suffix = 'bundle';
                break;

            default:
                $suffix = 'so';
            }
        }

        return dl($ext . '.' . $suffix) || dl('php_' . $ext . '.' . $suffix);
    }

    /**
     * Utility function to obtain PATH_INFO information.
     *
     * @return string  The PATH_INFO string.
     */
    public static function getPathInfo()
    {
        if (isset($_SERVER['PATH_INFO']) &&
            (strpos($_SERVER['SERVER_SOFTWARE'], 'lighttpd') === false)) {
            return $_SERVER['PATH_INFO'];
        } elseif (isset($_SERVER['REQUEST_URI']) &&
                  isset($_SERVER['SCRIPT_NAME'])) {
            $search = Horde_String::common($_SERVER['SCRIPT_NAME'], $_SERVER['REQUEST_URI']);
            if (substr($search, -1) == '/') {
                $search = substr($search, 0, -1);
            }
            $search = array($search);
            if (!empty($_SERVER['QUERY_STRING'])) {
                // We can't use QUERY_STRING directly because URL rewriting
                // might add more parameters to the query string than those
                // from the request URI.
                $url = parse_url($_SERVER['REQUEST_URI']);
                if (!empty($url['query'])) {
                    $search[] = '?' . $url['query'];
                }
            }
            $path = str_replace($search, '', $_SERVER['REQUEST_URI']);
            if ($path == '/') {
                $path = '';
            }
            return $path;
        }

        return '';
    }

}
