<?php
/**
 * YANA library
 *
 * @package yana
 * @licence http://creativecommons.org/licenses/by-nc-sa/2.5/
 */

/**
 * «utility» static class for error handling and debugging
 *
 * @access      public
 * @name        class_errorutility
 * @package     yana
 * @subpackage  core
 */
class ErrorUtility
{

    /**
     * custom error handler
     *
     * This function creates colorfull error messages that should provide
     * better readability.
     *
     * @access  public
     * @param   int     $error_nr
     * @param   string  $description
     * @param   string  $file
     * @param   int     $line_nr
     * @static
     * @ignore
     */
    function printError($error_nr, $description, $file, $line_nr)
    {
        /* NOTE: to trigger an user error inside an user error handler could cause an infinite loop
         * (and by the way it does'nt make any sense at all).
         * So errors need to be printed out directly.
         */
        if (!is_int($error_nr)) {
            print "Error: Illegal argument type for argument 1 in function ErrorUtility::printError(). Integer expected, found '".gettype($error_nr)."' instead.";
            exit(1);
        }
        if (!is_string($description)) {
            print "Error: Illegal argument type for argument 2 in function ErrorUtility::printError(). String expected, found '".gettype($description)."' instead.";
            exit(1);
        }
        if (!is_string($file)) {
            print "Error: Illegal argument type for argument 3 in function ErrorUtility::printError(). String expected, found '".gettype($file)."' instead.";
            exit(1);
        }
        if (!is_int($line_nr)) {
            print "Error: Illegal argument type for argument 4 in function ErrorUtility::printError(). Integer expected, found '".gettype($line_nr)."' instead.";
            exit(1);
        }

        /* to check error reporting levels fall back to PHP's default settings */
        if ($error_nr === E_USER_ASSERT) {
            $error_level = E_USER_WARNING;
        } else if ($error_nr === E_UNKNOWN_ERROR) {
            $error_level = E_NOTICE;
        }

        $error_reporting = error_reporting();

        /* print an error only if the current error reporting level is high enough */
        if (($error_reporting & ~$error_nr) !== $error_reporting) {
            $error_message = ErrorUtility::_formatError($error_nr, $description, $file, $line_nr, true);
            print $error_message;
        } else {
            /* intetionally left blank */
        }
    }

    /**
     * custom error handler
     *
     * this implements logging of errors
     *
     * Be aware that error logs can get very large very soon, if not reset or deleted frequently.
     * Don't use the logging feature in a productive environment.
     *
     * As of PHP 5 there is an additional error reporting level called E_STRICT to document PHP 4
     * features that have been marked deprecated in PHP 5. But some of those 'deprecated features'
     * are still needed for backwards compatibility reasons. So using 'E_STRICT' will posssibly
     * flood your error logs or even crash the script.
     *
     * @access  public
     * @param   int     $error_nr
     * @param   string  $description
     * @param   string  $file
     * @param   int     $line_nr
     * @static
     * @ignore
     */
    function logError($error_nr, $description, $file, $line_nr)
    {
        /* NOTE: to trigger an user error inside an user error handler could cause an infinite loop
         * (and by the way it does'nt make any sense at all).
         * So errors need to be printed out directly.
         */
        if (!is_int($error_nr)) {
            print "Error: Illegal argument type for argument 1 in function ErrorUtility::logError(). Integer expected, found '".gettype($error_nr)."' instead.";
            exit(1);
        }
        if (!is_string($description)) {
            print "Error: Illegal argument type for argument 2 in function ErrorUtility::logError(). String expected, found '".gettype($description)."' instead.";
            exit(1);
        }
        if (!is_string($file)) {
            print "Error: Illegal argument type for argument 3 in function ErrorUtility::logError(). String expected, found '".gettype($file)."' instead.";
            exit(1);
        }
        if (!is_int($line_nr)) {
            print "Error: Illegal argument type for argument 4 in function ErrorUtility::logError(). Integer expected, found '".gettype($line_nr)."' instead.";
            exit(1);
        }

        /* to check error reporting levels fall back to PHP's default settings */
        if ($error_nr === E_USER_ASSERT) {
            $error_level = E_USER_WARNING;
        } else if ($error_nr === E_UNKNOWN_ERROR) {
            $error_level = E_NOTICE;
        } else {
            $error_level = $error_nr;
        }
                       
        $error_reporting = error_reporting();

        /* log an error only if the current error reporting level is high enough */
        if (($error_reporting & ~$error_level) !== $error_reporting) {
            
            $error_message = ErrorUtility::_formatError($error_nr, $description, $file, $line_nr, false);
            $error_log     = new log($error_message);
            $filename      = 'cache/error.log';

            if (error_log($error_log->getLog()."\n", 3, $filename) === false) {
                print "Cannot write to file '$filename'.";
                exit(1);
            }

        }
    }

    /**
     * handle failed assertions
     *
     * Note: actually in PHP an assertion is treated as an "E_WARNING".
     * There is no such thing like an "E_ASSERT" error level.
     *
     * @access  public
     * @param   string  $file
     * @param   int     $line_nr
     * @param   string  $description
     * @static
     * @ignore
     */
    function logAssertion($file, $line_nr, $description)
    {
        if (!is_string($file)) {
            print "Error: Illegal argument type for argument 1 in function ErrorUtility::logAssertion(). String expected, found '".gettype($file)."' instead.";
            exit(1);
        }
        if (!is_int($line_nr)) {
            print "Error: Illegal argument type for argument 2 in function ErrorUtility::logAssertion(). Integer expected, found '".gettype($line_nr)."' instead.";
            exit(1);
        }
        if (!is_string($description)) {
            print "Error: Illegal argument type for argument 3 in function ErrorUtility::logAssertion(). String expected, found '".gettype($description)."' instead.";
            exit(1);
        }
        ErrorUtility::logError(E_USER_ASSERT, $description, $file, $line_nr);
    }

    /**
     * print failed assertions
     *
     * @access  public
     * @param   string  $file
     * @param   int     $line_nr
     * @param   string  $description
     * @static
     * @ignore
     */
    function printAssertion($file, $line_nr, $description)
    {
        if (!is_string($file)) {
            print "Error: Illegal argument type for argument 1 in function ErrorUtility::printAssertion(). String expected, found '".gettype($file)."' instead.";
            exit(1);
        }
        if (!is_int($line_nr)) {
            print "Error: Illegal argument type for argument 2 in function ErrorUtility::printAssertion(). Integer expected, found '".gettype($line_nr)."' instead.";
            exit(1);
        }
        if (!is_string($description)) {
            print "Error: Illegal argument type for argument 3 in function ErrorUtility::printAssertion(). String expected, found '".gettype($description)."' instead.";
            exit(1);
        }
        print ErrorUtility::_formatError(E_USER_ASSERT, $description, $file, $line_nr, true);
    }

    /**
     * Set error reporting level
     *
     * You may use the following constants:
     * <ol>
     * <li>  YANA_ERROR_ON = catch all errors and print to screen  </li>
     * <li>  YANA_ERROR_OFF = do not report any errors or messages  </li>
     * <li>  YANA_ERROR_LOG = write errors and messages to a log file  </li>
     * </ol>
     *
     * @access  public
     * @static
     * @name    index::errorreporting()
     * @param   integer|string    $errorLevel    examples: YANA_ERROR_OFF, YANA_ERROR_LOG, YANA_ERROR_ON or E_ALL, E_ALL & ~NOTICE
     * @uses    Index::errorReporting(YANA_ERROR_OFF)
     */
    function setErrorReporting($errorLevel)
    {
        /* Note: method overloading is not available in PHP. (like Perl, but unlike C++ or Java)
         * The following work-around is a common way to simulate that feature.
         */
        
        /* Integer */ 
        if (is_int($errorLevel)) {
            ErrorUtility::_setLevelOfErrorHandling($errorLevel);

        /* String */
        } else if (is_string($errorLevel)) {
            ErrorUtility::_setTypeOfErrorHandling($errorLevel);

        /* Other input */
        } else {
            trigger_error("Illegal argument type. Integer or string expected, found '".gettype($errorLevel)."' instead.", E_USER_ERROR);
        }
    }

    /**
     * Set error reporting level via PHP's error_reporting() function
     *
     * returns previous error level or bool(false) on error
     *
     * @access  private
     * @static
     * @name    index::_settypeoferrorhandling()
     * @param   int    $errorLevel    examples: E_ALL, E_ALL & ~E_NOTICE
     * @return  int|bool(false)
     *
     * @ignore
     */
    function _setLevelOfErrorHandling($errorLevel = null)
    {
        if (is_null($errorLevel)) {
            return error_reporting();
        } else if (!is_int($errorLevel)) {
            trigger_error("Illegal argument type. Integer expected, found '".gettype($errorLevel)."' instead.", E_USER_ERROR);
            return false;
        } else {
            return error_reporting($errorLevel);
        }
    }

    /**
     * Set error reporting to on, off, or log
     *
     * @access  private
     * @static
     * @name    index::_settypeoferrorhandling()
     * @param   string    $errorLevel    YANA_ERROR_OFF|YANA_ERROR_LOG|YANA_ERROR_ON
     * @return  string|bool(false)
     *
     * @ignore
     */
    function _setTypeOfErrorHandling($errorLevel)
    {
        if (defined('YANA_ERROR_REPORTING') && $errorLevel !== YANA_ERROR_REPORTING) {
            trigger_error('Error reporting level has already been set and can not be redefined.', E_USER_WARNING);
            return false;
        } else if (!is_string($errorLevel)) {
            trigger_error("Illegal argument type. String expected, found '".gettype($errorLevel)."' instead.", E_USER_ERROR);
            return false;
        } else {
            switch ($errorLevel)
            {
                case YANA_ERROR_LOG:
                    /* Write errors to a log file, rather than passing them to a browser.
                     */
                    if (defined('E_STRICT')) {
                        error_reporting(E_ALL & ~E_STRICT);
                    } else {
                        error_reporting(E_ALL);
                    }
                    set_error_handler(array('ErrorUtility','logError'));
                    assert_options( ASSERT_ACTIVE,     1 );
                    assert_options( ASSERT_CALLBACK,   array('ErrorUtility','logAssertion') );
                    assert_options( ASSERT_BAIL,       0 );
                    assert_options( ASSERT_WARNING,    0 );
                    assert_options( ASSERT_QUIET_EVAL, 0 );
                break;
                case YANA_ERROR_ON:
                    /* For debugging only:
                     * show all errors, warnings and notices and evaluate assertions.
                     */
                    if (defined('E_STRICT')) {
                        error_reporting(E_ALL & ~E_STRICT);
                    } else {
                        error_reporting(E_ALL);
                    }
                    set_error_handler(array('ErrorUtility','printError'));
                    assert_options( ASSERT_ACTIVE,     1 );
                    assert_options( ASSERT_CALLBACK,   array('ErrorUtility','printAssertion') );
                    assert_options( ASSERT_BAIL,       0 );
                    assert_options( ASSERT_WARNING,    0 );
                    assert_options( ASSERT_QUIET_EVAL, 0 );
                break;
                case YANA_ERROR_OFF: default:
                    /* Prevent PHP from showing error messages to avoid information leak to hackers.
                     * Do no evaluate assertions for better performance.
                     */
                    error_reporting(                  0);
                    assert_options( ASSERT_ACTIVE,    0);
                    $errorLevel = YANA_ERROR_OFF;
                break;
            }
            if (!defined('YANA_ERROR_REPORTING')) {
                /**
                 * what to do with errors and system messages
                 *
                 * This constant reflects the way how errors and other messages are treated.
                 * It can be one of the following: 'on', 'off' or 'log'.
                 *
                 * Meaning:
                 * <ol>
                 * <li>  on = catch all errors and print to screen  </li>
                 * <li>  off = do not report any errors or messages  </li>
                 * <li>  log = write errors and messages to a log file  </li>
                 * </ol>
                 */
                define('YANA_ERROR_REPORTING', $errorLevel);
            }
            return YANA_ERROR_REPORTING;
        }
    }

    /**
     * format error messages
     *
     * @access  private
     * @static
     * @param   int     $error_nr
     * @param   string  $description
     * @param   string  $file
     * @param   int     $line_nr
     * @param   bool    $as_html
     * @return  string
     * @ignore
     */
    function _formatError($error_nr, $description, $file, $line_nr, $as_html)
    {
        $base_style = 'font-size: 13px; font-weight: normal; padding: 5px; border: 1px solid #888; text-align: left;';
        $show_details = true;
        /* for readability do not report errors twice */
        global $YANA;
        if (isset($YANA)) {
            if ($YANA->getVar('lasterror.nr') === $error_nr && $YANA->getVar('lasterror.file') === $file && $YANA->getVar('lasterror.line') === $line_nr) {
                if ($YANA->getVar('lasterror.description') === $description) {
                    if ($YANA->getVar('lasterror.hasmore') === true) {
                        $error_message = '';
                    } else {
                        $YANA->setVar('lasterror.hasmore', true);
                        if ($as_html === true) {
                            $error_message = '<div style="'.$base_style.'"><pre>'."\t".'... the previous error was reported multiple times.</pre></div>';
                        } else {
                            $error_message = "\t... the previous error was reported multiple times.";
                        }
                    }
                    return $error_message;
                } else {
                    $show_details = false;
                }
            } else {
                $YANA->setVar('lasterror.hasmore',     false);
                $YANA->setVar('lasterror.nr',          $error_nr);
                $YANA->setVar('lasterror.description', $description);
                $YANA->setVar('lasterror.file',        $file);
                $YANA->setVar('lasterror.line',        $line_nr);
            }
        }

        $colors    = array (
        'critical'        => array('color' => '#f00', 'background' => '#fee'),
        'compiler'        => array('color' => '#000', 'background' => '#fff'),
        'error'           => array('color' => '#a00', 'background' => '#fff'),
        'warning'         => array('color' => '#d60', 'background' => '#fff'),
        'notice'          => array('color' => '#038', 'background' => '#fff'),
        'assert'          => array('color' => '#000', 'background' => '#ff8')
        );
        $errortype = array (
        E_CORE_ERROR      => array('Core Error',       $colors['critical']),
        E_CORE_WARNING    => array('Core Warning',     $colors['critical']),
        E_COMPILE_ERROR   => array('Compile Error',    $colors['compiler']),
        E_COMPILE_WARNING => array('Compile Warning',  $colors['compiler']),
        E_PARSE           => array('Parsing Error',    $colors['compiler']),
        E_ERROR           => array('Error',            $colors['error']),
        E_USER_ERROR      => array('Yana Error',       $colors['error']),
        E_WARNING         => array('Warning',          $colors['warning']),
        E_USER_WARNING    => array('Yana Warning',     $colors['warning']),
        E_NOTICE          => array('Notice',           $colors['notice']),
        E_USER_NOTICE     => array('Yana Notice',      $colors['notice']),
        E_USER_ASSERT     => array('Assertion failed', $colors['assert']),
        E_UNKNOWN_ERROR   => array('Unknown Error',    $colors['notice'])
        );

        /* Note: the E_STRICT error level was introduced in PHP 5 and is not present in PHP 4.
         * As of PHP 5, E_STRICT is not included in E_ALL, but it will be in PHP 6.
         *
         * Trying to catch E_STRICT Notices does not make sense unless you want to port a PHP 4
         * script to PHP version 5, or (someday soon) PHP version 6 and want to make sure you
         * did'nt miss any deprecated features.
         */
        if(defined('E_STRICT')) {
            $errortype[E_STRICT]             = array("Runtime Notice",        $colors['notice']);
        }

        /* Note: the E_RECOVERABLE_ERRROR error level was introduced in PHP 5.2 and is not
         * present in PHP 4 nor PHP 5 < 5.2.
         */
        if(defined('E_RECOVERABLE_ERRROR')) {
            $errortype[E_RECOVERABLE_ERRROR] = array("Catchable Fatal Error", $colors['warning']);
        }
        if (!isset($errortype[$error_nr])) {
            $error_nr = E_UNKNOWN_ERROR;
        }
        assert('isset($errortype[$error_nr]);');

        /* Note: for readability assertions can have a description in form of a comment.
         * Example: assert('some_test; // comment');
         *
         * Where a comment is provided, this function will show the comment rather than
         * the assert code.
         *
         * Example of usage:
         * assert('$input >= 3 and $input <= 15; // argument '$input' is out of range [3..15]');
         */
        if ($error_nr === E_USER_ASSERT) {
            $description = preg_replace('/^.*;\s*(?:\/\/|\/\*|#)\s*(\S+.*)\s*(?:\*\/)?\s*$/Us', '${1}', $description);
            if ($as_html !== true) {
                $description = "Assertion $description failed";
            }
        }
        assert('!isset($shortenFilepath); /* Cannot redeclare variable $backtrace */');
        $shortenFilepath = '/^'.toRegExp(getcwd()).'/';

        /* Backtracing */
        if ($show_details === true && function_exists('debug_backtrace') && ($error_nr === E_USER_ERROR || $error_nr === E_ERROR || $error_nr === E_USER_WARNING || $error_nr === E_WARNING)) {
            /* Note: debug_backtrace became available in PHP 4.3 */
            assert('!isset($temp); /* Cannot redeclare variable $temp */');
            assert('!isset($backtrace); /* Cannot redeclare variable $backtrace */');
            $temp = debug_backtrace();
            /* Format backtrace */
            $backtrace = array();
            assert('!isset($i); /* Cannot redeclare variable $i */');
            for ($i=0;$i<count($temp);$i++)
            {
                if (isset($temp[$i]['class']) && strcasecmp($temp[$i]['class'], 'ErrorUtility') === 0) {
                    continue;
                }
                /* initialize vars */
                $i1 = count($backtrace);
                $backtrace[$i1] = "";

                /* shorten file path for readability */
                if (isset($temp[$i]['file'])) {
                    assert('!isset($temp_file); /* Cannot redeclare variable $temp_file */');
                    assert('isset($shortenFilepath);');
                    $temp_file = preg_replace($shortenFilepath,'.',$temp[$i]['file']);
                }

                if (isset($temp[$i]['class'])) {
                    $backtrace[$i1] .= $temp[$i]['class'];
                    if (isset($temp[$i]['type'])) {
                        $backtrace[$i1] .= $temp[$i]['type'];
                    }
                }
                if (isset($temp[$i]['function'])) {
                    if (preg_match('/^(?:trigger_error|user_error)$/i', $temp[$i]['function'])) {
                        if (isset($temp_file)) {
                            $backtrace[$i1] = "Error was raised in file '".$temp_file."'";
                            if (isset($temp[$i]['line'])) {
                                $backtrace[$i1] .= " on line ".$temp[$i]['line'];
                            }                            
                            unset($temp_file);
                        }
                        continue;
                    } else {
                        $backtrace[$i1] .= $temp[$i]['function'];
                        $backtrace[$i1] .= '(';
                        if (isset($temp[$i]['args']) && is_array($temp[$i]['args'])) {
                            assert('!isset($j); /* Cannot redeclare variable $j */');
                            for ($j=0;$j<count($temp[$i]['args']);$j++)
                            {
                                if ($j > 0) {
                                    $backtrace[$i1] .= ', ';
                                }
                                $backtrace[$i1] .= gettype($temp[$i]['args'][$j]);
                            }
                            unset($j);
                        }
                        $backtrace[$i1] .= ')';
                    }
                }
                if (isset($temp_file)) {
                    /* hide path for files within the framework */
                    if (!preg_match('/^\.(?:\Wincludes\W|\Wengine\W|\Windex\.php$)/i',$temp_file)) {
                        $backtrace[$i1] .= "    in file '".$temp_file."'";
                        if (isset($temp[$i]['line'])) {
                            $backtrace[$i1] .= " on line ".$temp[$i]['line'];
                        }
                    }
                }
                unset($temp_file);
            } /* end for */
            unset($i);
            unset($temp);
        } else {
            $backtrace = null;
        }

        /* shorten file path for readability */
        assert('isset($shortenFilepath);');
        $file = preg_replace($shortenFilepath, '.', $file);

        if ($as_html === true) {
            $style          = 'color: '.$errortype[$error_nr][1]['color'].'; background: '.$errortype[$error_nr][1]['background'].';';
            $error_message  = '<div style="'.$base_style.$style.'"><pre>';
            if ($show_details) {
                $error_message .= '<span style="font-weight: bold; text-decoration: underline;">'.$errortype[$error_nr][0]."</span>\n";
                $error_message .= "  description:\t".$description."\n";
                $error_message .= "  file:\t\t$file\n";
                $error_message .= "  line:\t\t$line_nr</pre>";
                if (!is_null($backtrace)) {
                    $error_message .= '<div><pre><span style="font-weight: bold; font-style: italic;">Backtrace</span>';
                    $error_message .= "\n\t".implode("\n\t",$backtrace);
                    $error_message .= '</pre></div>';
                }
            } else {
                $error_message .= $description;
            }
            $error_message .= "</div>";
        } else {
            $error_message = $errortype[$error_nr][0].': '.$description." in file '$file' on line $line_nr.";
        }

        return $error_message;
    }

}