]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/ErrorManager.php
PhpWiki:DanFr --> Dan Frankowski
[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 is_a($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 PhpWiki 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;">';
269             echo _('Fatal PhpWiki Error')._(':');
270             echo "</div>\n";
271
272             if (defined('DEBUG') and (DEBUG & _DEBUG_TRACE)) {
273                 echo "error_reporting=", error_reporting(), "\n<br />";
274                 $error->printSimpleTrace(debug_backtrace());
275             }
276             $this->_die($error);
277         } elseif (($error->errno & error_reporting()) != 0) {
278             if (($error->errno & $this->_postpone_mask) != 0) {
279                 if (is_a($error, 'PhpErrorOnce')) {
280                     $error->removeDoublettes($this->_postponed_errors);
281                     if ($error->_count < 2)
282                         $this->_postponed_errors[] = $error;
283                 } else {
284                     $this->_postponed_errors[] = $error;
285                 }
286             } else {
287                 //echo "postponed errors: ";
288                 $this->_noCacheHeaders();
289                 if (defined('DEBUG') and (DEBUG & _DEBUG_TRACE)) {
290                     echo "error_reporting=", error_reporting(), "\n";
291                     $error->printSimpleTrace(debug_backtrace());
292                 }
293                 $error->printXML();
294             }
295         }
296         $in_handler = false;
297     }
298
299     function warning($msg, $errno = E_USER_NOTICE)
300     {
301         $this->handleError(new PhpWikiError($errno, $msg, '?', '?'));
302     }
303
304     private function _die($error)
305     {
306         global $WikiTheme;
307         //echo "\n\n<html><body>";
308         $error->printXML();
309         PrintXML($this->_flush_errors());
310         if ($this->_fatal_handler)
311             $this->_fatal_handler->call($error);
312         if (!$WikiTheme->DUMP_MODE) {
313             exit();
314         }
315     }
316
317     private function _flush_errors($keep_mask = 0)
318     {
319         $errors = &$this->_postponed_errors;
320         if (empty($errors)) return '';
321         $flushed = HTML();
322         for ($i = 0; $i < count($errors); $i++) {
323             $error =& $errors[$i];
324             if (!is_object($error)) {
325                 continue;
326             }
327             if (($error->errno & $keep_mask) != 0)
328                 continue;
329             unset($errors[$i]);
330             $flushed->pushContent($error);
331         }
332         return $flushed;
333     }
334
335     function _noCacheHeaders()
336     {
337         global $request;
338         static $already = false;
339
340         if (isset($request) and isset($request->_validators)) {
341             $request->_validators->_tag = false;
342             $request->_validators->_mtime = false;
343         }
344         if ($already) return;
345
346         // FIXME: Howto announce that to Request->cacheControl()?
347         if (!headers_sent()) {
348             header("Cache-control: no-cache");
349             header("Pragma: nocache");
350         }
351         $already = true;
352     }
353 }
354
355 /**
356  * Global error handler for class ErrorManager.
357  *
358  * This is necessary since PHP's set_error_handler() does not allow
359  * one to set an object method as a handler.
360  *
361  * @access private
362  */
363 function ErrorManager_errorHandler($errno, $errstr, $errfile, $errline)
364 {
365     // TODO: Temporary hack to have errors displayed on dev machines.
366     if (defined('DEBUG') and DEBUG and $errno < 2048) {
367         print "<br/>PhpWiki Warning: ($errno, $errstr, $errfile, $errline)";
368     }
369
370     if (!isset($GLOBALS['ErrorManager'])) {
371         $GLOBALS['ErrorManager'] = new ErrorManager;
372     }
373
374     if (defined('DEBUG') and DEBUG) {
375         $error = new PhpError($errno, $errstr, $errfile, $errline);
376     } else {
377         $error = new PhpErrorOnce($errno, $errstr, $errfile, $errline);
378     }
379     $GLOBALS['ErrorManager']->handleError($error);
380 }
381
382 /**
383  * A class representing a PHP error report.
384  *
385  * @see The PHP documentation for set_error_handler at
386  *      http://php.net/manual/en/function.set-error-handler.php .
387  */
388 class PhpError
389 {
390     /**
391      * The PHP errno
392      */
393     public $errno;
394
395     /**
396      * The PHP error message.
397      */
398     public $errstr;
399
400     /**
401      * The source file where the error occurred.
402      */
403     public $errfile;
404
405     /**
406      * The line number (in $this->errfile) where the error occured.
407      */
408     public $errline;
409
410     /**
411      * @param int $errno
412      * @param string $errstr
413      * @param string $errfile
414      * @param int $errline
415      */
416     function __construct($errno, $errstr, $errfile, $errline)
417     {
418         $this->errno = $errno;
419         $this->errstr = $errstr;
420         $this->errfile = $errfile;
421         $this->errline = $errline;
422     }
423
424     /**
425      * Determine whether this is a fatal error.
426      * @return boolean True if this is a fatal error.
427      */
428     function isFatal()
429     {
430         return ($this->errno & (2048 | EM_WARNING_ERRORS | EM_NOTICE_ERRORS)) == 0;
431     }
432
433     /**
434      * Determine whether this is a warning level error.
435      * @return boolean
436      */
437     function isWarning()
438     {
439         return ($this->errno & EM_WARNING_ERRORS) != 0;
440     }
441
442     /**
443      * Determine whether this is a notice level error.
444      * @return boolean
445      */
446     function isNotice()
447     {
448         return ($this->errno & EM_NOTICE_ERRORS) != 0;
449     }
450
451     function getHtmlClass()
452     {
453         if ($this->isNotice()) {
454             return 'hint';
455         } elseif ($this->isWarning()) {
456             return 'warning';
457         } else {
458             return 'errors';
459         }
460     }
461
462     function getDescription()
463     {
464         if ($this->isNotice()) {
465             return 'Notice';
466         } elseif ($this->isWarning()) {
467             return 'Warning';
468         } else {
469             return 'Error';
470         }
471     }
472
473     /**
474      * Get a printable, HTML, message detailing this error.
475      * @return object The detailed error message.
476      */
477     function _getDetail()
478     {
479         $dir = defined('PHPWIKI_DIR') ? PHPWIKI_DIR : substr(dirname(__FILE__), 0, -4);
480         if (substr(PHP_OS, 0, 3) == 'WIN') {
481             $dir = str_replace('/', '\\', $dir);
482             $this->errfile = str_replace('/', '\\', $this->errfile);
483             $dir .= "\\";
484         } else
485             $dir .= '/';
486         $errfile = preg_replace('|^' . preg_quote($dir) . '|', '', $this->errfile);
487         $lines = explode("\n", $this->errstr);
488         if (DEBUG & _DEBUG_VERBOSE) {
489             $msg = sprintf("%s:%d %s[%d]: %s",
490                 $errfile, $this->errline,
491                 $this->getDescription(), $this->errno,
492                 array_shift($lines));
493         } /* elseif (! $this->isFatal()) {
494           $msg = sprintf("%s:%d %s: \"%s\"",
495                          $errfile, $this->errline,
496                          $this->getDescription(),
497                          array_shift($lines));
498         }*/ else {
499             $msg = sprintf("%s:%d %s: \"%s\"",
500                 $errfile, $this->errline,
501                 $this->getDescription(),
502                 array_shift($lines));
503         }
504
505         $html = HTML::div(array('class' => $this->getHtmlClass()), HTML::p($msg));
506         // The class is now used for the div container.
507         // $html = HTML::div(HTML::p($msg));
508         if ($lines) {
509             $list = HTML::ul();
510             foreach ($lines as $line)
511                 $list->pushContent(HTML::li($line));
512             $html->pushContent($list);
513         }
514
515         return $html;
516     }
517
518     /**
519      * Print an HTMLified version of this error.
520      * @see asXML()
521      */
522     function printXML()
523     {
524         PrintXML($this->_getDetail());
525     }
526
527     /**
528      * Return an HTMLified version of this error.
529      */
530     function asXML()
531     {
532         return AsXML($this->_getDetail());
533     }
534
535     /**
536      * Return a plain-text version of this error.
537      */
538     function asString()
539     {
540         return AsString($this->_getDetail());
541     }
542
543     function printSimpleTrace($bt)
544     {
545         $nl = isset($_SERVER['REQUEST_METHOD']) ? "<br />" : "\n";
546         echo $nl . "Traceback:" . $nl;
547         foreach ($bt as $i => $elem) {
548             if (!array_key_exists('file', $elem)) {
549                 continue;
550             }
551             print "  " . $elem['file'] . ':' . $elem['line'] . $nl;
552         }
553         flush();
554     }
555 }
556
557 /**
558  * A class representing a PhpWiki warning.
559  *
560  * This is essentially the same as a PhpError, except that the
561  * error message is quieter: no source line, etc...
562  */
563 class PhpWikiError extends PhpError
564 {
565     /**
566      * @param int $errno
567      * @param string $errstr
568      * @param string $errfile
569      * @param int $errline
570      */
571     function __construct($errno, $errstr, $errfile, $errline)
572     {
573         parent::__construct($errno, $errstr, $errfile, $errline);
574     }
575
576     function _getDetail()
577     {
578         return HTML::div(array('class' => $this->getHtmlClass()),
579             HTML::p($this->getDescription() . ": $this->errstr"));
580     }
581 }
582
583 /**
584  * A class representing a Php warning, printed only the first time.
585  *
586  * Similar to PhpError, except only the first same error message is printed,
587  * with number of occurences.
588  */
589 class PhpErrorOnce extends PhpError
590 {
591     function __construct($errno, $errstr, $errfile, $errline)
592     {
593         $this->_count = 1;
594         parent::__construct($errno, $errstr, $errfile, $errline);
595     }
596
597     function _sameError($error)
598     {
599         if (!$error) return false;
600         return ($this->errno == $error->errno and
601             $this->errfile == $error->errfile and
602                 $this->errline == $error->errline);
603     }
604
605     // count similar handlers, increase _count and remove the rest
606     function removeDoublettes(&$errors)
607     {
608         for ($i = 0; $i < count($errors); $i++) {
609             if (!isset($errors[$i])) continue;
610             if ($this->_sameError($errors[$i])) {
611                 $errors[$i]->_count++;
612                 $this->_count++;
613                 if ($i) unset($errors[$i]);
614             }
615         }
616         return $this->_count;
617     }
618
619     function _getDetail($count = 0)
620     {
621         if (!$count) $count = $this->_count;
622         $dir = defined('PHPWIKI_DIR') ? PHPWIKI_DIR : substr(dirname(__FILE__), 0, -4);
623         if (substr(PHP_OS, 0, 3) == 'WIN') {
624             $dir = str_replace('/', '\\', $dir);
625             $this->errfile = str_replace('/', '\\', $this->errfile);
626             $dir .= "\\";
627         } else
628             $dir .= '/';
629         $errfile = preg_replace('|^' . preg_quote($dir) . '|', '', $this->errfile);
630         if (is_string($this->errstr))
631             $lines = explode("\n", $this->errstr);
632         elseif (is_object($this->errstr))
633             $lines = array($this->errstr->asXML());
634         $errtype = (DEBUG & _DEBUG_VERBOSE) ? sprintf("%s[%d]", $this->getDescription(), $this->errno)
635             : sprintf("%s", $this->getDescription());
636         if ((DEBUG & _DEBUG_VERBOSE) or $this->isFatal()) {
637             $msg = sprintf("%s:%d %s: %s %s",
638                 $errfile, $this->errline,
639                 $errtype,
640                 array_shift($lines),
641                 $count > 1 ? sprintf(" (...repeated %d times)", $count) : ""
642             );
643         } else {
644             $msg = sprintf("%s: \"%s\" %s",
645                 $errtype,
646                 array_shift($lines),
647                 $count > 1 ? sprintf(" (...repeated %d times)", $count) : "");
648         }
649         $html = HTML::div(array('class' => $this->getHtmlClass()),
650             HTML::p($msg));
651         if ($lines) {
652             $list = HTML::ul();
653             foreach ($lines as $line)
654                 $list->pushContent(HTML::li($line));
655             $html->pushContent($list);
656         }
657
658         return $html;
659     }
660 }
661
662 require_once(dirname(__FILE__) . '/HtmlElement.php');
663
664 if (!isset($GLOBALS['ErrorManager'])) {
665     $GLOBALS['ErrorManager'] = new ErrorManager;
666 }
667
668 // Local Variables:
669 // mode: php
670 // tab-width: 8
671 // c-basic-offset: 4
672 // c-hanging-comment-ender-p: nil
673 // indent-tabs-mode: nil
674 // End: