]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/ErrorManager.php
Add ;
[SourceForge/phpwiki.git] / lib / ErrorManager.php
1 <?php
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 & (~E_DEPRECATED));
16 define ('EM_WARNING_ERRORS',
17     E_WARNING | E_CORE_WARNING | E_COMPILE_WARNING | E_USER_WARNING | E_DEPRECATED);
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 (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 {
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      * As this is a singleton class, you should never call this.
48      */
49     function __construct()
50     {
51         $this->_handlers = array();
52         $this->_fatal_handler = false;
53         $this->_postpone_mask = 0;
54         $this->_postponed_errors = array();
55
56         set_error_handler('ErrorManager_errorHandler');
57     }
58
59     /**
60      * Get mask indicating which errors are currently being postponed.
61      * @return int The current postponed error mask.
62      */
63     public function getPostponedErrorMask()
64     {
65         return $this->_postpone_mask;
66     }
67
68     /**
69      * Set mask indicating which errors to postpone.
70      *
71      * The default value of the postpone mask is zero (no errors postponed.)
72      *
73      * When you set this mask, any queue errors which do not match the new
74      * mask are reported.
75      *
76      * @param $newmask int The new value for the mask.
77      */
78     public function setPostponedErrorMask($newmask)
79     {
80         $this->_postpone_mask = $newmask;
81         if (function_exists('PrintXML'))
82             PrintXML($this->_flush_errors($newmask));
83         else
84             echo($this->_flush_errors($newmask));
85
86     }
87
88     /**
89      * Report any queued error messages.
90      */
91     public function flushPostponedErrors()
92     {
93         if (function_exists('PrintXML'))
94             PrintXML($this->_flush_errors());
95         else
96             echo $this->_flush_errors();
97     }
98
99     /**
100      * Get rid of all pending error messages in case of all non-html
101      * - pdf or image - output.
102      */
103     public function destroyPostponedErrors()
104     {
105         $this->_postponed_errors = array();
106     }
107
108     /**
109      * Get postponed errors, formatted as HTML.
110      *
111      * This also flushes the postponed error queue.
112      *
113      * @return object HTML describing any queued errors (or false, if none).
114      */
115     function getPostponedErrorsAsHTML()
116     {
117         $flushed = $this->_flush_errors();
118         if (!$flushed)
119             return false;
120         if ($flushed->isEmpty())
121             return false;
122         // format it with the worst class (error, warning, notice)
123         $worst_err = $flushed->_content[0];
124         foreach ($flushed->_content as $err) {
125             if ($err and isa($err, 'PhpError') and $err->errno > $worst_err->errno) {
126                 $worst_err = $err;
127             }
128         }
129         if ($worst_err->isNotice())
130             return $flushed;
131         $class = $worst_err->getHtmlClass();
132         $html = HTML::div(array('class' => $class),
133             HTML::div(array('class' => 'errors'),
134                 "PHP " . $worst_err->getDescription()));
135         $html->pushContent($flushed);
136         return $html;
137     }
138
139     /**
140      * Push a custom error handler on the handler stack.
141      *
142      * Sometimes one is performing an operation where one expects
143      * certain errors or warnings. In this case, one might not want
144      * these errors reported in the normal manner. Installing a custom
145      * error handler via this method allows one to intercept such
146      * errors.
147      *
148      * An error handler installed via this method should be either a
149      * function or an object method taking one argument: a PhpError
150      * object.
151      *
152      * The error handler should return either:
153      * <dl>
154      * <dt> False <dd> If it has not handled the error. In this case,
155      *                 error processing will proceed as if the handler
156      *                 had never been called: the error will be passed
157      *                 to the next handler in the stack, or the
158      *                 default handler, if there are no more handlers
159      *                 in the stack.
160      *
161      * <dt> True <dd> If the handler has handled the error. If the
162      *                error was a non-fatal one, no further processing
163      *                will be done. If it was a fatal error, the
164      *                ErrorManager will still terminate the PHP
165      *                process (see setFatalHandler.)
166      *
167      * <dt> A PhpError object <dd> The error is not considered
168      *                             handled, and will be passed on to
169      *                             the next handler(s) in the stack
170      *                             (or the default handler). The
171      *                             returned PhpError need not be the
172      *                             same as the one passed to the
173      *                             handler. This allows the handler to
174      *                             "adjust" the error message.
175      * </dl>
176      * @param $handler WikiCallback  Handler to call.
177      */
178     public function pushErrorHandler($handler)
179     {
180         array_unshift($this->_handlers, $handler);
181     }
182
183     /**
184      * Pop an error handler off the handler stack.
185      */
186     public function popErrorHandler()
187     {
188         return array_shift($this->_handlers);
189     }
190
191     /**
192      * Set a termination handler.
193      *
194      * This handler will be called upon fatal errors. The handler
195      * gets passed one argument: a PhpError object describing the
196      * fatal error.
197      *
198      * @param $handler WikiCallback  Callback to call on fatal errors.
199      */
200     public function setFatalHandler($handler)
201     {
202         $this->_fatal_handler = $handler;
203     }
204
205     /**
206      * Handle an error.
207      *
208      * The error is passed through any registered error handlers, and
209      * then either reported or postponed.
210      *
211      * @param $error object A PhpError object.
212      */
213     public function handleError($error)
214     {
215         static $in_handler;
216
217         if (!empty($in_handler)) {
218             $msg = $error->_getDetail();
219             $msg->unshiftContent(HTML::h2(fmt("%s: error while handling error:",
220                 "ErrorManager")));
221             $msg->printXML();
222             return;
223         }
224
225         // template which flushed the pending errors already handled,
226         // so display now all errors directly.
227         if (!empty($GLOBALS['request']->_finishing)) {
228             $this->_postpone_mask = 0;
229         }
230
231         $in_handler = true;
232
233         foreach ($this->_handlers as $handler) {
234             if (!$handler) continue;
235             $result = $handler->call($error);
236             if (!$result) {
237                 continue; // Handler did not handle error.
238             } elseif (is_object($result)) {
239                 // handler filtered the result. Still should pass to
240                 // the rest of the chain.
241                 if ($error->isFatal()) {
242                     // Don't let handlers make fatal errors non-fatal.
243                     $result->errno = $error->errno;
244                 }
245                 $error = $result;
246             } else {
247                 // Handler handled error.
248                 if (!$error->isFatal()) {
249                     $in_handler = false;
250                     return;
251                 }
252                 break;
253             }
254         }
255
256         // Error was either fatal, or was not handled by a handler.
257         // Handle it ourself.
258         if ($error->isFatal()) {
259             $this->_noCacheHeaders();
260             echo "<!DOCTYPE html>\n";
261             echo "<html>\n";
262             echo "<head>\n";
263             echo "<meta charset=\"UTF-8\" />\n";
264             echo "<title>Fatal Error</title>\n";
265             echo '<link rel="stylesheet" type="text/css" href="themes/default/phpwiki.css" />'."\n";
266             echo "</head>\n";
267             echo "<body>\n";
268             echo "<div style=\"font-weight:bold; color:red;\">Fatal Error:</div>\n";
269
270             if (defined('DEBUG') and (DEBUG & _DEBUG_TRACE)) {
271                 echo "error_reporting=", error_reporting(), "\n<br />";
272                 $error->printSimpleTrace(debug_backtrace());
273             }
274             $this->_die($error);
275         } elseif (($error->errno & error_reporting()) != 0) {
276             if (($error->errno & $this->_postpone_mask) != 0) {
277                 if ((function_exists('isa') and isa($error, 'PhpErrorOnce'))
278                     or (!function_exists('isa') and
279                         (
280                             // stdlib independent isa()
281                             (strtolower(get_class($error)) == 'phperroronce')
282                                 or (is_subclass_of($error, 'PhpErrorOnce'))))
283                 ) {
284                     $error->removeDoublettes($this->_postponed_errors);
285                     if ($error->_count < 2)
286                         $this->_postponed_errors[] = $error;
287                 } else {
288                     $this->_postponed_errors[] = $error;
289                 }
290             } else {
291                 //echo "postponed errors: ";
292                 $this->_noCacheHeaders();
293                 if (defined('DEBUG') and (DEBUG & _DEBUG_TRACE)) {
294                     echo "error_reporting=", error_reporting(), "\n";
295                     $error->printSimpleTrace(debug_backtrace());
296                 }
297                 $error->printXML();
298             }
299         }
300         $in_handler = false;
301     }
302
303     function warning($msg, $errno = E_USER_NOTICE)
304     {
305         $this->handleError(new PhpWikiError($errno, $msg, '?', '?'));
306     }
307
308     private function _die($error)
309     {
310         global $WikiTheme;
311         //echo "\n\n<html><body>";
312         $error->printXML();
313         PrintXML($this->_flush_errors());
314         if ($this->_fatal_handler)
315             $this->_fatal_handler->call($error);
316         if (!$WikiTheme->DUMP_MODE) {
317             exit();
318         }
319     }
320
321     private function _flush_errors($keep_mask = 0)
322     {
323         $errors = &$this->_postponed_errors;
324         if (empty($errors)) return '';
325         $flushed = HTML();
326         for ($i = 0; $i < count($errors); $i++) {
327             $error =& $errors[$i];
328             if (!is_object($error)) {
329                 continue;
330             }
331             if (($error->errno & $keep_mask) != 0)
332                 continue;
333             unset($errors[$i]);
334             $flushed->pushContent($error);
335         }
336         return $flushed;
337     }
338
339     function _noCacheHeaders()
340     {
341         global $request;
342         static $already = false;
343
344         if (isset($request) and isset($request->_validators)) {
345             $request->_validators->_tag = false;
346             $request->_validators->_mtime = false;
347         }
348         if ($already) return;
349
350         // FIXME: Howto announce that to Request->cacheControl()?
351         if (!headers_sent()) {
352             header("Cache-control: no-cache");
353             header("Pragma: nocache");
354         }
355         $already = true;
356     }
357 }
358
359 /**
360  * Global error handler for class ErrorManager.
361  *
362  * This is necessary since PHP's set_error_handler() does not allow
363  * one to set an object method as a handler.
364  *
365  * @access private
366  */
367 function ErrorManager_errorHandler($errno, $errstr, $errfile, $errline)
368 {
369     // TODO: Temporary hack to have errors displayed on dev machines.
370     if (defined('DEBUG') and DEBUG and $errno < 2048) {
371         print "<br/>PhpWiki Warning: ($errno, $errstr, $errfile, $errline)";
372     }
373
374     if (!isset($GLOBALS['ErrorManager'])) {
375         $GLOBALS['ErrorManager'] = new ErrorManager;
376     }
377
378     if (defined('DEBUG') and DEBUG) {
379         $error = new PhpError($errno, $errstr, $errfile, $errline);
380     } else {
381         $error = new PhpErrorOnce($errno, $errstr, $errfile, $errline);
382     }
383     $GLOBALS['ErrorManager']->handleError($error);
384 }
385
386 /**
387  * A class representing a PHP error report.
388  *
389  * @see The PHP documentation for set_error_handler at
390  *      http://php.net/manual/en/function.set-error-handler.php .
391  */
392 class PhpError
393 {
394     /**
395      * The PHP errno
396      */
397     public $errno;
398
399     /**
400      * The PHP error message.
401      */
402     public $errstr;
403
404     /**
405      * The source file where the error occurred.
406      */
407     public $errfile;
408
409     /**
410      * The line number (in $this->errfile) where the error occured.
411      */
412     public $errline;
413
414     /**
415      * @param int $errno
416      * @param string $errstr
417      * @param string $errfile
418      * @param int $errline
419      */
420     function __construct($errno, $errstr, $errfile, $errline)
421     {
422         $this->errno = $errno;
423         $this->errstr = $errstr;
424         $this->errfile = $errfile;
425         $this->errline = $errline;
426     }
427
428     /**
429      * Determine whether this is a fatal error.
430      * @return boolean True if this is a fatal error.
431      */
432     function isFatal()
433     {
434         return ($this->errno & (2048 | EM_WARNING_ERRORS | EM_NOTICE_ERRORS)) == 0;
435     }
436
437     /**
438      * Determine whether this is a warning level error.
439      * @return boolean
440      */
441     function isWarning()
442     {
443         return ($this->errno & EM_WARNING_ERRORS) != 0;
444     }
445
446     /**
447      * Determine whether this is a notice level error.
448      * @return boolean
449      */
450     function isNotice()
451     {
452         return ($this->errno & EM_NOTICE_ERRORS) != 0;
453     }
454
455     function getHtmlClass()
456     {
457         if ($this->isNotice()) {
458             return 'hint';
459         } elseif ($this->isWarning()) {
460             return 'warning';
461         } else {
462             return 'errors';
463         }
464     }
465
466     function getDescription()
467     {
468         if ($this->isNotice()) {
469             return 'Notice';
470         } elseif ($this->isWarning()) {
471             return 'Warning';
472         } else {
473             return 'Error';
474         }
475     }
476
477     /**
478      * Get a printable, HTML, message detailing this error.
479      * @return object The detailed error message.
480      */
481     function _getDetail()
482     {
483         $dir = defined('PHPWIKI_DIR') ? PHPWIKI_DIR : substr(dirname(__FILE__), 0, -4);
484         if (substr(PHP_OS, 0, 3) == 'WIN') {
485             $dir = str_replace('/', '\\', $dir);
486             $this->errfile = str_replace('/', '\\', $this->errfile);
487             $dir .= "\\";
488         } else
489             $dir .= '/';
490         $errfile = preg_replace('|^' . preg_quote($dir) . '|', '', $this->errfile);
491         $lines = explode("\n", $this->errstr);
492         if (DEBUG & _DEBUG_VERBOSE) {
493             $msg = sprintf("%s:%d %s[%d]: %s",
494                 $errfile, $this->errline,
495                 $this->getDescription(), $this->errno,
496                 array_shift($lines));
497         } /* elseif (! $this->isFatal()) {
498           $msg = sprintf("%s:%d %s: \"%s\"",
499                          $errfile, $this->errline,
500                          $this->getDescription(),
501                          array_shift($lines));
502         }*/ else {
503             $msg = sprintf("%s:%d %s: \"%s\"",
504                 $errfile, $this->errline,
505                 $this->getDescription(),
506                 array_shift($lines));
507         }
508
509         $html = HTML::div(array('class' => $this->getHtmlClass()), HTML::p($msg));
510         // The class is now used for the div container.
511         // $html = HTML::div(HTML::p($msg));
512         if ($lines) {
513             $list = HTML::ul();
514             foreach ($lines as $line)
515                 $list->pushContent(HTML::li($line));
516             $html->pushContent($list);
517         }
518
519         return $html;
520     }
521
522     /**
523      * Print an HTMLified version of this error.
524      * @see asXML()
525      */
526     function printXML()
527     {
528         PrintXML($this->_getDetail());
529     }
530
531     /**
532      * Return an HTMLified version of this error.
533      */
534     function asXML()
535     {
536         return AsXML($this->_getDetail());
537     }
538
539     /**
540      * Return a plain-text version of this error.
541      */
542     function asString()
543     {
544         return AsString($this->_getDetail());
545     }
546
547     function printSimpleTrace($bt)
548     {
549         $nl = isset($_SERVER['REQUEST_METHOD']) ? "<br />" : "\n";
550         echo $nl . "Traceback:" . $nl;
551         foreach ($bt as $i => $elem) {
552             if (!array_key_exists('file', $elem)) {
553                 continue;
554             }
555             print "  " . $elem['file'] . ':' . $elem['line'] . $nl;
556         }
557         flush();
558     }
559 }
560
561 /**
562  * A class representing a PhpWiki warning.
563  *
564  * This is essentially the same as a PhpError, except that the
565  * error message is quieter: no source line, etc...
566  */
567 class PhpWikiError extends PhpError
568 {
569     /**
570      * @param int $errno
571      * @param string $errstr
572      * @param string $errfile
573      * @param int $errline
574      */
575     function __construct($errno, $errstr, $errfile, $errline)
576     {
577         parent::__construct($errno, $errstr, $errfile, $errline);
578     }
579
580     function _getDetail()
581     {
582         return HTML::div(array('class' => $this->getHtmlClass()),
583             HTML::p($this->getDescription() . ": $this->errstr"));
584     }
585 }
586
587 /**
588  * A class representing a Php warning, printed only the first time.
589  *
590  * Similar to PhpError, except only the first same error message is printed,
591  * with number of occurences.
592  */
593 class PhpErrorOnce extends PhpError
594 {
595     function __construct($errno, $errstr, $errfile, $errline)
596     {
597         $this->_count = 1;
598         parent::__construct($errno, $errstr, $errfile, $errline);
599     }
600
601     function _sameError($error)
602     {
603         if (!$error) return false;
604         return ($this->errno == $error->errno and
605             $this->errfile == $error->errfile and
606                 $this->errline == $error->errline);
607     }
608
609     // count similar handlers, increase _count and remove the rest
610     function removeDoublettes(&$errors)
611     {
612         for ($i = 0; $i < count($errors); $i++) {
613             if (!isset($errors[$i])) continue;
614             if ($this->_sameError($errors[$i])) {
615                 $errors[$i]->_count++;
616                 $this->_count++;
617                 if ($i) unset($errors[$i]);
618             }
619         }
620         return $this->_count;
621     }
622
623     function _getDetail($count = 0)
624     {
625         if (!$count) $count = $this->_count;
626         $dir = defined('PHPWIKI_DIR') ? PHPWIKI_DIR : substr(dirname(__FILE__), 0, -4);
627         if (substr(PHP_OS, 0, 3) == 'WIN') {
628             $dir = str_replace('/', '\\', $dir);
629             $this->errfile = str_replace('/', '\\', $this->errfile);
630             $dir .= "\\";
631         } else
632             $dir .= '/';
633         $errfile = preg_replace('|^' . preg_quote($dir) . '|', '', $this->errfile);
634         if (is_string($this->errstr))
635             $lines = explode("\n", $this->errstr);
636         elseif (is_object($this->errstr))
637             $lines = array($this->errstr->asXML());
638         $errtype = (DEBUG & _DEBUG_VERBOSE) ? sprintf("%s[%d]", $this->getDescription(), $this->errno)
639             : sprintf("%s", $this->getDescription());
640         if ((DEBUG & _DEBUG_VERBOSE) or $this->isFatal()) {
641             $msg = sprintf("%s:%d %s: %s %s",
642                 $errfile, $this->errline,
643                 $errtype,
644                 array_shift($lines),
645                 $count > 1 ? sprintf(" (...repeated %d times)", $count) : ""
646             );
647         } else {
648             $msg = sprintf("%s: \"%s\" %s",
649                 $errtype,
650                 array_shift($lines),
651                 $count > 1 ? sprintf(" (...repeated %d times)", $count) : "");
652         }
653         $html = HTML::div(array('class' => $this->getHtmlClass()),
654             HTML::p($msg));
655         if ($lines) {
656             $list = HTML::ul();
657             foreach ($lines as $line)
658                 $list->pushContent(HTML::li($line));
659             $html->pushContent($list);
660         }
661
662         return $html;
663     }
664 }
665
666 require_once(dirname(__FILE__) . '/HtmlElement.php');
667
668 if (!isset($GLOBALS['ErrorManager'])) {
669     $GLOBALS['ErrorManager'] = new ErrorManager;
670 }
671
672 // Local Variables:
673 // mode: php
674 // tab-width: 8
675 // c-basic-offset: 4
676 // c-hanging-comment-ender-p: nil
677 // indent-tabs-mode: nil
678 // End: