<?php
//
// +----------------------------------------------------------------------+
// | PHP Version 4                                                        |
// +----------------------------------------------------------------------+
// | Copyright (c) 1997-2005 The PHP Group                                |
// +----------------------------------------------------------------------+
// | This source file is subject to version 2.02 of the PHP license,      |
// | that is bundled with this package in the file LICENSE, and is        |
// | available at through the world-wide-web at                           |
// | http://www.php.net/license/2_02.txt.                                 |
// | If you did not receive a copy of the PHP license and are unable to   |
// | obtain it through the world-wide-web, please send a note to          |
// | license@php.net so we can mail you a copy immediately.               |
// +----------------------------------------------------------------------+
// | Author: Sterling Hughes <sterling@php.net>                           |
// +----------------------------------------------------------------------+
//
// $Id$
//

require_once 'PEAR.php';

define('FILE_FIND_VERSION', '@package_version@');

// to debug uncomment this string
// define('FILE_FIND_DEBUG', '');

/**
*  Commonly needed functions searching directory trees
*
* @access public
* @version $Id$
* @package File
* @author Sterling Hughes <sterling@php.net>
*/
class File_Find
{
    /**
     * internal dir-list
     * @var array
     */
    var $_dirs = array();

    /**
     * directory separator
     * @var string
     */
    var $dirsep = "/";

    /**
     * found files
     * @var array
     */
    var $files = array();

    /**
     * found dirs
     * @var array
     */
    var $directories = array();

    /**
     * Search specified directory to find matches for specified pattern
     *
     * @param string $pattern a string containing the pattern to search
     * the directory for.
     *
     * @param string $dirpath a string containing the directory path
     * to search.
     *
     * @param string $pattern_type a string containing the type of
     * pattern matching functions to use (can either be 'php',
     * 'perl' or 'shell').
     *
     * @return array containing all of the files and directories
     * matching the pattern or null if no matches
     *
     * @author Sterling Hughes <sterling@php.net>
     * @access public
     * @static
     */
    function &glob($pattern, $dirpath, $pattern_type = 'php')
    {
        $dh = @opendir($dirpath);

        if (!$dh) {
            $pe = PEAR::raiseError("Cannot open directory $dirpath");
            return $pe;
        }

        $match_function = File_Find::_determineRegex($pattern, $pattern_type);
        $matches = array();

        // empty string cannot be specified for 'php' and 'perl' pattern
        if ($pattern || ($pattern_type != 'php' && $pattern_type != 'perl')) {
            while (false !== ($entry = @readdir($dh))) {
                if ($match_function($pattern, $entry) &&
                    $entry != '.' && $entry != '..') {
                    $matches[] = $entry;
                }
            }
        }

        @closedir($dh);

        if (0 == count($matches)) {
            $matches = null;
        }

        sort($matches);

        return $matches ;
    }

    /**
     * Map the directory tree given by the directory_path parameter.
     *
     * @param string $directory contains the directory path that you
     * want to map.
     *
     * @return array a two element array, the first element containing a list
     * of all the directories, the second element containing a list of all the
     * files.
     *
     * @author Sterling Hughes <sterling@php.net>
     * @access public
     */
    function &maptree($directory)
    {

        /* if called statically */
        if (!isset($this)  || !is_a($this, "File_Find")) {
            $obj = new File_Find();
            return $obj->maptree($directory);
        }
      
        /* clear the results just in case */
        $this->files       = array();
        $this->directories = array();

        /* strip out trailing slashes */
        $directory = preg_replace('![\\\\/]+$!', '', $directory);

        $this->_dirs = array($directory);

        while (count($this->_dirs)) {
            $dir = array_pop($this->_dirs);
            File_Find::_build($dir, $this->dirsep);
            array_push($this->directories, $dir);
        }

        sort($this->directories);
        sort($this->files);

        $retval = array($this->directories, $this->files);
        return $retval;

    }

    /**
     * Map the directory tree given by the directory parameter.
     *
     * @param string $directory contains the directory path that you
     * want to map.
     * @param integer $maxrecursion maximun number of folders to recursive 
     * map
     *
     * @return array a multidimensional array containing all subdirectories
     * and their files. For example:
     *
     * Array
     * (
     *    [0] => file_1.php
     *    [1] => file_2.php
     *    [subdirname] => Array
     *       (
     *          [0] => file_1.php
     *       )
     * )
     *
     * @author Mika Tuupola <tuupola@appelsiini.net>
     * @access public
     * @static
     */
    function &mapTreeMultiple($directory, $maxrecursion = 0, $count = 0)
    {   
        $retval = array();

        $count++;

        /* strip trailing slashes */
        $directory = preg_replace('![\\\\/]+$!', '', $directory);
        
        if (is_readable($directory)) {
            $dh = opendir($directory);
            while (false !== ($entry = @readdir($dh))) {
                if ($entry != '.' && $entry != '..') {
                     array_push($retval, $entry);
                }
            }
            closedir($dh);
        }

        sort($retval);

        while (list($key, $val) = each($retval)) {
      
            if (!is_array($val)) {
                $path = $directory . "/" . $val;
                if (is_dir($path)) {
                    unset($retval[$key]);
                    if ($maxrecursion == 0 || $count < $maxrecursion) {
                        $retval[$val] = &File_Find::mapTreeMultiple($path, 
                                        $maxrecursion, $count);
                    }
                }
            }
        }

        return $retval;
    }

    /**
     * Search the specified directory tree with the specified pattern.  Return
     * an array containing all matching files (no directories included).
     *
     * @param string $pattern the pattern to match every file with.
     *
     * @param string $directory the directory tree to search in.
     *
     * @param string $type the type of regular expression support to use, either
     * 'php', 'perl' or 'shell'.
     *
     * @param bool $fullpath whether the regex should be matched against the
     * full path or only against the filename
     *
     * @param string $match can be either 'files', 'dirs' or 'both' to specify
     * the kind of list to return
     *
     * @return array a list of files matching the pattern parameter in the the
     * directory path specified by the directory parameter
     *
     * @author Sterling Hughes <sterling@php.net>
     * @access public
     * @static
     */
    function &search($pattern, $directory, $type = 'php', $fullpath = true, $match = 'files')
    {

        $matches = array();
        list ($directories,$files)  = File_Find::maptree($directory);
        switch($match) {
            case 'directories': 
                $data = $directories; 
                break;
            case 'both': 
                $data = array_merge($directories, $files); 
                break;
            case 'files':
            default:
                $data = $files;
        }
        unset($files, $directories);

        $match_function = File_Find::_determineRegex($pattern, $type);

        reset($data);
        // check if empty string given (ok for 'shell' method, but bad for others)
        if ($pattern || ($type != 'php' && $type != 'perl')) {
            while (list(,$entry) = each($data)) {
                if ($match_function($pattern, 
                                    $fullpath ? $entry : basename($entry))) {
                    $matches[] = $entry;
                } 
            }
        }

        sort($matches);

        return $matches;
    }

    /**
     * Determine whether or not a variable is a PEAR error
     *
     * @param object PEAR_Error $var the variable to test.
     *
     * @return boolean returns true if the variable is a PEAR error, otherwise
     * it returns false.
     * @access public
     */
    function isError(&$var)
    {
        return PEAR::isError($var);
    }

    /**
     * internal function to build singular directory trees, used by
     * File_Find::maptree()
     *
     * @param string $directory name of the directory to read
     * @param string $separator directory separator
     * @return void
     */
    function _build($directory, $separator = "/")
    {

        $dh = @opendir($directory);

        if (!$dh) {
            $pe = PEAR::raiseError("Cannot open directory");
            return $pe;
        }

        while (false !== ($entry = @readdir($dh))) {
            if ($entry != '.' && $entry != '..') {

                $entry = $directory.$separator.$entry;

                if (is_dir($entry)) {
                    array_push($this->_dirs, $entry);
                } else {
                    array_push($this->files, $entry);
                }
            }
        }

        @closedir($dh);
    }

    /**
     * internal function to determine the type of regular expression to
     * use, implemented by File_Find::glob() and File_Find::search()
     *
     * @param string $type given RegExp type
     * @return string kind of function ( "eregi", "ereg" or "preg_match") ;
     *
     */
    function _determineRegex($pattern, $type)
    {
        if (!strcasecmp($type, 'shell')) {
            $match_function = 'File_Find_match_shell';
        } else if (!strcasecmp($type, 'perl')) {
            $match_function = 'preg_match';
        } else if (!strcasecmp(substr($pattern, -2), '/i')) {
            $match_function = 'eregi';
        } else {
            $match_function = 'ereg';
        }
        return $match_function;
    }

}

/**
* Package method to match via 'shell' pattern. Provided in global
* scope, because it should be called like 'preg_match' and 'eregi'
* and can be easily copied into other packages
*
* @author techtonik <techtonik@php.net>
* @return mixed bool on success and PEAR_Error on failure
*/ 
function File_Find_match_shell($pattern, $filename)
{
    // {{{ convert pattern to positive and negative regexps
        $positive = $pattern;
        $negation = substr_count($pattern, "|");

        if ($negation > 1) {
            PEAR::raiseError("Mask string contains errors!");
            return FALSE;
        } elseif ($negation) {
            list($positive, $negative) = explode("|", $pattern);
            if (strlen($negative) == 0) {
                PEAR::raiseError("File-mask string contains errors!");
                return FALSE;
            }
        }

       $positive = _File_Find_match_shell_get_pattern($positive);
       if ($negation) {
           $negative = _File_Find_match_shell_get_pattern($negative);
       }
    // }}} convert end 


    if (defined("FILE_FIND_DEBUG")) {
        print("Method: $type\nPattern: $pattern\n Converted pattern:");
        print_r($positive);
        if (isset($negative)) print_r($negative);
    }

    if (!preg_match($positive, $filename)) {
        return FALSE;
    } else {
        if (isset($negative) 
              && preg_match($negative, $filename)) {
            return FALSE;
        } else {
            return TRUE;
        }
    }
}

/**
* function used by File_Find_match_shell to convert 'shell' mask
* into pcre regexp. Some of the rules (see testcases for more): 
*  escaping all special chars and replacing 
*    . with \.
*    * with .*
*    ? with .{1}
*    also adding ^ and $ as the pattern matches whole filename
*
* @author techtonik <techtonik@php.net>
* @return string pcre regexp for preg_match
*/ 
function _File_Find_match_shell_get_pattern($mask) {
    // get array of several masks (if any) delimited by comma
    // do not touch commas in char class
    $premasks = preg_split("|(\[[^\]]+\])|", $mask, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
    if (defined("FILE_FIND_DEBUG")) {
        print("\nPremask: ");
        print_r($premasks);
    }
    $pi = 0;
    foreach($premasks as $pm) {
        if (!isset($masks[$pi])) $masks[$pi] = "";
        if ($pm{0} == '[' && $pm{strlen($pm)-1} == ']') {
            // strip commas from character class
            $masks[$pi] .= str_replace(",", "", $pm);
        } else {
            $tarr = explode(",", $pm);
            if (sizeof($tarr) == 1) {
                $masks[$pi] .= $pm;
            } else {
                foreach ($tarr as $te) {
                    $masks[$pi++] .= $te;
                    $masks[$pi] = "";
                }
                unset($masks[$pi--]);
            }
        }
    }

    // if empty string given return *.* pattern
    if (strlen($mask) == 0) return "!^.*$!";

    // convert to preg regexp
    $regexmask = implode("|", $masks);
    if (defined("FILE_FIND_DEBUG")) {
        print("regexMask step one(implode): $regexmask");
    }
    $regexmask = addcslashes($regexmask, '^$}!{)(\/.+');
    if (defined("FILE_FIND_DEBUG")) {
        print("\nregexMask step two(addcslashes): $regexmask");
    }
    $regexmask = preg_replace("!(\*|\?)!", ".$1", $regexmask);
    if (defined("FILE_FIND_DEBUG")) {
        print("\nregexMask step three(* ? -> .* .?): $regexmask");
    }
    // a special case '*.' at the end means that there is no extension
    $regexmask = preg_replace("!\.\*\\\.(\||$)!", "[^\.]*$1", $regexmask);
    // it is impossible to have dot at the end of filename
    $regexmask = preg_replace("!\\\.(\||$)!", "$1", $regexmask);
    // and .* at the end also means that there could be nothing at all
    //   (i.e. no dot at the end also)
    $regexmask = preg_replace("!\\\.\.\*(\||$)!", "(\\\\..*)?$1", $regexmask);
    if (defined("FILE_FIND_DEBUG")) {
        print("\nregexMask step two and half(*.$ \\..*$ .$ -> [^.]*$ .?.* $): $regexmask");
    }
    // if no extension supplied - add .* to match partially from filename start
    if (strpos($regexmask, "\\.") === FALSE) $regexmask .= ".*";

    // file mask match whole name - adding restrictions
    $regexmask = preg_replace("!(\|)!", '^'."$1".'$', $regexmask);
    $regexmask = '^'.$regexmask.'$';
    if (defined("FILE_FIND_DEBUG")) {
        print("\nregexMask step three(^ and $ to match whole name): $regexmask");
    }
    // wrap regex into ! since all ! are already escaped
    $regexmask = "!$regexmask!i";
    if (defined("FILE_FIND_DEBUG")) {
        print("\nWrapped regex: $regexmask\n");
    }
    return $regexmask;
}

/*
 * Local variables:
 * tab-width: 4
 * c-basic-offset: 4
 * End:
 */

?>
