<?php
/**
 * Check Compatibility of chunk of PHP code
 *
 * PHP versions 4 and 5
 *
 * LICENSE: This source file is subject to version 3.01 of the PHP license
 * that is available through the world-wide-web at the following URI:
 * http://www.php.net/license/3_01.txt.  If you did not receive a copy of
 * the PHP License and are unable to obtain it through the web, please
 * send a note to license@php.net so we can mail you a copy immediately.
 *
 * @category PHP
 * @package  PHP_CompatInfo
 * @author   Davey Shafik <davey@php.net>
 * @author   Laurent Laville <pear@laurent-laville.org>
 * @license  http://www.php.net/license/3_01.txt  PHP License 3.01
 * @version  CVS: $Id: CompatInfo.php,v 1.92 2008/04/17 20:34:02 farell Exp $
 * @link     http://pear.php.net/package/PHP_CompatInfo
 * @since    File available since Release 0.7.0
 */

require_once 'File/Find.php';

/**
 * An array of function init versions and extension
 */
require_once 'PHP/CompatInfo/func_array.php';

/**
 * An array of constants and their init versions
 */
require_once 'PHP/CompatInfo/const_array.php';

/**
 * Check Compatibility of chunk of PHP code
 *
 * @example docs/examples/checkConstants.php
 *          Example that shows minimum version with Constants
 * @example docs/examples/parseFile.php
 *          Example on how to parse a file
 * @example docs/examples/parseDir.php
 *          Example on how to parse a directory
 * @example docs/examples/parseArray.php
 *          Example on using using parseArray() to parse a script
 * @example docs/examples/parseString.php
 *          Example on how to parse a string
 * @example docs/examples/cliCustom.php
 *          Example of using PHP_CompatInfo_Cli
 *
 * @category  PHP
 * @package   PHP_CompatInfo
 * @author    Davey Shafik <davey@php.net>
 * @author    Laurent Laville <pear@laurent-laville.org>
 * @copyright 2003 Davey Shafik and Synaptic Media. All Rights Reserved.
 * @license   http://www.php.net/license/3_01.txt  PHP License 3.01
 * @version   Release: 1.7.0
 * @link      http://pear.php.net/package/PHP_CompatInfo
 * @since     Class available since Release 0.7.0
 */

class PHP_CompatInfo
{
    /**
     * @var string Earliest version of PHP to use
     * @since  0.7.0
     */
    var $latest_version = '3.0.0';

    /**
     * @var string Last version of PHP to use
     */
    var $earliest_version = '';

    /**
     * @var boolean Toggle parseDir recursion
     * @since  0.7.0
     */
    var $recurse_dir = true;

    /**
     * Parse a single file
     *
     * Parse a file for its compatibility info
     *
     * @param string $file    Path of File to parse
     * @param array  $options An array of options where:
     *  - 'debug'                   Contains a boolean to control whether
     *                              extra ouput is shown.
     *  - 'ignore_functions'        Contains an array of functions to ignore
     *                              when calculating the version needed.
     *  - 'ignore_constants'        Contains an array of constants to ignore
     *                              when calculating the version needed.
     *  - 'ignore_extensions'       Contains an array of php extensions to ignore
     *                              when calculating the version needed.
     *  - 'ignore_versions'         Contains an array of php versions to ignore
     *                              when calculating the version needed.
     *  - 'ignore_functions_match'  Contains an array of function patterns to ignore
     *                              when calculating the version needed.
     *  - 'ignore_extensions_match' Contains an array of extension patterns to ignore
     *                              when calculating the version needed.
     *  - 'ignore_constants_match'  Contains an array of constant patterns to ignore
     *                              when calculating the version needed.
     *
     * @access public
     * @return array
     * @since  version 0.7.0 (2004-03-09)
     */
    function parseFile($file, $options = array())
    {
        if (is_string($file) && !file_exists($file)) {
            // filter invalid input
            return false;
        }
        $options = array_merge(array('debug' => false), $options);
        $tokens  = $this->_tokenize($file);
        $results = $this->_parseTokens($tokens, $options);
        if ($options['debug'] === false) {
            $results['cond_code'][1] = array();
        }
        return $results;
    }

    /**
     * Parse a string
     *
     * Parse a string for its compatibility info
     *
     * @param string $string  PHP Code to parses
     * @param array  $options An array of options where:
     *  - 'debug'                   Contains a boolean to control whether
     *                              extra ouput is shown.
     *  - 'ignore_functions'        Contains an array of functions to ignore
     *                              when calculating the version needed.
     *  - 'ignore_constants'        Contains an array of constants to ignore
     *                              when calculating the version needed.
     *  - 'ignore_extensions'       Contains an array of php extensions to ignore
     *                              when calculating the version needed.
     *  - 'ignore_versions'         Contains an array of php versions to ignore
     *                              when calculating the version needed.
     *  - 'ignore_functions_match'  Contains an array of function patterns to ignore
     *                              when calculating the version needed.
     *  - 'ignore_extensions_match' Contains an array of extension patterns to ignore
     *                              when calculating the version needed.
     *  - 'ignore_constants_match'  Contains an array of constant patterns to ignore
     *                              when calculating the version needed.
     *
     * @access public
     * @return array|false
     * @since  version 0.7.0 (2004-03-09)
     */
    function parseString($string, $options = array())
    {
        if (!is_string($string)) {
            // filter invalid input
            return false;
        }
        $options = array_merge(array('debug' => false), $options);
        $tokens  = $this->_tokenize($string, true);
        $results = $this->_parseTokens($tokens, $options);
        if ($options['debug'] === false) {
            $results['cond_code'][1] = array();
        }
        return $results;
    }

    /**
     * Parse a directory
     *
     * Parse a directory recursively for its compatibility info
     *
     * @param string $dir     Path of folder to parse
     * @param array  $options An array of options where:
     *  - 'file_ext'                Contains an array of file extensions to parse
     *                              for PHP code. Default: php, php4, inc, phtml
     *  - 'recurse_dir'             Boolean on whether to recursively find files
     *  - 'debug'                   Contains a boolean to control whether
     *                              extra ouput is shown.
     *  - 'ignore_functions'        Contains an array of functions to ignore
     *                              when calculating the version needed.
     *  - 'ignore_constants'        Contains an array of constants to ignore
     *                              when calculating the version needed.
     *  - 'ignore_files'            Contains an array of files to ignore.
     *                              File names are case insensitive.
     *  - 'ignore_dirs'             Contains an array of directories to ignore.
     *                              Directory names are case insensitive.
     *  - 'ignore_extensions'       Contains an array of php extensions to ignore
     *                              when calculating the version needed.
     *  - 'ignore_versions'         Contains an array of php versions to ignore
     *                              when calculating the version needed.
     *  - 'ignore_functions_match'  Contains an array of function patterns to ignore
     *                              when calculating the version needed.
     *  - 'ignore_extensions_match' Contains an array of extension patterns to ignore
     *                              when calculating the version needed.
     *  - 'ignore_constants_match'  Contains an array of constant patterns to ignore
     *                              when calculating the version needed.
     *
     * @access public
     * @return array|false
     * @since  version 0.8.0 (2004-04-22)
     * @see PHP_CompatInfo::_fileList()
     */
    function parseDir($dir, $options = array())
    {
        if (!is_dir($dir) || !is_readable($dir)) {
            // filter invalid input
            return false;
        }

        $files_parsed       = array();
        $latest_version     = $this->latest_version;
        $earliest_version   = $this->earliest_version;
        $extensions         = array();
        $constants          = array();
        $tokens             = array();
        $ignored            = array();
        $ignored_functions  = array();
        $ignored_extensions = array();
        $ignored_constants  = array();
        $function_exists    = array();
        $extension_loaded   = array();
        $defined            = array();
        $cond_code          = 0;
        $default_options    = array(
            'file_ext' => array('php', 'php4', 'inc', 'phtml'),
            'recurse_dir' => true,
            'debug' => false,
            'ignore_files' => array(),
            'ignore_dirs' => array()
            );

        $options = array_merge($default_options, $options);

        if ($dir{strlen($dir)-1} == '/' || $dir{strlen($dir)-1} == '\\') {
            $path = $dir;
            $dir  = substr($dir, 0, -1);
        } else {
            $path = $dir . DIRECTORY_SEPARATOR;
        }

        $options['file_ext']
            = array_map('strtolower', $options['file_ext']);

        // use system directory separator rather than forward slash by default
        $ff         = new File_Find();
        $ff->dirsep = DIRECTORY_SEPARATOR;

        // get directory list that should be ignored from scope
        $ignore_dirs = array();
        if (count($options['ignore_dirs']) > 0) {
            foreach ($options['ignore_dirs'] as $cond) {
                $cond        = str_replace('\\', "\\\\", $cond);
                $dirs        = $ff->search('`'.$cond.'`', $dir, 'perl',
                                           true, 'directories');
                $ignore_dirs = array_merge($ignore_dirs, $dirs);
            }
        }

        // get file list that should be ignored from scope
        $ignore_files = array();
        if (count($options['ignore_files']) > 0) {
            foreach ($options['ignore_files'] as $cond) {
                $cond         = str_replace('\\', "\\\\", $cond);
                $files        = $ff->search('`'.$cond.'`', $dir, 'perl',
                                            true, 'files');
                $ignore_files = array_merge($ignore_files, $files);
            }
        }

        $files_filter = array();

        if ($options['recurse_dir'] === false) {
            $files = File_Find::glob('`.*`', $dir, 'perl');

            foreach ($files as $f) {
                $file_info = pathinfo($f);
                $entry     = $path . $file_info['basename'];
                if (is_dir($entry)) {
                    continue;
                } else {
                    if (in_array($entry, $ignore_files)) {
                        $ignored[] = $entry;
                        continue;
                    }
                }
                $files_filter[] = $entry;
            }
        } else {
            list($directories, $files) = $ff->maptree($dir);

            foreach ($files as $entry) {
                $file_info = pathinfo($entry);
                if (in_array($file_info['dirname'], $ignore_dirs)) {
                    $ignored[] = $entry;
                    continue;
                }
                if (in_array($entry, $ignore_files)) {
                    $ignored[] = $entry;
                    continue;
                }
                $files_filter[] = $entry;
            }
        }

        foreach ($files_filter as $file) {
            $file_info = pathinfo($file);
            if (isset($file_info['extension'])
                && in_array(strtolower($file_info['extension']),
                    $options['file_ext'])) {
                $tokens_list         = $this->_tokenize($file);
                $files_parsed[$file] = $this->_parseTokens($tokens_list, $options);
            }
        }
        foreach ($files_parsed as $fn => $file) {
            $cmp = version_compare($latest_version, $file['version']);
            if ($cmp === -1) {
                $latest_version = $file['version'];
            }
            if ($file['max_version'] != '') {
                $cmp = version_compare($earliest_version, $file['max_version']);
                if ($earliest_version == '' || $cmp === 1) {
                    $earliest_version = $file['max_version'];
                }
            }
            foreach ($file['extensions'] as $ext) {
                if (!in_array($ext, $extensions)) {
                    $extensions[] = $ext;
                }
            }
            foreach ($file['constants'] as $const) {
                if (!in_array($const, $constants)) {
                    $constants[] = $const;
                }
            }
            foreach ($file['tokens'] as $token) {
                if (!in_array($token, $tokens)) {
                    $tokens[] = $token;
                }
            }
            foreach ($file['ignored_functions'] as $if) {
                if (!in_array($if, $ignored_functions)) {
                    $ignored_functions[] = $if;
                }
            }
            foreach ($file['ignored_extensions'] as $ie) {
                if (!in_array($ie, $ignored_extensions)) {
                    $ignored_extensions[] = $ie;
                }
            }
            foreach ($file['ignored_constants'] as $ic) {
                if (!in_array($ic, $ignored_constants)) {
                    $ignored_constants[] = $ic;
                }
            }
            foreach ($file['cond_code'][1][0] as $ccf) {
                if (!in_array($ccf, $function_exists)) {
                    $function_exists[] = $ccf;
                }
            }
            foreach ($file['cond_code'][1][1] as $cce) {
                if (!in_array($cce, $extension_loaded)) {
                    $extension_loaded[] = $cce;
                }
            }
            foreach ($file['cond_code'][1][2] as $ccc) {
                if (!in_array($ccc, $defined)) {
                    $defined[] = $ccc;
                }
            }
            if ($options['debug'] === false) {
                $files_parsed[$fn]['cond_code'][1][0] = array();
                $files_parsed[$fn]['cond_code'][1][1] = array();
                $files_parsed[$fn]['cond_code'][1][2] = array();
            }
        }

        if (count($files_parsed) == 0) {
            return false;
        }

        if (count($function_exists) > 0) {
            $cond_code += 1;
        }
        if (count($extension_loaded) > 0) {
            $cond_code += 2;
        }
        if (count($defined) > 0) {
            $cond_code += 4;
        }
        if ($options['debug'] === false) {
            $function_exists  = array();
            $extension_loaded = array();
            $defined          = array();
        }
        $cond_code = array($cond_code,
                           array($function_exists, $extension_loaded, $defined));

        $main_info = array('ignored_files'      => $ignored,
                           'ignored_functions'  => $ignored_functions,
                           'ignored_extensions' => $ignored_extensions,
                           'ignored_constants'  => $ignored_constants,
                           'max_version'   => $earliest_version,
                           'version'       => $latest_version,
                           'extensions'    => $extensions,
                           'constants'     => $constants,
                           'tokens'        => $tokens,
                           'cond_code'     => $cond_code);

        $files_parsed = array_merge($main_info, $files_parsed);
        return $files_parsed;
    }

    /**
     * Alias of parseDir
     *
     * Alias of parseDir function
     *
     * @param string $folder  Path of folder to parse
     * @param array  $options An array of options
     *
     * @uses   PHP_CompatInfo::parseDir()
     * @access public
     * @return array
     * @since  version 0.7.0 (2004-03-09)
     */
    function parseFolder($folder, $options = array())
    {
        return $this->parseDir($folder, $options);
    }

    /**
     * Parse an Array of Files
     *
     * You can parse an array of Files or Strings, to parse
     * strings, $options['is_string'] must be set to true
     *
     * @param array $files   Array of file names or code strings
     * @param array $options An array of options where:
     *  - 'file_ext'                Contains an array of file extensions to parse
     *                              for PHP code. Default: php, php4, inc, phtml
     *  - 'debug'                   Contains a boolean to control whether
     *                              extra ouput is shown.
     *  - 'ignore_functions'        Contains an array of functions to ignore
     *                              when calculating the version needed.
     *  - 'ignore_constants'        Contains an array of constants to ignore
     *                              when calculating the version needed.
     *  - 'ignore_files'            Contains an array of files to ignore.
     *                              File names are case insensitive.
     *  - 'is_string'               Contains a boolean which says if the array values
     *                              are strings or file names.
     *  - 'ignore_extensions'       Contains an array of php extensions to ignore
     *                              when calculating the version needed.
     *  - 'ignore_versions'         Contains an array of php versions to ignore
     *                              when calculating the version needed.
     *  - 'ignore_functions_match'  Contains an array of function patterns to ignore
     *                              when calculating the version needed.
     *  - 'ignore_extensions_match' Contains an array of extension patterns to ignore
     *                              when calculating the version needed.
     *  - 'ignore_constants_match'  Contains an array of constant patterns to ignore
     *                              when calculating the version needed.
     *
     * @access public
     * @return array|false
     * @since  version 0.7.0 (2004-03-09)
     */
    function parseArray($files, $options = array())
    {
        $files_parsed       = array();
        $latest_version     = $this->latest_version;
        $earliest_version   = $this->earliest_version;
        $extensions         = array();
        $constants          = array();
        $tokens             = array();
        $ignored            = array();
        $ignored_functions  = array();
        $ignored_extensions = array();
        $ignored_constants  = array();
        $function_exists    = array();
        $extension_loaded   = array();
        $defined            = array();
        $cond_code          = 0;

        $options = array_merge(array(
            'file_ext' => array('php', 'php4', 'inc', 'phtml'),
            'is_string' => false,
            'debug' => false,
            'ignore_files' => array()
            ), $options);

        $options['ignore_files'] = array_map('strtolower', $options['ignore_files']);
        foreach ($files as $file) {
            if ($options['is_string'] === false) {
                $pathinfo = pathinfo($file);
                if (!in_array(strtolower($file), $options['ignore_files'])
                    && in_array($pathinfo['extension'], $options['file_ext'])) {
                    $tokens_list = $this->_tokenize($file, $options['is_string']);
                    $files_parsed[$file]
                            = $this->_parseTokens($tokens_list, $options);
                } else {
                    $ignored[] = $file;
                }
            } else {
                $token_list     = $this->_tokenize($file, $options['is_string']);
                $files_parsed[] = $this->_parseTokens($token_list, $options);
            }
        }

        foreach ($files_parsed as $i => $file) {
            $cmp = version_compare($latest_version, $file['version']);
            if ($cmp === -1) {
                $latest_version = $file['version'];
            }
            if ($file['max_version'] != '') {
                $cmp = version_compare($earliest_version, $file['max_version']);
                if ($earliest_version == '' || $cmp === 1) {
                    $earliest_version = $file['max_version'];
                }
            }
            foreach ($file['extensions'] as $ext) {
                if (!in_array($ext, $extensions)) {
                    $extensions[] = $ext;
                }
            }
            foreach ($file['constants'] as $const) {
                if (!in_array($const, $constants)) {
                    $constants[] = $const;
                }
            }
            foreach ($file['tokens'] as $token) {
                if (!in_array($token, $tokens)) {
                    $tokens[] = $token;
                }
            }
            foreach ($file['ignored_functions'] as $if) {
                if (!in_array($if, $ignored_functions)) {
                    $ignored_functions[] = $if;
                }
            }
            foreach ($file['ignored_extensions'] as $ie) {
                if (!in_array($ie, $ignored_extensions)) {
                    $ignored_extensions[] = $ie;
                }
            }
            foreach ($file['ignored_constants'] as $ic) {
                if (!in_array($ic, $ignored_constants)) {
                    $ignored_constants[] = $ic;
                }
            }
            foreach ($file['cond_code'][1][0] as $ccf) {
                if (!in_array($ccf, $function_exists)) {
                    $function_exists[] = $ccf;
                }
            }
            foreach ($file['cond_code'][1][1] as $cce) {
                if (!in_array($cce, $extension_loaded)) {
                    $extension_loaded[] = $cce;
                }
            }
            foreach ($file['cond_code'][1][2] as $ccc) {
                if (!in_array($ccc, $defined)) {
                    $defined[] = $ccc;
                }
            }
            if ($options['debug'] === false) {
                $files_parsed[$i]['cond_code'][1][0] = array();
                $files_parsed[$i]['cond_code'][1][1] = array();
                $files_parsed[$i]['cond_code'][1][2] = array();
            }
        }

        if (count($files_parsed) == 0) {
            return false;
        }

        if (count($function_exists) > 0) {
            $cond_code += 1;
        }
        if (count($extension_loaded) > 0) {
            $cond_code += 2;
        }
        if (count($defined) > 0) {
            $cond_code += 4;
        }
        if ($options['debug'] === false) {
            $function_exists  = array();
            $extension_loaded = array();
            $defined          = array();
        }
        $cond_code = array($cond_code,
                           array($function_exists, $extension_loaded, $defined));

        $main_info = array('ignored_files'      => $ignored,
                           'ignored_functions'  => $ignored_functions,
                           'ignored_extensions' => $ignored_extensions,
                           'ignored_constants'  => $ignored_constants,
                           'max_version'   => $earliest_version,
                           'version'       => $latest_version,
                           'extensions'    => $extensions,
                           'constants'     => $constants,
                           'tokens'        => $tokens,
                           'cond_code'     => $cond_code);

        $files_parsed = array_merge($main_info, $files_parsed);
        return $files_parsed;
    }

    /**
     * Load components list
     *
     * Load components list for a PHP version or subset
     *
     * @param string         $min           PHP minimal version
     * @param string|boolean $max           (optional) PHP maximal version
     * @param boolean        $include_const (optional) include constants list
     *                                                 in final result
     *
     * @return array         An array of php function/constant names to ignore
     * @access public
     * @static
     * @since  version 1.2.0 (2006-08-23)
     */
    function loadVersion($min, $max = false, $include_const = false)
    {
        $keys = array();
        foreach ($GLOBALS['_PHP_COMPATINFO_FUNCS'] as $func => $arr) {
            if (isset($arr['pecl']) && $arr['pecl'] === true) {
                continue;
            }
            if (version_compare($arr['init'], $min) < 0) {
                continue;
            }
            if ($max) {
                $end = (isset($arr['end'])) ? $arr['end'] : $arr['init'];

                if (version_compare($end, $max) < 1) {
                    $keys[] = $func;
                }
            } else {
                $keys[] = $func;
            }
        }

        if ($include_const === true) {
            $keys = array('functions' => $keys, 'constants' => array());
            foreach ($GLOBALS['_PHP_COMPATINFO_CONST'] as $const => $arr) {
                if (version_compare($arr['init'], $min) < 0) {
                    continue;
                }
                if ($max) {
                    $end = (isset($arr['end'])) ? $arr['end'] : $arr['init'];

                    if (version_compare($end, $max) < 1) {
                        $keys['constants'][] = $arr['name'];
                    }
                } else {
                    $keys['constants'][] = $arr['name'];
                }
            }
        }
        return $keys;
    }

    /**
     * Parse the given Tokens
     *
     * The tokens are those returned by
     * token_get_all() which is nicely
     * wrapped in PHP_CompatInfo::_tokenize
     *
     * @param array   $tokens  Array of PHP Tokens
     * @param boolean $options Show Extra Output
     *
     * @access private
     * @return array
     * @since  version 0.7.0 (2004-03-09)
     */
    function _parseTokens($tokens, $options)
    {
        static $akeys;

        $functions          = array();
        $functions_version  = array();
        $latest_version     = $this->latest_version;
        $earliest_version   = $this->earliest_version;
        $extensions         = array();
        $constants          = array();
        $constant_names     = array();
        $token_names        = array();
        $udf                = array();
        $ignore_functions   = array();
        $ignored_functions  = array();
        $ignore_extensions  = array();
        $ignored_extensions = array();
        $ignore_constants   = array();
        $ignored_constants  = array();
        $function_exists    = array();
        $extension_loaded   = array();
        $defined            = array();
        $cond_code          = 0;

        if (isset($options['ignore_constants'])) {
            $options['ignore_constants']
                = array_map('strtoupper', $options['ignore_constants']);
        } else {
            $options['ignore_constants'] = array();
        }
        if (isset($options['ignore_extensions'])) {
            $options['ignore_extensions']
                = array_map('strtolower', $options['ignore_extensions']);
        } else {
            $options['ignore_extensions'] = array();
        }
        if (isset($options['ignore_versions'][0])) {
            $min_ver = $options['ignore_versions'][0];
        } else {
            $min_ver = false;
        }
        if (isset($options['ignore_versions'][1])) {
            $max_ver = $options['ignore_versions'][1];
        } else {
            $max_ver = false;
        }

        if (isset($options['ignore_functions_match'])) {
            list($ifm_compare, $ifm_patterns) = $options['ignore_functions_match'];
        } else {
            $ifm_compare = false;
        }
        if (isset($options['ignore_extensions_match'])) {
            list($iem_compare, $iem_patterns) = $options['ignore_extensions_match'];
        } else {
            $iem_compare = false;
        }
        if (isset($options['ignore_constants_match'])) {
            list($icm_compare, $icm_patterns) = $options['ignore_constants_match'];
        } else {
            $icm_compare = false;
        }

        $token_count = sizeof($tokens);
        $i           = 0;
        $found_class = false;
        while ($i < $token_count) {
            if ($this->_isToken($tokens[$i], 'T_FUNCTION')) {
                $found_func = false;
            } else {
                $found_func = true;
            }
            while ($found_func == false) {
                $i += 1;
                if ($this->_isToken($tokens[$i], 'T_STRING')) {
                    $found_func = true;
                    $func       = $tokens[$i][1];
                    if ($found_class === false
                        || in_array($func, $function_exists)) {
                        $udf[] = $func;
                    }
                }
            }

            // Try to detect PHP method chaining implementation
            if ($this->_isToken($tokens[$i], 'T_VARIABLE')
                && $this->_isToken($tokens[$i+1], 'T_OBJECT_OPERATOR')
                && $this->_isToken($tokens[$i+2], 'T_STRING')
                && $this->_isToken($tokens[$i+3], '(')) {

                $i                   += 3;
                $php5_method_chaining = false;
                while ((!is_array($tokens[$i]) && $tokens[$i] == ';') === false) {
                    $i += 1;
                    if ((($this->_isToken($tokens[$i], ')'))
                        || ($this->_isToken($tokens[$i], 'T_WHITESPACE')))
                        && $this->_isToken($tokens[$i+1], 'T_OBJECT_OPERATOR')) {

                        $php5_method_chaining = true;
                    }
                }
            }

            // Compare "ignore_functions_match" pre-condition
            if (is_string($ifm_compare)) {
                if (strcasecmp('preg_match', $ifm_compare) != 0) {
                    // Try to catch function_exists() condition
                    if ($this->_isToken($tokens[$i], 'T_STRING')
                        && (strcasecmp($tokens[$i][1], $ifm_compare) == 0)) {

                        while ((!$this->_isToken($tokens[$i],
                                                 'T_CONSTANT_ENCAPSED_STRING'))) {
                            $i += 1;
                        }
                        $func = trim($tokens[$i][1], "'");

                        /**
                         * try if function_exists()
                         * match one or more pattern condition
                         */
                        foreach ($ifm_patterns as $pattern) {
                            if (preg_match($pattern, $func) === 1) {
                                $ignore_functions[] = $func;
                            }
                        }
                    }
                }
            }

            // Compare "ignore_extensions_match" pre-condition
            if (is_string($iem_compare)) {
                if (strcasecmp('preg_match', $iem_compare) != 0) {
                    // Try to catch extension_loaded() condition
                    if ($this->_isToken($tokens[$i], 'T_STRING')
                        && (strcasecmp($tokens[$i][1], $iem_compare) == 0)) {

                        while ((!$this->_isToken($tokens[$i],
                                                 'T_CONSTANT_ENCAPSED_STRING'))) {
                            $i += 1;
                        }
                        $ext = trim($tokens[$i][1], "'");

                        /**
                         * try if extension_loaded()
                         * match one or more pattern condition
                         */
                        foreach ($iem_patterns as $pattern) {
                            if (preg_match($pattern, $ext) === 1) {
                                $ignore_extensions[] = $ext;
                            }
                        }
                    }
                }
            }

            // Compare "ignore_constants_match" pre-condition
            if (is_string($icm_compare)) {
                if (strcasecmp('preg_match', $icm_compare) != 0) {
                    // Try to catch defined() condition
                    if ($this->_isToken($tokens[$i], 'T_STRING')
                        && (strcasecmp($tokens[$i][1], $icm_compare) == 0)) {

                        while ((!$this->_isToken($tokens[$i],
                                                 'T_CONSTANT_ENCAPSED_STRING'))) {
                            $i += 1;
                        }
                        $cst = trim($tokens[$i][1], "'");

                        /**
                         * try if defined()
                         * match one or more pattern condition
                         */
                        foreach ($icm_patterns as $pattern) {
                            if (preg_match($pattern, $cst) === 1) {
                                $ignore_constants[] = $cst;
                            }
                        }
                    }
                }
            }

            if ($this->_isToken($tokens[$i], 'T_STRING')
                && (isset($tokens[$i+1]))
                && $this->_isToken($tokens[$i+1], '(')) {

                $is_function = false;

                if (isset($tokens[$i-1])
                    && !$this->_isToken($tokens[$i-1], 'T_DOUBLE_COLON')
                    && !$this->_isToken($tokens[$i-1], 'T_OBJECT_OPERATOR')) {

                    if (isset($tokens[$i-2])
                        && $this->_isToken($tokens[$i-2], 'T_FUNCTION')) {
                        // its a function declaration
                    } else {
                        $is_function = true;
                    }
                }
                if ($is_function == true || !is_array($tokens[$i-1])) {
                    $functions[] = strtolower($tokens[$i][1]);
                }
            }

            // try to detect condition function_exists()
            if ($this->_isToken($tokens[$i], 'T_STRING')
                && (strcasecmp($tokens[$i][1], 'function_exists') == 0)) {

                $j = $i;
                while ((!$this->_isToken($tokens[$j],
                                         'T_CONSTANT_ENCAPSED_STRING'))) {
                    $j++;
                }
                $t_string          = $tokens[$j][1];
                $t_string          = trim($t_string, "'");
                $t_string          = trim($t_string, '"');
                $function_exists[] = $t_string;
            }
            // try to detect condition extension_loaded()
            if ($this->_isToken($tokens[$i], 'T_STRING')
                && (strcasecmp($tokens[$i][1], 'extension_loaded') == 0)) {

                $j = $i;
                while ((!$this->_isToken($tokens[$j],
                                         'T_CONSTANT_ENCAPSED_STRING'))) {
                    $j++;
                }
                $t_string           = $tokens[$j][1];
                $t_string           = trim($t_string, "'");
                $t_string           = trim($t_string, '"');
                $extension_loaded[] = $t_string;
            }
            // try to detect condition defined()
            if ($this->_isToken($tokens[$i], 'T_STRING')
                && (strcasecmp($tokens[$i][1], 'defined') == 0)) {

                $j = $i;
                while ((!$this->_isToken($tokens[$j],
                                         'T_CONSTANT_ENCAPSED_STRING'))) {
                    $j++;
                }
                $t_string  = $tokens[$j][1];
                $t_string  = trim($t_string, "'");
                $t_string  = trim($t_string, '"');
                $defined[] = $t_string;
            }

            // try to detect beginning of a class
            if ($this->_isToken($tokens[$i], 'T_CLASS')) {
                $found_class = true;
            }

            if (is_array($tokens[$i])) {
                if (!isset($akeys)) {
                    // build contents one time only (static variable)
                    $akeys = array_keys($GLOBALS['_PHP_COMPATINFO_CONST']);
                }
                $const = strtoupper($tokens[$i][1]);
                $found = array_search($const, $akeys);
                if ($found !== false) {
                    if ($this->_isToken($tokens[$i], 'T_ENCAPSED_AND_WHITESPACE')) {
                        // PHP 5 constant tokens found into a string
                    } else {
                        // Compare "ignore_constants_match" free condition
                        $icm_preg_match = false;
                        if (is_string($icm_compare)) {
                            if (strcasecmp('preg_match', $icm_compare) == 0) {
                                /**
                                 * try if preg_match()
                                 * match one or more pattern condition
                                 */
                                foreach ($icm_patterns as $pattern) {
                                    if (preg_match($pattern, $const) === 1) {
                                        $icm_preg_match = true;
                                        break;
                                    }
                                }
                            }
                        }

                        $init = $GLOBALS['_PHP_COMPATINFO_CONST'][$const]['init'];
                        if (!PHP_CompatInfo::_ignore($init, $min_ver, $max_ver)) {
                            $constants[] = $const;
                            if (in_array($const, $ignore_constants)
                                || in_array($const, $options['ignore_constants'])
                                || $icm_preg_match) {
                                $ignored_constants[] = $const;
                            } else {
                                $latest_version = $init;
                            }
                        }
                    }
                }
            }
            $i += 1;
        }

        $functions = array_unique($functions);
        if (isset($options['ignore_functions'])) {
            $options['ignore_functions']
                = array_map('strtolower', $options['ignore_functions']);
        } else {
            $options['ignore_functions'] = array();
        }
        if (count($ignore_functions) > 0) {
            $ignore_functions = array_map('strtolower', $ignore_functions);
            $options['ignore_functions']
                = array_merge($options['ignore_functions'], $ignore_functions);
            $options['ignore_functions']
                = array_unique($options['ignore_functions']);
        }
        if (count($ignore_extensions) > 0) {
            $ignore_extensions = array_map('strtolower', $ignore_extensions);
            $options['ignore_extensions']
                = array_merge($options['ignore_extensions'], $ignore_extensions);
            $options['ignore_extensions']
                = array_unique($options['ignore_extensions']);
        }

        foreach ($functions as $name) {
            if (!isset($GLOBALS['_PHP_COMPATINFO_FUNCS'][$name])) {
                continue;  // skip this unknown function
            }
            $func = $GLOBALS['_PHP_COMPATINFO_FUNCS'][$name];

            // retrieve if available the extension name
            if ((isset($func['ext']))
                && ($func['ext'] != 'ext_standard')
                && ($func['ext'] != 'zend')) {
                if ($func['pecl'] === false) {
                    $extension = substr($func['ext'], 4);
                    if ($extension{0} == '_') {
                        $extension = $func['ext'];
                    }
                } else {
                    $extension = $func['ext'];
                }
            } else {
                $extension = false;
            }

            // Compare "ignore_functions_match" free condition
            $ifm_preg_match = false;
            if (is_string($ifm_compare)) {
                if (strcasecmp('preg_match', $ifm_compare) == 0) {
                    /**
                     * try if preg_match()
                     * match one or more pattern condition
                     */
                    foreach ($ifm_patterns as $pattern) {
                        if (preg_match($pattern, $name) === 1) {
                            $ifm_preg_match = true;
                            break;
                        }
                    }
                }
            }

            if ((!in_array($name, $udf))
                && (!in_array($name, $options['ignore_functions']))
                && ($ifm_preg_match === false)) {

                if ($extension && !in_array($extension, $extensions)) {
                    $extensions[] = substr($func['ext'], 0, 4) == 'ext_'
                        ? $extension : $func['ext'];
                }

                // Compare "ignore_extensions_match" free condition
                $iem_preg_match = false;
                if (is_string($iem_compare)) {
                    if (strcasecmp('preg_match', $iem_compare) == 0) {
                        /**
                         * try if preg_match()
                         * match one or more pattern condition
                         */
                        foreach ($iem_patterns as $pattern) {
                            if (preg_match($pattern, $extension) === 1) {
                                $iem_preg_match = true;
                                break;
                            }
                        }
                    }
                }

                if ($extension
                    && (in_array($extension, $options['ignore_extensions'])
                        || $iem_preg_match)) {
                    if (!in_array($extension, $ignored_extensions)) {
                        // extension is ignored (only once)
                        $ignored_extensions[] = $extension;
                    }
                    // all extension functions are also ignored
                    $ignored_functions[] = $name;
                    continue;  // skip this extension function
                }

                if (PHP_CompatInfo::_ignore($func['init'], $min_ver, $max_ver)) {
                    continue;  // skip this function version
                }

                if ($options['debug'] == true) {
                    $functions_version[$func['init']][] = array(
                        'function' => $name,
                        'extension' => substr($func['ext'], 0, 4) == 'ext_'
                            ? $extension : $func['ext'],
                        'pecl' => $func['pecl']
                        );
                }
                if ($extension === false
                    || (isset($func['pecl']) && $func['pecl'] === false) ) {
                    $cmp = version_compare($latest_version, $func['init']);
                    if ($cmp === -1) {
                        $latest_version = $func['init'];
                    }
                    if (array_key_exists('end', $func)) {
                        $cmp = version_compare($earliest_version, $func['end']);
                        if ($earliest_version == '' || $cmp === 1) {
                            $earliest_version = $func['end'];
                        }
                    }
                }

            } else {
                // function is ignored
                $ignored_functions[] = $name;
            }
        }

        $ignored_constants = array_unique($ignored_constants);
        $constants         = array_unique($constants);
        foreach ($constants as $constant) {
            $const = $GLOBALS['_PHP_COMPATINFO_CONST'][$constant];
            if (PHP_CompatInfo::_ignore($const['init'], $min_ver, $max_ver)) {
                continue;  // skip this constant version
            }
            if (!in_array($constant, $ignored_constants)) {
                $cmp = version_compare($latest_version, $const['init']);
                if ($cmp === -1) {
                    $latest_version = $const['init'];
                }
                if (array_key_exists('end', $const)) {
                    $cmp = version_compare($earliest_version, $const['end']);
                    if ($earliest_version == '' || $cmp === 1) {
                        $earliest_version = $const['end'];
                    }
                }
            }
            if (!in_array($const['name'], $constant_names)) {
                // split PHP5 tokens and pure PHP constants
                if ($const['name'] == strtolower($const['name'])) {
                    $token_names[] = $const['name'];
                } else {
                    $constant_names[] = $const['name'];
                }
            }
        }

        if (isset($php5_method_chaining)
            && $php5_method_chaining === true
            && version_compare($latest_version, '5.0.0') < 0) {
            // when PHP Method chaining is detected, only available for PHP 5
            $latest_version = '5.0.0';
        }

        ksort($functions_version);

        if (count($function_exists) > 0) {
            $cond_code += 1;
        }
        if (count($extension_loaded) > 0) {
            $cond_code += 2;
        }
        if (count($defined) > 0) {
            $cond_code += 4;
        }
        $cond_code = array($cond_code,
                           array($function_exists, $extension_loaded, $defined));

        $main_info = array('ignored_functions'  => $ignored_functions,
                           'ignored_extensions' => $ignored_extensions,
                           'ignored_constants'  => $ignored_constants,
                           'max_version' => $earliest_version,
                           'version'     => $latest_version,
                           'extensions'  => $extensions,
                           'constants'   => $constant_names,
                           'tokens'      => $token_names,
                           'cond_code'   => $cond_code);

        $functions_version = array_merge($main_info, $functions_version);
        return $functions_version;
    }

    /**
     * Checks if function which has $init version should be keep
     * or ignore (version is between $min_ver and $max_ver).
     *
     * @param string $init    version of current function
     * @param string $min_ver minimum version of function to ignore
     * @param string $max_ver maximum version of function to ignore
     *
     * @access private
     * @return boolean True to ignore function/constant, false otherwise
     * @since  version 1.4.0 (2006-09-27)
     * @static
     */
    function _ignore($init, $min_ver, $max_ver)
    {
        if ($min_ver) {
            $cmp = version_compare($init, $min_ver);
            if ($max_ver && $cmp >= 0) {
                $cmp = version_compare($init, $max_ver);
                if ($cmp < 1) {
                    return true;
                }
            } elseif ($cmp === 0) {
                return true;
            }
        }
        return false;
    }

    /**
     * Token a file or string
     *
     * @param string  $input     Filename or PHP code
     * @param boolean $is_string Whether or note the input is a string
     * @param boolean $debug     add token names for human read
     *
     * @access private
     * @return array
     * @since  version 0.7.0 (2004-03-09)
     */
    function _tokenize($input, $is_string = false, $debug = false)
    {
        if ($is_string === false) {
            $input = file_get_contents($input, true);
        }
        $tokens = token_get_all($input);

        if ($debug === true) {
            $r = array();
            foreach ($tokens as $token) {
                if (is_array($token)) {
                    $token[] = token_name($token[0]);
                } else {
                    $token = $token[0];
                }
                $r[] = $token;
            }
        } else {
            $r = $tokens;
        }
        return $r;
    }

    /**
     * Checks if the given token is of this symbolic name
     *
     * @param mixed  $token    Single PHP token to test
     * @param string $symbolic Symbolic name of the given token
     *
     * @access private
     * @return bool
     * @since  version 1.7.0b4 (2008-04-03)
     */
    function _isToken($token, $symbolic)
    {
        if (is_array($token)) {
            $t = token_name($token[0]);
        } else {
            $t = $token;
        }
        return ($t == $symbolic);
    }
}
?>