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