<?php // rcs_id('$Id: FileFinder.php 7692 2010-09-17 11:32:55Z rurban $');

require_once(dirname(__FILE__).'/stdlib.php');

/**
 * A class for finding files.
 * 
 * This should really provided by pear. We don't want really to mess around 
 * with all the lousy systems. (WindowsNT, Win95, Mac, ...)
 * But pear has only System and File, which do nothing.
 * Anyway, in good PHP style we ignore the rest of the world and try to behave 
 * as on unix only. That means we use / as pathsep in all our constants.
 */
class FileFinder
{
    //var $_pathsep, $_path;

    /**
     * Constructor.
     *
     * @param $path array A list of directories in which to search for files.
     */
    function FileFinder ($path = false) {
        $this->_pathsep = $this->_get_syspath_separator();
        if (!isset($this->_path) and $path === false)
            $path = $this->_get_include_path();
        $this->_path = $path;
    }

    /**
     * Find file.
     *
     * @param $file string File to search for.
     * @return string The filename (including path), if found, otherwise false.
     */
    function findFile ($file, $missing_okay = false) {
        if ($this->_is_abs($file)) {
            if (file_exists($file))
                return $file;
        }
        elseif ( ($dir = $this->_search_path($file)) ) {
            return $dir . $this->_use_path_separator($dir) . $file;
        }
        return $missing_okay ? false : $this->_not_found($file);
    }

    /**
     * Unify used pathsep character.
     * Accepts array of paths also.
     * This might not work on Windows95 or FAT volumes. (not tested)
     */
    function slashifyPath ($path) {
        return $this->forcePathSlashes($path, $this->_pathsep);
    }

    /**
     * Force using '/' as path seperator.
     */
    function forcePathSlashes ($path, $sep='/') {
    	if (is_array($path)) {
    	    $result = array();
    	    foreach ($path as $dir) { $result[] = $this->forcePathSlashes($dir,$sep); }
    	    return $result;
    	} else {
            if (isWindows() or $this->_isOtherPathsep()) {
                if (isWindows()) $from = "\\";
                else $from = "\\";
                // PHP is stupid enough to use \\ instead of \
                if (isWindows()) {
                    if (substr($path,0,2) != '\\\\')
                        $path = str_replace('\\\\','\\',$path);
                    else // UNC paths
                        $path = '\\\\' . str_replace('\\\\','\\',substr($path,2));
                }
                return strtr($path, $from, $sep);
            } else 
                return $path;
    	}
    }

    /**
     * Try to include file.
     *
     * If file is found in the path, then the files directory is added
     * to PHP's include_path (if it's not already there.) Then the
     * file is include_once()'d.
     *
     * @param $file string File to include.
     * @return bool True if file was successfully included.
     */
    function includeOnce ($file) {
        if ( ($ret = @include_once($file)) )
            return $ret;

        if (!$this->_is_abs($file)) {
            if ( ($dir = $this->_search_path($file)) && is_file($dir . $this->_pathsep . $file)) {
                $this->_append_to_include_path($dir);
                return include_once($file);
            }
        }
        return $this->_not_found($file);
    }

    function _isOtherPathsep() {
        return $this->_pathsep != '/';
    }

    /**
     * The system-dependent path-separator character. 
     * UNIX,WindowsNT,MacOSX: /
     * Windows95: \
     * Mac:       :
     *
     * @access private
     * @return string path_separator.
     */
    function _get_syspath_separator () {
    	if (!empty($this->_pathsep)) return $this->_pathsep;
        elseif (isWindowsNT()) return "/"; // we can safely use '/'
        elseif (isWindows()) return "\\";  // FAT might use '\'
        // VMS or LispM is really weird, we ignore it.
        else return '/';
    }

    /**
     * The path-separator character of the given path. 
     * Windows accepts "/" also, but gets confused with mixed path_separators,
     * e.g "C:\Apache\phpwiki/locale/button"
     * > dir "C:\Apache\phpwiki/locale/button" => 
     *       Parameterformat nicht korrekt - "locale"
     * So if there's any '\' in the path, either fix them to '/' (not in Win95 or FAT?) 
     * or use '\' for ours.
     *
     * @access private
     * @return string path_separator.
     */
    function _use_path_separator ($path) {
        if (isWindows95()) {
            if (empty($path)) return "\\";
            else return (strchr($path,"\\")) ? "\\" : '/';
        } else {
            return $this->_get_syspath_separator();
        }
    }

    /**
     * Determine if path is absolute.
     *
     * @access private
     * @param $path string Path.
     * @return bool True if path is absolute. 
     */
    function _is_abs($path) {
        if (substr($path,0,1) == '/') {
            return true;
        } elseif (isWindows() and preg_match("/^[a-z]:/i", $path)
                  and (substr($path,2,1) == "/" or substr($path,2,1) == "\\")) 
        {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Strip ending '/' or '\' from path.
     *
     * @access private
     * @param $path string Path.
     * @return bool New path (destructive)
     */
    function _strip_last_pathchar(&$path) {
        if (substr($path,-1) == '/' or substr($path,-1) == "\\") 
            $path = substr($path,0,-1);
        return $path;
    }

    /**
     * Report a "file not found" error.
     *
     * @access private
     * @param $file string Name of missing file.
     * @return bool false.
     */
    function _not_found($file) {
        if (function_exists("_"))
            trigger_error(sprintf(_("%s: file not found"), $file), E_USER_ERROR);
        else
            trigger_error(sprintf("%s: file not found", $file), E_USER_ERROR);
        return false;
    }


    /**
     * Search our path for a file.
     *
     * @access private
     * @param $file string File to find.
     * @return string Directory which contains $file, or false.
     * [5x,44ms]
     */
    function _search_path ($file) {
        foreach ($this->_path as $dir) {
            // ensure we use the same pathsep
            if ($this->_isOtherPathsep()) {
            	$dir = $this->slashifyPath($dir);
            	$file = $this->slashifyPath($file);
                if (file_exists($dir . $this->_pathsep . $file))
                    return $dir;
            } elseif (@file_exists($dir . $this->_pathsep . $file))
               	return $dir;
        }
        return false;
    }

    /**
     * The system-dependent path-separator character. On UNIX systems,
     * this character is ':'; on Win32 systems it is ';'.
     * Fixme:
     * On Mac it cannot be : because this is the seperator there!
     *
     * @access private
     * @return string path_separator.
     */
    function _get_ini_separator () {
        return isWindows() ? ';' : ':';
        // return preg_match('/^Windows/', php_uname()) 
    }

    /**
     * Get the value of PHP's include_path.
     *
     * @access private
     * @return array Include path.
     */
    function _get_include_path() {
        if (defined("INCLUDE_PATH"))
            $path = INCLUDE_PATH;
        else {
            $path = @get_cfg_var('include_path'); // FIXME: report warning
            if (empty($path)) $path = @ini_get('include_path');
        }
        if (empty($path))
            $path = '.';
        return explode($this->_get_ini_separator(), $this->slashifyPath($path));
    }

    /**
     * Add a directory to the end of PHP's include_path.
     *
     * The directory is appended only if it is not already listed in
     * the include_path.
     *
     * @access private
     * @param $dir string Directory to add.
     */
    function _append_to_include_path ($dir) {
        $dir = $this->slashifyPath($dir);
        if (!in_array($dir, $this->_path)) {
            $this->_path[] = $dir;
        }
        /*
         * Some (buggy) PHP's (notable SourceForge's PHP 4.0.6)
         * sometimes don't seem to heed their include_path.
         * I.e. sometimes a file is not found even though it seems to
         * be in the current include_path. A simple
         * ini_set('include_path', ini_get('include_path')) seems to
         * be enough to fix the problem
         *
         * This following line should be in the above if-block, but we
         * put it here, as it seems to work-around the bug.
         */
        $GLOBALS['INCLUDE_PATH'] = implode($this->_get_ini_separator(), $this->_path);
        @ini_set('include_path', $GLOBALS['INCLUDE_PATH']);
    }

    /**
     * Add a directory to the front of PHP's include_path.
     *
     * The directory is prepended, and removed from the tail if already existing.
     *
     * @access private
     * @param $dir string Directory to add.
     */
    function _prepend_to_include_path ($dir) {
        $dir = $this->slashifyPath($dir);
        // remove duplicates
        if ($i = array_search($dir, $this->_path) !== false) {
            array_splice($this->_path, $i, 1);
        }
        array_unshift($this->_path, $dir);
        $GLOBALS['INCLUDE_PATH'] = implode($this->_path, $this->_get_ini_separator());
        @ini_set('include_path', $GLOBALS['INCLUDE_PATH']);
    }

    // Return all the possible shortened locale specifiers for the given locale.
    // Most specific first.
    // de_DE.iso8859-1@euro => de_DE.iso8859-1, de_DE, de
    // This code might needed somewhere else also.
    function locale_versions ($lang) {
        // Try less specific versions of the locale
        $langs[] = $lang;
        foreach (array('@', '.', '_') as $sep) {
            if ( ($tail = strchr($lang, $sep)) )
                $langs[] = substr($lang, 0, -strlen($tail));
        }
        return $langs;
    }

    /**
     * Try to figure out the appropriate value for $LANG.
     *
     *@access private
     *@return string The value of $LANG.
     */
    function _get_lang() {
        if (!empty($GLOBALS['LANG']))
            return $GLOBALS['LANG'];

        foreach (array('LC_ALL', 'LC_MESSAGES', 'LC_RESPONSES') as $var) {
            $lang = setlocale(constant($var), 0);
            if (!empty($lang))
                return $lang;
        }
            
        foreach (array('LC_ALL', 'LC_MESSAGES', 'LC_RESPONSES', 'LANG') as $var) {
            $lang = getenv($var);
            if (!empty($lang))
                return $lang;
        }

        return "C";
    }
}

/**
 * A class for finding PEAR code.
 *
 * This is a subclass of FileFinder which searches a standard list of
 * directories where PEAR code is likely to be installed.
 *
 * Example usage:
 *
 * <pre>
 *   $pearFinder = new PearFileFinder;
 *   $pearFinder->includeOnce('DB.php');
 * </pre>
 *
 * The above code will look for 'DB.php', if found, the directory in
 * which it was found will be added to PHP's include_path, and the
 * file will be included. (If the file is not found, and E_USER_ERROR
 * will be thrown.)
 */
class PearFileFinder
    extends FileFinder
{
    /**
     * Constructor.
     *
     * @param $path array Where to look for PEAR library code.
     * A good set of defaults is provided, so you can probably leave
     * this parameter blank.
     */
    function PearFileFinder ($path = array()) {
        $this->FileFinder(array_merge(
                          $path,
                          array('/usr/share/php4',
                                '/usr/share/php',
                                '/usr/lib/php4',
                                '/usr/lib/php',
                                '/usr/local/share/php4',
                                '/usr/local/share/php',
                                '/usr/local/lib/php4',
                                '/usr/local/lib/php',
                                '/System/Library/PHP',
                                '/Apache/pear'        // Windows
                                )));
    }
}

/**
 * Find PhpWiki localized files.
 *
 * This is a subclass of FileFinder which searches PHP's include_path
 * for files. It looks first for "locale/$LANG/$file", then for
 * "$file".
 *
 * If $LANG is something like "de_DE.iso8859-1@euro", this class will
 * also search under various less specific variations like
 * "de_DE.iso8859-1", "de_DE" and "de".
 */
class LocalizedFileFinder
extends FileFinder
{
    /**
     * Constructor.
     */
    function LocalizedFileFinder () {
        $this->_pathsep = $this->_get_syspath_separator();
        $include_path = $this->_get_include_path();
        $path = array();

        $lang = $this->_get_lang();
        assert(!empty($lang));

        if ($locales = $this->locale_versions($lang)) {
            foreach ($locales as $lang) {
                if ($lang == 'C') $lang = 'en';
                foreach ($include_path as $dir) {
                    $path[] = $this->slashifyPath($dir . "/locale/$lang");
                }
            }
        }
        $this->FileFinder(array_merge($path, $include_path));
    }
}

/**
 * Find PhpWiki localized theme buttons.
 *
 * This is a subclass of FileFinder which searches PHP's include_path
 * for files. It looks first for "buttons/$LANG/$file", then for
 * "$file".
 *
 * If $LANG is something like "de_DE.iso8859-1@euro", this class will
 * also search under various less specific variations like
 * "de_DE.iso8859-1", "de_DE" and "de".
 */
class LocalizedButtonFinder
extends FileFinder
{
    /**
     * Constructor.
     */
    function LocalizedButtonFinder () {
        global $WikiTheme;
        $this->_pathsep = $this->_get_syspath_separator();
        $include_path = $this->_get_include_path();
        $path = array();

        $lang = $this->_get_lang();
        assert(!empty($lang));
        assert(!empty($WikiTheme));

        if (is_object($WikiTheme)) {
            $langs = $this->locale_versions($lang);
            foreach ($langs as $lang) {
                if ($lang == 'C') $lang = 'en';
                foreach ($include_path as $dir) {
                    $path[] = $this->slashifyPath($WikiTheme->file("buttons/$lang"));
                }
            }
        }

        $this->FileFinder(array_merge($path, $include_path));
    }
}

// Search PHP's include_path to find file or directory.
function FindFile ($file, $missing_okay = false, $slashify = false)
{
    static $finder;
    if (!isset($finder)) {
        $finder = new FileFinder;
    	// remove "/lib" from dirname(__FILE__)
    	$wikidir = preg_replace('/.lib$/','',dirname(__FILE__));
        // let the system favor its local pear?
    	$finder->_append_to_include_path(dirname(__FILE__)."/pear");
    	$finder->_prepend_to_include_path($wikidir);
        // Don't override existing INCLUDE_PATH config.
        if (!defined("INCLUDE_PATH"))
 	    define("INCLUDE_PATH", implode($finder->_get_ini_separator(), $finder->_path));
    }
    $s = $finder->findFile($file, $missing_okay);
    if ($slashify)
        $s = $finder->slashifyPath($s);
    return $s;
}

// Search PHP's include_path to find file or directory.
// Searches for "locale/$LANG/$file", then for "$file".
function FindLocalizedFile ($file, $missing_okay = false, $re_init = false)
{
    static $finder;
    if ($re_init or !isset($finder))
        $finder = new LocalizedFileFinder;
    return $finder->findFile($file, $missing_okay);
}

function FindLocalizedButtonFile ($file, $missing_okay = false, $re_init = false)
{
    static $buttonfinder;
    if ($re_init or !isset($buttonfinder))
        $buttonfinder = new LocalizedButtonFinder;
    return $buttonfinder->findFile($file, $missing_okay);
}

/** 
 * Prefixes with PHPWIKI_DIR and slashify.
 * For example to unify with 
 *   require_once dirname(__FILE__).'/lib/file.php'
 *   require_once 'lib/file.php' loading style.
 * Doesn't expand "~" or symlinks yet. truename would be perfect.
 *
 * NormalizeLocalFileName("lib/config.php") => /home/user/phpwiki/lib/config.php
 */
function NormalizeLocalFileName($file) {
    static $finder;
    if (!isset($finder)) {
        $finder = new FileFinder;
    }
    // remove "/lib" from dirname(__FILE__)
    if ($finder->_is_abs($file))
        return $finder->slashifyPath($file);
    else {
        if (defined("PHPWIKI_DIR")) $wikidir = PHPWIKI_DIR;
        else $wikidir = preg_replace('/.lib$/','',dirname(__FILE__));
        $wikidir = $finder->_strip_last_pathchar($wikidir);
        $pathsep = $finder->_use_path_separator($wikidir);
        return $finder->slashifyPath($wikidir . $pathsep . $file);
        // return PHPWIKI_DIR . "/" . $file;
    }
}

/** 
 * Prefixes with DATA_PATH and slashify
 */
function NormalizeWebFileName($file) {
    static $finder;
    if (!isset($finder)) {
        $finder = new FileFinder;
    }
    if (defined("DATA_PATH")) {
        $wikipath = DATA_PATH;
        $wikipath = $finder->_strip_last_pathchar($wikipath);
        if (!$file)
            return $finder->forcePathSlashes($wikipath);
        else
            return $finder->forcePathSlashes($wikipath . '/' . $file);
    } else {
        return $finder->forcePathSlashes($file);
    }
}

function isWindows() {
    static $win;
    if (isset($win)) return $win;
    //return preg_match('/^Windows/', php_uname());
    $win = (substr(PHP_OS,0,3) == 'WIN');
    return $win;
}

function isWindows95() {
    static $win95;
    if (isset($win95)) return $win95;
    $win95 = isWindows() and !isWindowsNT();
    return $win95;
}

function isWindowsNT() {
    static $winnt;
    if (isset($winnt)) return $winnt;
    // FIXME: Do this using PHP_OS instead of php_uname().
    // $winnt = (PHP_OS == "WINNT"); // example from http://www.php.net/manual/en/ref.readline.php
    if (function_usable('php_uname'))
        $winnt = preg_match('/^Windows NT/', php_uname());
    else
        $winnt = false;         // FIXME: punt.
    return $winnt;
}

// Local Variables:
// mode: php
// tab-width: 8
// c-basic-offset: 4
// c-hanging-comment-ender-p: nil
// indent-tabs-mode: nil
// End:
?>
