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