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