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