]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/ErrorManager.php
re-enable colored boxed errors
[SourceForge/phpwiki.git] / lib / ErrorManager.php
1 <?php rcs_id('$Id: ErrorManager.php,v 1.42 2005-02-26 18:29:07 rurban Exp $');
2
3 if (isset($GLOBALS['ErrorManager'])) return;
4
5 // php5: ignore E_STRICT (var warnings)
6 /*
7 if (defined('E_STRICT') 
8     and (E_ALL & E_STRICT)
9     and (error_reporting() & E_STRICT)) {
10     echo " errormgr: error_reporting=", error_reporting();
11     echo "\nplease fix that in your php.ini!";
12     error_reporting(E_ALL & ~E_STRICT);
13 }
14 */
15 define ('EM_FATAL_ERRORS', E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | ~2048);
16 define ('EM_WARNING_ERRORS',
17         E_WARNING | E_CORE_WARNING | E_COMPILE_WARNING | E_USER_WARNING);
18 define ('EM_NOTICE_ERRORS', E_NOTICE | E_USER_NOTICE);
19
20 /* It is recommended to leave assertions on. 
21    You can simply comment the two lines below to leave them on.
22    Only where absolute speed is necessary you might want to turn 
23    them off.
24 */
25 if (1 or (defined('DEBUG') and DEBUG))
26     assert_options (ASSERT_ACTIVE, 1);
27 else
28     assert_options (ASSERT_ACTIVE, 0);
29 assert_options (ASSERT_CALLBACK, 'wiki_assert_handler');
30
31 function wiki_assert_handler ($file, $line, $code) {
32     ErrorManager_errorHandler( $code, sprintf("<br />%s:%s: %s: Assertion failed <br />", $file, $line, $code), $file, $line);
33 }
34
35 /**
36  * A class which allows custom handling of PHP errors.
37  *
38  * This is a singleton class. There should only be one instance
39  * of it --- you can access the one instance via $GLOBALS['ErrorManager'].
40  *
41  * FIXME: more docs.
42  */ 
43 class ErrorManager 
44 {
45     /**
46      * Constructor.
47      *
48      * As this is a singleton class, you should never call this.
49      * @access private
50      */
51     function ErrorManager() {
52         $this->_handlers = array();
53         $this->_fatal_handler = false;
54         $this->_postpone_mask = 0;
55         $this->_postponed_errors = array();
56
57         set_error_handler('ErrorManager_errorHandler');
58     }
59
60     /**
61      * Get mask indicating which errors are currently being postponed.
62      * @access public
63      * @return int The current postponed error mask.
64      */
65     function getPostponedErrorMask() {
66         return $this->_postpone_mask;
67     }
68
69     /**
70      * Set mask indicating which errors to postpone.
71      *
72      * The default value of the postpone mask is zero (no errors postponed.)
73      *
74      * When you set this mask, any queue errors which do not match the new
75      * mask are reported.
76      *
77      * @access public
78      * @param $newmask int The new value for the mask.
79      */
80     function setPostponedErrorMask($newmask) {
81         $this->_postpone_mask = $newmask;
82         if (function_exists('PrintXML'))
83             PrintXML($this->_flush_errors($newmask));
84         else
85             echo($this->_flush_errors($newmask));
86
87     }
88
89     /**
90      * Report any queued error messages.
91      * @access public
92      */
93     function flushPostponedErrors() {
94         if (function_exists('PrintXML'))
95             PrintXML($this->_flush_errors());
96         else
97             echo $this->_flush_errors();
98     }
99
100     /**
101      * Get postponed errors, formatted as HTML.
102      *
103      * This also flushes the postponed error queue.
104      *
105      * @return object HTML describing any queued errors (or false, if none). 
106      */
107     function getPostponedErrorsAsHTML() {
108         $flushed = $this->_flush_errors();
109         if (!$flushed)
110             return false;
111         if ($flushed->isEmpty())
112             return false;
113         // format it with the worst class (error, warning, notice)
114         //$class = 'notice';
115         $cur_err = new PhpError(0,"","","");
116         foreach ($flushed->_content as $err) {
117             if ($err and isa($err, 'PhpError') and $err->errno > $cur_err->errno) {
118                 $cur_err = $err;
119             }
120         }
121         if ($cur_err->isNotice())
122             return $flushed;
123         $class = $cur_err->getHtmlClass(); 
124         $html = HTML::div(array('class' => $class),
125                           HTML::h4(array('class' => 'errors'), 
126                                    "PHP " . $cur_err->getDescription()));
127         $html->pushContent($flushed);
128         return $html;
129     }
130     
131     /**
132      * Push a custom error handler on the handler stack.
133      *
134      * Sometimes one is performing an operation where one expects
135      * certain errors or warnings. In this case, one might not want
136      * these errors reported in the normal manner. Installing a custom
137      * error handler via this method allows one to intercept such
138      * errors.
139      *
140      * An error handler installed via this method should be either a
141      * function or an object method taking one argument: a PhpError
142      * object.
143      *
144      * The error handler should return either:
145      * <dl>
146      * <dt> False <dd> If it has not handled the error. In this case,
147      *                 error processing will proceed as if the handler
148      *                 had never been called: the error will be passed
149      *                 to the next handler in the stack, or the
150      *                 default handler, if there are no more handlers
151      *                 in the stack.
152      *
153      * <dt> True <dd> If the handler has handled the error. If the
154      *                error was a non-fatal one, no further processing
155      *                will be done. If it was a fatal error, the
156      *                ErrorManager will still terminate the PHP
157      *                process (see setFatalHandler.)
158      *
159      * <dt> A PhpError object <dd> The error is not considered
160      *                             handled, and will be passed on to
161      *                             the next handler(s) in the stack
162      *                             (or the default handler). The
163      *                             returned PhpError need not be the
164      *                             same as the one passed to the
165      *                             handler. This allows the handler to
166      *                             "adjust" the error message.
167      * </dl>
168      * @access public
169      * @param $handler WikiCallback  Handler to call.
170      */
171     function pushErrorHandler($handler) {
172         array_unshift($this->_handlers, $handler);
173     }
174
175     /**
176      * Pop an error handler off the handler stack.
177      * @access public
178      */
179     function popErrorHandler() {
180         return array_shift($this->_handlers);
181     }
182
183     /**
184      * Set a termination handler.
185      *
186      * This handler will be called upon fatal errors. The handler
187      * gets passed one argument: a PhpError object describing the
188      * fatal error.
189      *
190      * @access public
191      * @param $handler WikiCallback  Callback to call on fatal errors.
192      */
193     function setFatalHandler($handler) {
194         $this->_fatal_handler = $handler;
195     }
196
197     /**
198      * Handle an error.
199      *
200      * The error is passed through any registered error handlers, and
201      * then either reported or postponed.
202      *
203      * @access public
204      * @param $error object A PhpError object.
205      */
206     function handleError($error) {
207         static $in_handler;
208
209         if (!empty($in_handler)) {
210             $msg = $error->_getDetail();
211             $msg->unshiftContent(HTML::h2(fmt("%s: error while handling error:",
212                                               "ErrorManager")));
213             $msg->printXML();
214             return;
215         }
216         $in_handler = true;
217
218         foreach ($this->_handlers as $handler) {
219             if (!$handler) continue;
220             $result = $handler->call($error);
221             if (!$result) {
222                 continue;       // Handler did not handle error.
223             }
224             elseif (is_object($result)) {
225                 // handler filtered the result. Still should pass to
226                 // the rest of the chain.
227                 if ($error->isFatal()) {
228                     // Don't let handlers make fatal errors non-fatal.
229                     $result->errno = $error->errno;
230                 }
231                 $error = $result;
232             }
233             else {
234                 // Handler handled error.
235                 if (!$error->isFatal()) {
236                     $in_handler = false;
237                     return;
238                 }
239                 break;
240             }
241         }
242
243         $this->_noCacheHeaders();
244
245         // Error was either fatal, or was not handled by a handler.
246         // Handle it ourself.
247         if ($error->isFatal()) {
248             echo "<html><body><div style=\"font-weight:bold; color:red\">Fatal Error:</div>\n";
249             if (defined('DEBUG') and (DEBUG & _DEBUG_TRACE)) {
250                 echo "error_reporting=",error_reporting(),"\n<br>";
251                 if (function_exists("debug_backtrace")) // >= 4.3.0
252                     $error->printSimpleTrace(debug_backtrace());
253             }
254             $this->_die($error);
255         }
256         else if (($error->errno & error_reporting()) != 0) {
257             if  (($error->errno & $this->_postpone_mask) != 0) {
258                 if ((function_exists('is_a') and is_a($error,'PhpErrorOnce'))
259                     or (!function_exists('is_a') and 
260                     (
261                      // stdlib independent isa()
262                      (strtolower(get_class($error)) == 'phperroronce')
263                      or (is_subclass_of($error, 'PhpErrorOnce'))))) {
264                     $error->removeDoublettes($this->_postponed_errors);
265                     if ( $error->_count < 2 )
266                         $this->_postponed_errors[] = $error;
267                 } else {
268                     $this->_postponed_errors[] = $error;
269                 }
270             }
271             else {
272                 //echo "postponed errors: ";
273                 if (defined('DEBUG') and (DEBUG & _DEBUG_TRACE)) {
274                     echo "error_reporting=",error_reporting(),"\n";
275                     if (function_exists("debug_backtrace")) // >= 4.3.0
276                         $error->printSimpleTrace(debug_backtrace());
277                 }
278                 $error->printXML();
279             }
280         }
281         $in_handler = false;
282     }
283
284     function warning($msg, $errno = E_USER_NOTICE) {
285         $this->handleError(new PhpWikiError($errno, $msg));
286     }
287     
288     /**
289      * @access private
290      */
291     function _die($error) {
292         //echo "\n\n<html><body>";
293         $error->printXML();
294         PrintXML($this->_flush_errors());
295         if ($this->_fatal_handler)
296             $this->_fatal_handler->call($error);
297         exit -1;
298     }
299
300     /**
301      * @access private
302      */
303     function _flush_errors($keep_mask = 0) {
304         $errors = &$this->_postponed_errors;
305         if (empty($errors)) return '';
306         $flushed = HTML();
307         for ($i=0; $i<count($errors); $i++) {
308             $error =& $errors[$i];
309             if (!is_object($error)) {
310                 continue;
311             }
312             if (($error->errno & $keep_mask) != 0)
313                 continue;
314             unset($errors[$i]);
315             $flushed->pushContent($error);
316         }
317         return $flushed;
318     }
319
320     function _noCacheHeaders() {
321         global $request;
322         static $already = false;
323
324         if (isset($request) and isset($request->_validators)) {
325             $request->_validators->_tag = false;
326             $request->_validators->_mtime = false;
327         }
328         if ($already) return;
329         
330         // FIXME: Howto announce that to Request->cacheControl()?
331         if (!headers_sent()) {
332             header( "Cache-control: no-cache" );
333             header( "Pragma: nocache" );
334         }
335         $already = true;
336     }
337 }
338
339 /**
340  * Global error handler for class ErrorManager.
341  *
342  * This is necessary since PHP's set_error_handler() does not allow
343  * one to set an object method as a handler.
344  * 
345  * @access private
346  */
347 function ErrorManager_errorHandler($errno, $errstr, $errfile, $errline) 
348 {
349     if (!isset($GLOBALS['ErrorManager'])) {
350       $GLOBALS['ErrorManager'] = new ErrorManager;
351     }
352         
353     $error = new PhpErrorOnce($errno, $errstr, $errfile, $errline);
354     $GLOBALS['ErrorManager']->handleError($error);
355 }
356
357
358 /**
359  * A class representing a PHP error report.
360  *
361  * @see The PHP documentation for set_error_handler at
362  *      http://php.net/manual/en/function.set-error-handler.php .
363  */
364 class PhpError {
365     /**
366      * The PHP errno
367      */
368     //var $errno;
369
370     /**
371      * The PHP error message.
372      */
373     //var $errstr;
374
375     /**
376      * The source file where the error occurred.
377      */
378     //var $errfile;
379
380     /**
381      * The line number (in $this->errfile) where the error occured.
382      */
383     //var $errline;
384
385     /**
386      * Construct a new PhpError.
387      * @param $errno   int
388      * @param $errstr  string
389      * @param $errfile string
390      * @param $errline int
391      */
392     function PhpError($errno, $errstr, $errfile, $errline) {
393         $this->errno   = $errno;
394         $this->errstr  = $errstr;
395         $this->errfile = $errfile;
396         $this->errline = $errline;
397     }
398
399     /**
400      * Determine whether this is a fatal error.
401      * @return boolean True if this is a fatal error.
402      */
403     function isFatal() {
404         return ($this->errno & (2048|EM_WARNING_ERRORS|EM_NOTICE_ERRORS)) == 0;
405     }
406
407     /**
408      * Determine whether this is a warning level error.
409      * @return boolean
410      */
411     function isWarning() {
412         return ($this->errno & EM_WARNING_ERRORS) != 0;
413     }
414
415     /**
416      * Determine whether this is a notice level error.
417      * @return boolean
418      */
419     function isNotice() {
420         return ($this->errno & EM_NOTICE_ERRORS) != 0;
421     }
422     function getHtmlClass() {
423         if ($this->isNotice()) {
424             return 'hint';
425         } elseif ($this->isWarning()) {
426             return 'errors';
427         } else {
428             return 'errors';
429         }
430     }
431     
432     function getDescription() {
433         if ($this->isNotice()) {
434             return 'Notice';
435         } elseif ($this->isWarning()) {
436             return 'Warning';
437         } else {
438             return 'Error';
439         }
440     }
441
442     /**
443      * Get a printable, HTML, message detailing this error.
444      * @return object The detailed error message.
445      */
446     function _getDetail() {
447         $dir = defined('PHPWIKI_DIR') ? PHPWIKI_DIR : substr(dirname(__FILE__),0,-4);
448         if (substr(PHP_OS,0,3) == 'WIN') {
449            $dir = str_replace('/','\\',$dir);
450            $this->errfile = str_replace('/','\\',$this->errfile);
451            $dir .= "\\";
452         } else 
453            $dir .= '/';
454         $errfile = preg_replace('|^' . preg_quote($dir) . '|', '', $this->errfile);
455         $lines = explode("\n", $this->errstr);
456         if (DEBUG & _DEBUG_VERBOSE) {
457           $msg = sprintf("%s:%d: %s[%d]: %s",
458                          $errfile, $this->errline,
459                          $this->getDescription(), $this->errno,
460                          array_shift($lines));
461         } else {
462           $msg = sprintf("%s:%d: %s: \"%s\"",
463                          $errfile, $this->errline,
464                          $this->getDescription(),
465                          array_shift($lines));
466         }
467         
468         //$html = HTML::div(array('class' => $this->getHtmlClass()), HTML::p($msg));
469         // The class is now used for the div container.
470         $html = HTML::div(HTML::p($msg));
471         if ($lines) {
472             $list = HTML::ul();
473             foreach ($lines as $line)
474                 $list->pushContent(HTML::li($line));
475             $html->pushContent($list);
476         }
477         
478         return $html;
479     }
480
481     /**
482      * Print an HTMLified version of this error.
483      * @see asXML()
484      */
485     function printXML() {
486         PrintXML($this->_getDetail());
487     }
488
489     /**
490      * Return an HTMLified version of this error.
491      */
492     function asXML() {
493         return AsXML($this->_getDetail());
494     }
495
496     /**
497      * Return a plain-text version of this error.
498      */
499     function asString() {
500         return AsString($this->_getDetail());
501     }
502
503     function printSimpleTrace($bt) {
504         global $HTTP_SERVER_VARS;
505         $nl = isset($HTTP_SERVER_VARS['REQUEST_METHOD']) ? "<br />" : "\n";
506         echo $nl."Traceback:".$nl;
507         foreach ($bt as $i => $elem) {
508             if (!array_key_exists('file', $elem)) {
509                 continue;
510             }
511             print "  " . $elem['file'] . ':' . $elem['line'] . $nl;
512         }
513         flush();
514     }
515 }
516
517 /**
518  * A class representing a PhpWiki warning.
519  *
520  * This is essentially the same as a PhpError, except that the
521  * error message is quieter: no source line, etc...
522  */
523 class PhpWikiError extends PhpError {
524     /**
525      * Construct a new PhpError.
526      * @param $errno   int
527      * @param $errstr  string
528      */
529     function PhpWikiError($errno, $errstr) {
530         $this->PhpError($errno, $errstr, '?', '?');
531     }
532
533     function _getDetail() {
534         return HTML::div(array('class' => $this->getHtmlClass()), 
535                          HTML::p($this->getDescription() . ": $this->errstr"));
536     }
537 }
538
539 /**
540  * A class representing a Php warning, printed only the first time.
541  *
542  * Similar to PhpError, except only the first same error message is printed, 
543  * with number of occurences.
544  */
545 class PhpErrorOnce extends PhpError {
546
547     function PhpErrorOnce($errno, $errstr, $errfile, $errline) {
548         $this->_count = 1;
549         $this->PhpError($errno, $errstr, $errfile, $errline);
550     }
551
552     function _sameError($error) {
553         if (!$error) return false;
554         return ($this->errno == $error->errno and
555                 $this->errfile == $error->errfile and
556                 $this->errline == $error->errline);
557     }
558
559     // count similar handlers, increase _count and remove the rest
560     function removeDoublettes(&$errors) {
561         for ($i=0; $i < count($errors); $i++) {
562             if (!isset($errors[$i])) continue;
563             if ($this->_sameError($errors[$i])) {
564                 $errors[$i]->_count++;
565                 $this->_count++;
566                 if ($i) unset($errors[$i]);
567             }
568         }
569         return $this->_count;
570     }
571     
572     function _getDetail($count=0) {
573         if (!$count) $count = $this->_count;
574             $dir = defined('PHPWIKI_DIR') ? PHPWIKI_DIR : substr(dirname(__FILE__),0,-4);
575         if (substr(PHP_OS,0,3) == 'WIN') {
576            $dir = str_replace('/','\\',$dir);
577            $this->errfile = str_replace('/','\\',$this->errfile);
578            $dir .= "\\";
579         } else 
580            $dir .= '/';
581         $errfile = preg_replace('|^' . preg_quote($dir) . '|', '', $this->errfile);
582         if (is_string($this->errstr))
583                 $lines = explode("\n", $this->errstr);
584             elseif (is_object($this->errstr))
585                 $lines = array($this->errstr->asXML());
586         $errtype = (DEBUG & _DEBUG_VERBOSE) ? sprintf("%s[%d]", $this->getDescription(), $this->errno)
587                                             : sprintf("%s", $this->getDescription());
588         $msg = sprintf("%s:%d: %s: %s %s",
589                        $errfile, $this->errline,
590                        $errtype,
591                        array_shift($lines),
592                        $count > 1 ? sprintf(" (...repeated %d times)",$count) : ""
593                        );
594         $html = HTML::div(array('class' => $this->getHtmlClass()), 
595                           HTML::p($msg));
596         if ($lines) {
597             $list = HTML::ul();
598             foreach ($lines as $line)
599                 $list->pushContent(HTML::li($line));
600             $html->pushContent($list);
601         }
602         
603         return $html;
604     }
605 }
606
607 require_once(dirname(__FILE__).'/HtmlElement.php');
608
609 if (!isset($GLOBALS['ErrorManager'])) {
610     $GLOBALS['ErrorManager'] = new ErrorManager;
611 }
612
613 // $Log: not supported by cvs2svn $
614 // Revision 1.41  2004/12/26 17:08:36  rurban
615 // php5 fixes: case-sensitivity, no & new
616 //
617 // Revision 1.40  2004/12/13 14:39:46  rurban
618 // aesthetics
619 //
620 // Revision 1.39  2004/11/05 18:04:20  rurban
621 // print errno only if _DEBUG_VERBOSE
622 //
623 // Revision 1.38  2004/10/19 17:34:55  rurban
624 // <4.3 fix
625 //
626 // Revision 1.37  2004/10/14 19:23:58  rurban
627 // remove debugging prints
628 //
629 // Revision 1.36  2004/10/12 15:35:43  rurban
630 // avoid Php Notice header
631 //
632 // Revision 1.35  2004/10/12 13:13:19  rurban
633 // php5 compatibility (5.0.1 ok)
634 //
635 // Revision 1.34  2004/09/24 18:52:19  rurban
636 // in deferred html error messages use the worst header and class
637 // (notice => warning => errors)
638 //
639 // Revision 1.33  2004/09/14 10:28:21  rurban
640 // use assert, maybe we should only turn it off for releases
641 //
642 // Revision 1.32  2004/07/08 13:50:32  rurban
643 // various unit test fixes: print error backtrace on _DEBUG_TRACE; allusers fix; new PHPWIKI_NOMAIN constant for omitting the mainloop
644 //
645 // Revision 1.31  2004/07/02 09:55:58  rurban
646 // more stability fixes: new DISABLE_GETIMAGESIZE if your php crashes when loading LinkIcons: failing getimagesize in old phps; blockparser stabilized
647 //
648 // Revision 1.30  2004/06/25 14:29:12  rurban
649 // WikiGroup refactoring:
650 //   global group attached to user, code for not_current user.
651 //   improved helpers for special groups (avoid double invocations)
652 // new experimental config option ENABLE_XHTML_XML (fails with IE, and document.write())
653 // fixed a XHTML validation error on userprefs.tmpl
654 //
655 // Revision 1.29  2004/06/20 15:30:04  rurban
656 // get_class case-sensitivity issues
657 //
658 // Revision 1.28  2004/06/16 11:51:04  rurban
659 // fixed typo: undefined object #235
660 //
661 // Revision 1.27  2004/06/13 09:38:20  rurban
662 // isa() workaround, if stdlib.php is not loaded
663 //
664 // Revision 1.26  2004/06/02 18:01:45  rurban
665 // init global FileFinder to add proper include paths at startup
666 //   adds PHPWIKI_DIR if started from another dir, lib/pear also
667 // fix slashify for Windows
668 // fix USER_AUTH_POLICY=old, use only USER_AUTH_ORDER methods (besides HttpAuth)
669 //
670 // Revision 1.25  2004/06/02 10:18:36  rurban
671 // assert only if DEBUG is non-false
672 //
673 // Revision 1.24  2004/05/27 17:49:05  rurban
674 // renamed DB_Session to DbSession (in CVS also)
675 // added WikiDB->getParam and WikiDB->getAuthParam method to get rid of globals
676 // remove leading slash in error message
677 // added force_unlock parameter to File_Passwd (no return on stale locks)
678 // fixed adodb session AffectedRows
679 // added FileFinder helpers to unify local filenames and DATA_PATH names
680 // editpage.php: new edit toolbar javascript on ENABLE_EDIT_TOOLBAR
681 //
682 //
683
684 // (c-file-style: "gnu")
685 // Local Variables:
686 // mode: php
687 // tab-width: 8
688 // c-basic-offset: 4
689 // c-hanging-comment-ender-p: nil
690 // indent-tabs-mode: nil
691 // End:
692 ?>