<?php
/**
 * Class for "transparent" PHP code pre-filtering.
 * (C) 2005 Dmitry Koterov, http://forum.dklab.ru/users/DmitryKoterov/
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * See http://www.gnu.org/copyleft/lesser.html

 * Модуль позволяет выполнять префильтрацию PHP-кода перед его 
 * выполнением, по аналогии с тем, как это сделано в библиотеке 
 * Filter::Util::Call языка Perl.
 * 
 * Может использоваться, например, для того, чтобы заставить
 * стандартные операторы PHP {?= ... ?} выполнять htmlspecialchars()
 * перед выводом, обезопасивая, таким образом, данные.
 *
 * Version: 0.12
 */

// Need global variable! $this->curContext is unfortunately not 
// accessible in OB handler. Maybe PHP bug?..
$GLOBALS['PHP_CodeFilter_Context'] = array();

class 
PHP_CodeFilter
{
    var 
$_cf_prevErrorHandler null;
    var 
$_cf_filters = array();


    
// constructor()
    // Create new code filter.
    
function PHP_CodeFilter()
    {
    }


    
// void addFilter(callback $func)
    // Add new filter in filters chain.
    
function addFilter($func)
    {
        
$this->_cf_filters[] =& $func;
    }

    
// mixed evalCode(string $code, string $filename="eval", int $line=1)
    // Run the PHP code in "context" of $fname:$line.
    // All warnings, notices and rather error messages will be printed
    // starting from $fname:$line, NOT as they were generated by 
    // standard eval().
    
function evalCode($code$fname='eval'$line=1)
    {
        return 
$this->_evalCode($code$fname$line);
    }

    
    
// mixed includeFile(string $filename)
    // Include the file using pre-filtering.
    // Filters may be added in caller programs. They are called one by one.
    
function includeFile($fname)
    {
        
// Absolutize file path.
        
if ($this->_cf_filters) {
            
// Read file & filter it.
            
$file $this->_getRealPathUsingIncludeDirs($fname);
            
$code file_get_contents($filetrue);
            if (
$code === false) {
                
trigger_error("croak[1]: Could not include $fname"E_USER_WARNING);
                return;
            }
            
$code $this->filter($code$file);
            
// Run the code in specified context.
            
return $this->_evalCode('?>'.$code$file1);
        } else {
            
// Run the code directly from file.
            
$this->_pushContext($fname);
                
$result $this->_fileCaller($fname);
            
$this->_popContext();
            return 
$result;
        }
    }


    
// list of array debug_backtrace()
    // Replacement for standard debug_backtrace().
    // Remove all internal stack elements related to PHP_CodeFilter library.
    // Remove eval references from filtered files.
    
function debug_backtrace()
    {
        
$contexts $this->_getContexts();
        
$orig debug_backtrace(); array_shift($orig);
        
$stack = array();
        foreach (
$orig as $s) {
            if (!empty(
$s['file']) && $this->_isInEval($s['file']) && $contexts) {
                
$context array_pop($contexts);
                
$s['file'] = $context['file'];
                if (isset(
$s['line']) && isset($context['line'])) $s['line'] += $context['line'] - 1;
            } else if (
$this->_isInCodeFilterLibrary($s)) {
                continue;
            }
            
$stack[] = $s;
        }
        return 
$stack;
    }


    
// string filter(string $code, string $fname=null)
    // Call all filters for code.
    
function filter($code$fname=null)
    {
        foreach (
array_reverse($this->_cf_filters) as $filter) {
            
$code call_user_func($filter$code$fname);
        }
        return 
$code;
    }



    
// string formatError(bool $isHtml, int $no, string $msg, string $file, int $line)
    // Emulate PHP behaviour of error message formatting.
    
function formatError($isHtml$no$msg$file$line)
    {
        if (
is_numeric($no)) {
            
// Prepare error context.
            
$types = array(
                
"Fatal error" => array("E_ERROR""E_CORE_ERROR""E_COMPILE_ERROR""E_USER_ERROR"),
                
"Warning"     => array("E_WARNING""E_CORE_WARNING""E_COMPILE_WARNING""E_USER_WARNING"),
                
"Parse error" => array("E_PARSE"),
                
"Notice"      => array("E_NOTICE""E_USER_NOTICE"),
            );
            
// Textual error type.
            
$type "Unknown error";
            foreach (
$types as $message=>$groups) {
                foreach (
$groups as $t) {
                    
$e defined($t)? constant($t) : 0;
                    if (
$no == $e) {
                        
$type $message;
                        break(
2);
                    }
                }
            }
        } else {
            
$type $no;
        }
        
// Format message.
        
if ($isHtml) {
            return
                
ini_get('error_prepend_string') . 
                
"<b>$type</b>:  $msg in <b>$file</b> on line <b>$line</b><br />" .
                
ini_get('error_append_string');
        } else {
            return 
"$type:  $msg in $file on line $line";
        }
    }


    
/**
     * Internal functions.
     */

    // mixed _evalCode(string $code, string $filename, int $line)
    // Run the PHP code in "context" of $fname:$line.
    // All warnings, notices and rather error messages will be printed
    // starting from $fname:$line, NOT as they were generated by 
    // standard eval().
    
function _evalCode($code$fname$line)
    {
        
// Set up new context.
        
$this->_pushContext($fname$line);

            
// Run the code.
            
$prevFile = isset($GLOBALS['__FILE__'])? $GLOBALS['__FILE__'] : null;
            
$GLOBALS['__FILE__'] = $fname;
            
$result $this->_codeCaller($code);
            if (
$prevFile !== null$GLOBALS['__FILE__'] = $prevFile; else unset($GLOBALS['__FILE__']);

        
// Restore context.
        
$this->_popContext();

        return 
$result;
    }


    
// string _getRealPathUsingIncludeDirs(string $path)
    // Stub. Should return absolute path using include_path.
    
function _getRealPathUsingIncludeDirs($path)
    {
        return 
realpath($path);
    }


    
// string _abs_path(string $path)
    // Should return absolute path by relative one.
    
function _abs_path($path)
    {
        if (
preg_match('{^(\w:|[/\\\\])}s'$path)) return $path;
        return 
getcwd()."/".$path;
    }


    
// list of array _getContexts()
    // Return complete context stack.
    
function& _getContexts()
    {
        return 
$GLOBALS['PHP_CodeFilter_Context'];
    }


    
// array _getCurContext()
    // Return current (last) context.
    
function _getCurContext()
    {
        
$contexts $this->_getContexts();
        if (
$contexts) return $contexts[count($contexts)-1];
        return 
null;
    }


    
// void _pushContext(string $fname=null, int $line=1)
    // Save current context data in the stack and activete new context.
    
function _pushContext($fname=null$line=1)
    {
        
$contexts =& $this->_getContexts();
        
$context = array();
        
// Handle fatal and non-fatal errors only once!
        
if (!$contexts) {
            
ob_start(array(&$this'_obHandler'));
            
$context['error_handler']  = set_error_handler(array(&$this"_errorHandler"));  
            
$context['display_errors'] = ini_set('display_errors'true);
            
$context['log_errors']     = ini_set('log_errors'false);
            
ini_set('error_log'$this->_abs_path(ini_get('error_log')));
        }
        
// If not use "@" - in PHP 4.4.0 error generated on undefined $GLOBALS['__FILE__']:
        // "User error handler must not modify error context". 
        
$context['prev__FILE__'] = @$GLOBALS['__FILE__'];
        
$GLOBALS['__FILE__'] = $fname;
        
$context['file'] = $fname;
        
$context['line'] = $line;
        
$contexts[] = $context;
    }


    
// void _popContext()
    // Restore previous context from the stack.
    
function _popContext()
    {
        
$contexts =& $this->_getContexts();
        
$context array_pop($contexts);

        
$GLOBALS['__FILE__'] = $context['prev__FILE__'];

        
// Restore handlers.
        
if (!$contexts) {
            
// To avoid conflict with header().
            
if (ob_get_contents() !== ""ob_end_flush(); else ob_end_clean();
            
restore_error_handler();
            
ini_set('display_errors'$context['display_errors']);
            
ini_set('log_errors',     $context['log_errors']);
        }
    }

    
    
// mixed _codeCaller(string $__code__)
    // Eval() wrapper - used to globalize variables before evalling.
    
function _codeCaller($__code__)
    {
        
// Globalize all variables.
        
for (reset($GLOBALS); ($__k__=key($GLOBALS)) != nullnext($GLOBALS)) global $$__k__;
        unset(
$__k__);
        
// Call eval().
        
$__result__ = eval($__code__); 
        if (
$__result__ === false) {
            
// In case of parse error - process now (because file name later may be wrong).
            // Use the same variable as temporary!!!
            
$__result__ ob_get_contents();
            
ob_clean();
            echo 
$this->_obHandler($__result__);
            
$__result__ false;
        }
        return 
$__result__;
    }


    
// mixed _fileCaller(string $filename)
    // include() wrapper - used to globalize variables before including.
    
function _fileCaller($__fname__)
    {
        
// Globalize all variables.
        
for (reset($GLOBALS); ($__k__=key($GLOBALS)) != nullnext($GLOBALS)) global $$__k__;
        unset(
$__k__);
        
// Call include().
        
return include($__fname__);
    }


    
// string _obHandler(string $text)
    // Output handler.
    // Used to handle fatal error messages and teplace them to messages
    // in right (non-eval) context.
    
function _obHandler($text)
    {
        
// RE to determine is there was an error.
        
$prefix ini_get('error_prepend_string');
        
$suffix ini_get('error_append_string');
        
$re '{^(.*)(' .
            
preg_quote($prefix'{}') .
            
"<br />\r?\n<b>(\w+ error)</b>: \s*" .
            
'(.*?)' .
            
' in <b>)(.*?)(</b>' .
            
' on line <b>)(\d+)(</b><br />' .
            
"\r?\n" .
            
preg_quote($suffix'{}') .
            
')()$' .
        
'}s';
#       echo "$re\n\n[$text]\n\n";
        
$p null;
        if (!
preg_match($re$text$p)) return $text;
        list (, 
$content$beforeFile$error$msg$file$beforeLine$line$afterLine$tail) = $p;

        
// Do it only for code inside eval().
        
if (!$this->_isInEval($file)) return $text;

        
// Correct line and file number. Wow!
        
$context $this->_getCurContext();
        if (
$context) {
            
$file $context['file'];
            
$line += $context['line'] - 1;
        }

        
$contexts $this->_getContexts();
        
$firstContext $contexts$contexts[0] : null;

        if (!
$firstContext || $firstContext['display_errors']) {
            
// Make new page content with REPLACED error message.
            
$content $content $beforeFile $file $beforeLine $line $afterLine $tail;
        }
        if (
$firstContext && $firstContext['log_errors']) {
            
$text $this->formatError(false$error$msg$file$line);
            
error_log($text);
        }

        return 
$content;
    }

    
    
// void _errorHandler($no, $msg, $file, $line, $context)
    // Warning and notice handler.
    // Used to handle non-faral errors (e.g. E_NOTICE).
    // Also replace error message context.
    
function _errorHandler($no$msg$file$line$dummy
    {
        
// @-based lines are totally ignored.
        
if (!($no error_reporting())) return;
        
// If we are in eval(), correct file and line.

        
$contexts $this->_getContexts();
        
$context $this->_getCurContext();
                    
        
$firstContext $contexts$contexts[0] : null;

        if (
$this->_isInEval($file) && $context) {
            
$file $context['file'];
            
$line += $context['line'] - 1;
        }

        
// If there was previous error handler, use it directly.
        
$prev $contexts$contexts[0]['error_handler'] : null;
        if (
$prev) {
            
// Special case for Debug_BacktraceDumper.
            
if (is_array($prev) && is_a($prev[0], 'Debug_BacktraceDumper')) {
                
$prev[0]->setTracer(array(&$this'debug_backtrace'));
            }
            if (
$firstContext) {
                
$display_errors ini_set('display_errors'$firstContext['display_errors']);
                
$log_errors     ini_set('log_errors',     $firstContext['log_errors']);
            }
            
// Call previous error handler.
            
call_user_func($prev$no$msg$file$line$context);
            if (
$firstContext) {
                
ini_set('display_errors'$display_errors);
                
ini_set('log_errors',     $log_errors);
            }
        } else {
            
// Default handler! Emulate standard PHP behaviour (r-rrrrr).
            
if ($firstContext && $firstContext['display_errors']) {
                
$text $this->formatError(true$no$msg$file$line);
                echo 
$text;
            }
            if (
$firstContext && $firstContext['log_errors']) {
                
$text $this->formatError(false$no$msg$file$line);
                
error_log("PHP $text");
            }
        }
        return 
true;
    }


    
// bool _isInEval(string $filename)
    // Return true if $fname is like eval()'d location called in __FILE__.
    
function _isInEval($fname
    {
        
$fname str_replace('\\''/'$fname);
        
$curFile str_replace('\\''/'__FILE__);
        
$re '{^'.preg_quote($curFile'{}').'\b.*eval\(\)\'d code$}s';
        return 
preg_match($re$fname);
    }


    
// bool _isInCodeFilterLibrary(array $stackEntry)
    // Return true if $filename is the file of THIS library.
    
function _isInCodeFilterLibrary($s
    {
        
$fname str_replace('\\''/', @$s['file']);
        
$curFile str_replace('\\''/'__FILE__);
        
$re '{^'.preg_quote($curFile'{}').'$}s';
        return 
            (!empty(
$s['class']) && !strcasecmp($s['class'], __CLASS__) && @$s['function']{0} == "_"
            || (
preg_match($re$fname) && (!strcasecmp(@$s['function'], 'call_user_func') || !strcasecmp(@$s['function'], 'eval')));
    }
}
?>