<?php rcs_id('$Id: ErrorManager.php,v 1.43 2005/04/11 19:41:23 rurban Exp $');

if (isset($GLOBALS['ErrorManager'])) return;

// php5: ignore E_STRICT (var warnings)
/*
if (defined('E_STRICT') 
    and (E_ALL & E_STRICT)
    and (error_reporting() & E_STRICT)) {
    echo " errormgr: error_reporting=", error_reporting();
    echo "\nplease fix that in your php.ini!";
    error_reporting(E_ALL & ~E_STRICT);
}
*/
define ('EM_FATAL_ERRORS', E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | ~2048);
define ('EM_WARNING_ERRORS',
	E_WARNING | E_CORE_WARNING | E_COMPILE_WARNING | E_USER_WARNING);
define ('EM_NOTICE_ERRORS', E_NOTICE | E_USER_NOTICE);

/* It is recommended to leave assertions on. 
   You can simply comment the two lines below to leave them on.
   Only where absolute speed is necessary you might want to turn 
   them off.
*/
if (1 or (defined('DEBUG') and DEBUG))
    assert_options (ASSERT_ACTIVE, 1);
else
    assert_options (ASSERT_ACTIVE, 0);
assert_options (ASSERT_CALLBACK, 'wiki_assert_handler');

function wiki_assert_handler ($file, $line, $code) {
    ErrorManager_errorHandler( $code, sprintf("<br />%s:%s: %s: Assertion failed <br />", $file, $line, $code), $file, $line);
}

/**
 * A class which allows custom handling of PHP errors.
 *
 * This is a singleton class. There should only be one instance
 * of it --- you can access the one instance via $GLOBALS['ErrorManager'].
 *
 * FIXME: more docs.
 */ 
class ErrorManager 
{
    /**
     * Constructor.
     *
     * As this is a singleton class, you should never call this.
     * @access private
     */
    function ErrorManager() {
        $this->_handlers = array();
        $this->_fatal_handler = false;
        $this->_postpone_mask = 0;
        $this->_postponed_errors = array();

        set_error_handler('ErrorManager_errorHandler');
    }

    /**
     * Get mask indicating which errors are currently being postponed.
     * @access public
     * @return int The current postponed error mask.
     */
    function getPostponedErrorMask() {
        return $this->_postpone_mask;
    }

    /**
     * Set mask indicating which errors to postpone.
     *
     * The default value of the postpone mask is zero (no errors postponed.)
     *
     * When you set this mask, any queue errors which do not match the new
     * mask are reported.
     *
     * @access public
     * @param $newmask int The new value for the mask.
     */
    function setPostponedErrorMask($newmask) {
        $this->_postpone_mask = $newmask;
        if (function_exists('PrintXML'))
            PrintXML($this->_flush_errors($newmask));
        else
            echo($this->_flush_errors($newmask));

    }

    /**
     * Report any queued error messages.
     * @access public
     */
    function flushPostponedErrors() {
        if (function_exists('PrintXML'))
            PrintXML($this->_flush_errors());
        else
            echo $this->_flush_errors();
    }

    /**
     * Get postponed errors, formatted as HTML.
     *
     * This also flushes the postponed error queue.
     *
     * @return object HTML describing any queued errors (or false, if none). 
     */
    function getPostponedErrorsAsHTML() {
        $flushed = $this->_flush_errors();
        if (!$flushed)
            return false;
        if ($flushed->isEmpty())
            return false;
        // format it with the worst class (error, warning, notice)
        $worst_err = $flushed->_content[0];
        foreach ($flushed->_content as $err) {
            if ($err and isa($err, 'PhpError') and $err->errno > $worst_err->errno) {
                $worst_err = $err;
            }
        }
        if ($worst_err->isNotice())
            return $flushed;
        $class = $worst_err->getHtmlClass(); 
        $html = HTML::div(array('style' => 'border: none', 'class' => $class),
                          HTML::h4(array('class' => 'errors'), 
                                   "PHP " . $worst_err->getDescription()));
        $html->pushContent($flushed);
        return $html;
    }
    
    /**
     * Push a custom error handler on the handler stack.
     *
     * Sometimes one is performing an operation where one expects
     * certain errors or warnings. In this case, one might not want
     * these errors reported in the normal manner. Installing a custom
     * error handler via this method allows one to intercept such
     * errors.
     *
     * An error handler installed via this method should be either a
     * function or an object method taking one argument: a PhpError
     * object.
     *
     * The error handler should return either:
     * <dl>
     * <dt> False <dd> If it has not handled the error. In this case,
     *                 error processing will proceed as if the handler
     *                 had never been called: the error will be passed
     *                 to the next handler in the stack, or the
     *                 default handler, if there are no more handlers
     *                 in the stack.
     *
     * <dt> True <dd> If the handler has handled the error. If the
     *                error was a non-fatal one, no further processing
     *                will be done. If it was a fatal error, the
     *                ErrorManager will still terminate the PHP
     *                process (see setFatalHandler.)
     *
     * <dt> A PhpError object <dd> The error is not considered
     *                             handled, and will be passed on to
     *                             the next handler(s) in the stack
     *                             (or the default handler). The
     *                             returned PhpError need not be the
     *                             same as the one passed to the
     *                             handler. This allows the handler to
     *                             "adjust" the error message.
     * </dl>
     * @access public
     * @param $handler WikiCallback  Handler to call.
     */
    function pushErrorHandler($handler) {
        array_unshift($this->_handlers, $handler);
    }

    /**
     * Pop an error handler off the handler stack.
     * @access public
     */
    function popErrorHandler() {
        return array_shift($this->_handlers);
    }

    /**
     * Set a termination handler.
     *
     * This handler will be called upon fatal errors. The handler
     * gets passed one argument: a PhpError object describing the
     * fatal error.
     *
     * @access public
     * @param $handler WikiCallback  Callback to call on fatal errors.
     */
    function setFatalHandler($handler) {
        $this->_fatal_handler = $handler;
    }

    /**
     * Handle an error.
     *
     * The error is passed through any registered error handlers, and
     * then either reported or postponed.
     *
     * @access public
     * @param $error object A PhpError object.
     */
    function handleError($error) {
        static $in_handler;

        if (!empty($in_handler)) {
            $msg = $error->_getDetail();
            $msg->unshiftContent(HTML::h2(fmt("%s: error while handling error:",
                                              "ErrorManager")));
            $msg->printXML();
            return;
        }
        $in_handler = true;

        foreach ($this->_handlers as $handler) {
            if (!$handler) continue;
            $result = $handler->call($error);
            if (!$result) {
                continue;       // Handler did not handle error.
            }
            elseif (is_object($result)) {
                // handler filtered the result. Still should pass to
                // the rest of the chain.
                if ($error->isFatal()) {
                    // Don't let handlers make fatal errors non-fatal.
                    $result->errno = $error->errno;
                }
                $error = $result;
            }
            else {
                // Handler handled error.
                if (!$error->isFatal()) {
                    $in_handler = false;
                    return;
                }
                break;
            }
        }

        $this->_noCacheHeaders();

        // Error was either fatal, or was not handled by a handler.
        // Handle it ourself.
        if ($error->isFatal()) {
            echo "<html><body><div style=\"font-weight:bold; color:red\">Fatal Error:</div>\n";
            if (defined('DEBUG') and (DEBUG & _DEBUG_TRACE)) {
                echo "error_reporting=",error_reporting(),"\n<br>";
                if (function_exists("debug_backtrace")) // >= 4.3.0
                    $error->printSimpleTrace(debug_backtrace());
            }
            $this->_die($error);
        }
        else if (($error->errno & error_reporting()) != 0) {
            if  (($error->errno & $this->_postpone_mask) != 0) {
                if ((function_exists('is_a') and is_a($error,'PhpErrorOnce'))
                    or (!function_exists('is_a') and 
                    (
                     // stdlib independent isa()
                     (strtolower(get_class($error)) == 'phperroronce')
                     or (is_subclass_of($error, 'PhpErrorOnce'))))) {
                    $error->removeDoublettes($this->_postponed_errors);
                    if ( $error->_count < 2 )
                        $this->_postponed_errors[] = $error;
                } else {
                    $this->_postponed_errors[] = $error;
                }
            }
            else {
                //echo "postponed errors: ";
                if (defined('DEBUG') and (DEBUG & _DEBUG_TRACE)) {
                    echo "error_reporting=",error_reporting(),"\n";
                    if (function_exists("debug_backtrace")) // >= 4.3.0
                        $error->printSimpleTrace(debug_backtrace());
                }
                $error->printXML();
            }
        }
        $in_handler = false;
    }

    function warning($msg, $errno = E_USER_NOTICE) {
        $this->handleError(new PhpWikiError($errno, $msg));
    }
    
    /**
     * @access private
     */
    function _die($error) {
        //echo "\n\n<html><body>";
        $error->printXML();
        PrintXML($this->_flush_errors());
        if ($this->_fatal_handler)
            $this->_fatal_handler->call($error);
        exit -1;
    }

    /**
     * @access private
     */
    function _flush_errors($keep_mask = 0) {
        $errors = &$this->_postponed_errors;
        if (empty($errors)) return '';
        $flushed = HTML();
        for ($i=0; $i<count($errors); $i++) {
            $error =& $errors[$i];
            if (!is_object($error)) {
                continue;
            }
            if (($error->errno & $keep_mask) != 0)
                continue;
            unset($errors[$i]);
            $flushed->pushContent($error);
        }
        return $flushed;
    }

    function _noCacheHeaders() {
        global $request;
        static $already = false;

        if (isset($request) and isset($request->_validators)) {
            $request->_validators->_tag = false;
            $request->_validators->_mtime = false;
        }
        if ($already) return;
        
        // FIXME: Howto announce that to Request->cacheControl()?
        if (!headers_sent()) {
            header( "Cache-control: no-cache" );
            header( "Pragma: nocache" );
        }
        $already = true;
    }
}

/**
 * Global error handler for class ErrorManager.
 *
 * This is necessary since PHP's set_error_handler() does not allow
 * one to set an object method as a handler.
 * 
 * @access private
 */
function ErrorManager_errorHandler($errno, $errstr, $errfile, $errline) 
{
    if (!isset($GLOBALS['ErrorManager'])) {
      $GLOBALS['ErrorManager'] = new ErrorManager;
    }
	
    $error = new PhpErrorOnce($errno, $errstr, $errfile, $errline);
    $GLOBALS['ErrorManager']->handleError($error);
}


/**
 * A class representing a PHP error report.
 *
 * @see The PHP documentation for set_error_handler at
 *      http://php.net/manual/en/function.set-error-handler.php .
 */
class PhpError {
    /**
     * The PHP errno
     */
    //var $errno;

    /**
     * The PHP error message.
     */
    //var $errstr;

    /**
     * The source file where the error occurred.
     */
    //var $errfile;

    /**
     * The line number (in $this->errfile) where the error occured.
     */
    //var $errline;

    /**
     * Construct a new PhpError.
     * @param $errno   int
     * @param $errstr  string
     * @param $errfile string
     * @param $errline int
     */
    function PhpError($errno, $errstr, $errfile, $errline) {
        $this->errno   = $errno;
        $this->errstr  = $errstr;
        $this->errfile = $errfile;
        $this->errline = $errline;
    }

    /**
     * Determine whether this is a fatal error.
     * @return boolean True if this is a fatal error.
     */
    function isFatal() {
        return ($this->errno & (2048|EM_WARNING_ERRORS|EM_NOTICE_ERRORS)) == 0;
    }

    /**
     * Determine whether this is a warning level error.
     * @return boolean
     */
    function isWarning() {
        return ($this->errno & EM_WARNING_ERRORS) != 0;
    }

    /**
     * Determine whether this is a notice level error.
     * @return boolean
     */
    function isNotice() {
        return ($this->errno & EM_NOTICE_ERRORS) != 0;
    }
    function getHtmlClass() {
        if ($this->isNotice()) {
            return 'hint';
        } elseif ($this->isWarning()) {
            return 'warning';
        } else {
            return 'errors';
        }
    }
    
    function getDescription() {
        if ($this->isNotice()) {
            return 'Notice';
        } elseif ($this->isWarning()) {
            return 'Warning';
        } else {
            return 'Error';
        }
    }

    /**
     * Get a printable, HTML, message detailing this error.
     * @return object The detailed error message.
     */
    function _getDetail() {
        $dir = defined('PHPWIKI_DIR') ? PHPWIKI_DIR : substr(dirname(__FILE__),0,-4);
        if (substr(PHP_OS,0,3) == 'WIN') {
           $dir = str_replace('/','\\',$dir);
           $this->errfile = str_replace('/','\\',$this->errfile);
           $dir .= "\\";
        } else 
           $dir .= '/';
        $errfile = preg_replace('|^' . preg_quote($dir) . '|', '', $this->errfile);
        $lines = explode("\n", $this->errstr);
        if (DEBUG & _DEBUG_VERBOSE) {
          $msg = sprintf("%s:%d: %s[%d]: %s",
                         $errfile, $this->errline,
                         $this->getDescription(), $this->errno,
                         array_shift($lines));
        } else {
          $msg = sprintf("%s:%d: %s: \"%s\"",
                         $errfile, $this->errline,
                         $this->getDescription(),
                         array_shift($lines));
        }
        
        $html = HTML::div(array('class' => $this->getHtmlClass()), HTML::p($msg));
        // The class is now used for the div container.
        // $html = HTML::div(HTML::p($msg));
        if ($lines) {
            $list = HTML::ul();
            foreach ($lines as $line)
                $list->pushContent(HTML::li($line));
            $html->pushContent($list);
        }
        
        return $html;
    }

    /**
     * Print an HTMLified version of this error.
     * @see asXML()
     */
    function printXML() {
        PrintXML($this->_getDetail());
    }

    /**
     * Return an HTMLified version of this error.
     */
    function asXML() {
        return AsXML($this->_getDetail());
    }

    /**
     * Return a plain-text version of this error.
     */
    function asString() {
        return AsString($this->_getDetail());
    }

    function printSimpleTrace($bt) {
        global $HTTP_SERVER_VARS;
        $nl = isset($HTTP_SERVER_VARS['REQUEST_METHOD']) ? "<br />" : "\n";
        echo $nl."Traceback:".$nl;
        foreach ($bt as $i => $elem) {
            if (!array_key_exists('file', $elem)) {
                continue;
            }
            print "  " . $elem['file'] . ':' . $elem['line'] . $nl;
        }
        flush();
    }
}

/**
 * A class representing a PhpWiki warning.
 *
 * This is essentially the same as a PhpError, except that the
 * error message is quieter: no source line, etc...
 */
class PhpWikiError extends PhpError {
    /**
     * Construct a new PhpError.
     * @param $errno   int
     * @param $errstr  string
     */
    function PhpWikiError($errno, $errstr) {
        $this->PhpError($errno, $errstr, '?', '?');
    }

    function _getDetail() {
        return HTML::div(array('class' => $this->getHtmlClass()), 
                         HTML::p($this->getDescription() . ": $this->errstr"));
    }
}

/**
 * A class representing a Php warning, printed only the first time.
 *
 * Similar to PhpError, except only the first same error message is printed, 
 * with number of occurences.
 */
class PhpErrorOnce extends PhpError {

    function PhpErrorOnce($errno, $errstr, $errfile, $errline) {
        $this->_count = 1;
        $this->PhpError($errno, $errstr, $errfile, $errline);
    }

    function _sameError($error) {
        if (!$error) return false;
        return ($this->errno == $error->errno and
                $this->errfile == $error->errfile and
                $this->errline == $error->errline);
    }

    // count similar handlers, increase _count and remove the rest
    function removeDoublettes(&$errors) {
        for ($i=0; $i < count($errors); $i++) {
            if (!isset($errors[$i])) continue;
            if ($this->_sameError($errors[$i])) {
                $errors[$i]->_count++;
                $this->_count++;
                if ($i) unset($errors[$i]);
            }
        }
        return $this->_count;
    }
    
    function _getDetail($count=0) {
    	if (!$count) $count = $this->_count;
	    $dir = defined('PHPWIKI_DIR') ? PHPWIKI_DIR : substr(dirname(__FILE__),0,-4);
        if (substr(PHP_OS,0,3) == 'WIN') {
           $dir = str_replace('/','\\',$dir);
           $this->errfile = str_replace('/','\\',$this->errfile);
           $dir .= "\\";
        } else 
           $dir .= '/';
        $errfile = preg_replace('|^' . preg_quote($dir) . '|', '', $this->errfile);
        if (is_string($this->errstr))
	        $lines = explode("\n", $this->errstr);
	    elseif (is_object($this->errstr))
	        $lines = array($this->errstr->asXML());
        $errtype = (DEBUG & _DEBUG_VERBOSE) ? sprintf("%s[%d]", $this->getDescription(), $this->errno)
                                            : sprintf("%s", $this->getDescription());
        $msg = sprintf("%s:%d: %s: %s %s",
                       $errfile, $this->errline,
                       $errtype,
                       array_shift($lines),
                       $count > 1 ? sprintf(" (...repeated %d times)",$count) : ""
                       );
        $html = HTML::div(array('class' => $this->getHtmlClass()), 
                          HTML::p($msg));
        if ($lines) {
            $list = HTML::ul();
            foreach ($lines as $line)
                $list->pushContent(HTML::li($line));
            $html->pushContent($list);
        }
        
        return $html;
    }
}

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

if (!isset($GLOBALS['ErrorManager'])) {
    $GLOBALS['ErrorManager'] = new ErrorManager;
}

// $Log: ErrorManager.php,v $
// Revision 1.43  2005/04/11 19:41:23  rurban
// Improve postponed errors+warnins list layout.
//
// Revision 1.42  2005/02/26 18:29:07  rurban
// re-enable colored boxed errors
//
// Revision 1.41  2004/12/26 17:08:36  rurban
// php5 fixes: case-sensitivity, no & new
//
// Revision 1.40  2004/12/13 14:39:46  rurban
// aesthetics
//
// Revision 1.39  2004/11/05 18:04:20  rurban
// print errno only if _DEBUG_VERBOSE
//
// Revision 1.38  2004/10/19 17:34:55  rurban
// <4.3 fix
//
// Revision 1.37  2004/10/14 19:23:58  rurban
// remove debugging prints
//
// Revision 1.36  2004/10/12 15:35:43  rurban
// avoid Php Notice header
//
// Revision 1.35  2004/10/12 13:13:19  rurban
// php5 compatibility (5.0.1 ok)
//
// Revision 1.34  2004/09/24 18:52:19  rurban
// in deferred html error messages use the worst header and class
// (notice => warning => errors)
//
// Revision 1.33  2004/09/14 10:28:21  rurban
// use assert, maybe we should only turn it off for releases
//
// Revision 1.32  2004/07/08 13:50:32  rurban
// various unit test fixes: print error backtrace on _DEBUG_TRACE; allusers fix; new PHPWIKI_NOMAIN constant for omitting the mainloop
//
// Revision 1.31  2004/07/02 09:55:58  rurban
// more stability fixes: new DISABLE_GETIMAGESIZE if your php crashes when loading LinkIcons: failing getimagesize in old phps; blockparser stabilized
//
// Revision 1.30  2004/06/25 14:29:12  rurban
// WikiGroup refactoring:
//   global group attached to user, code for not_current user.
//   improved helpers for special groups (avoid double invocations)
// new experimental config option ENABLE_XHTML_XML (fails with IE, and document.write())
// fixed a XHTML validation error on userprefs.tmpl
//
// Revision 1.29  2004/06/20 15:30:04  rurban
// get_class case-sensitivity issues
//
// Revision 1.28  2004/06/16 11:51:04  rurban
// fixed typo: undefined object #235
//
// Revision 1.27  2004/06/13 09:38:20  rurban
// isa() workaround, if stdlib.php is not loaded
//
// Revision 1.26  2004/06/02 18:01:45  rurban
// init global FileFinder to add proper include paths at startup
//   adds PHPWIKI_DIR if started from another dir, lib/pear also
// fix slashify for Windows
// fix USER_AUTH_POLICY=old, use only USER_AUTH_ORDER methods (besides HttpAuth)
//
// Revision 1.25  2004/06/02 10:18:36  rurban
// assert only if DEBUG is non-false
//
// Revision 1.24  2004/05/27 17:49:05  rurban
// renamed DB_Session to DbSession (in CVS also)
// added WikiDB->getParam and WikiDB->getAuthParam method to get rid of globals
// remove leading slash in error message
// added force_unlock parameter to File_Passwd (no return on stale locks)
// fixed adodb session AffectedRows
// added FileFinder helpers to unify local filenames and DATA_PATH names
// editpage.php: new edit toolbar javascript on ENABLE_EDIT_TOOLBAR
//
//

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