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