Source for file Debug.php
Documentation is available at Debug.php
6: * Copyright (c) 2004, 2009 David Grudl (http://davidgrudl.com)
8: * This source file is subject to the "Nette license" that is bundled
9: * with this package in the file license.txt.
11: * For more information please see http://nettephp.com
13: * @copyright Copyright (c) 2004, 2009 David Grudl
14: * @license http://nettephp.com/license Nette license
15: * @link http://nettephp.com
22: require_once dirname(__FILE__) .
'/compatibility.php';
24: require_once dirname(__FILE__) .
'/exceptions.php';
26: require_once dirname(__FILE__) .
'/Framework.php';
31: * Debug static class.
33: * @author David Grudl
34: * @copyright Copyright (c) 2004, 2009 David Grudl
39: /** @var bool determines whether a server is running in production mode */
40: public static $productionMode;
42: /** @var bool determines whether a server is running in console mode */
43: public static $consoleMode;
48: /** @var bool is Firebug & FirePHP detected? */
49: private static $firebugDetected;
51: /** @var bool is AJAX request detected? */
52: private static $ajaxDetected;
54: /** @var array payload filled by {@link Debug::consoleDump()} */
55: private static $consoleData;
57: /********************* Debug::dump() ****************d*g**/
59: /** @var int how many nested levels of array/object properties display {@link Debug::dump()} */
60: public static $maxDepth =
3;
62: /** @var int how long strings display {@link Debug::dump()} */
63: public static $maxLen =
150;
65: /** @var int display location? {@link Debug::dump()} */
66: public static $showLocation =
FALSE;
68: /********************* errors and exceptions reporing ****************d*g**/
70: /**#@+ server modes {@link Debug::enable()} */
71: const DEVELOPMENT =
FALSE;
72: const PRODUCTION =
TRUE;
76: /** @var bool determines whether to consider all errors as fatal */
77: public static $strictMode =
FALSE;
79: /** @var array of callbacks specifies the functions that are automatically called after fatal error */
80: public static $onFatalError =
array();
83: public static $mailer =
array(__CLASS__
, 'defaultMailer');
85: /** @var int interval for sending email is 2 days */
86: public static $emailSnooze =
172800;
88: /** @var bool {@link Debug::enable()} */
89: private static $enabled =
FALSE;
91: /** @var string name of the file where script errors should be logged */
92: private static $logFile;
95: private static $logHandle;
97: /** @var bool send e-mail notifications of errors? */
98: private static $sendEmails;
100: /** @var string e-mail headers & body */
101: private static $emailHeaders =
array(
103: 'From' =>
'noreply@%host%',
104: 'X-Mailer' =>
'Nette Framework',
105: 'Subject' =>
'PHP: An error occurred on the server %host%',
106: 'Body' =>
'[%date%] %message%',
110: private static $colophons =
array(array(__CLASS__
, 'getDefaultColophons'));
112: /********************* profiler ****************d*g**/
114: /** @var bool {@link Debug::enableProfiler()} */
115: private static $enabledProfiler =
FALSE;
117: /** @var array free counters for your usage */
118: public static $counters =
array();
120: /********************* Firebug extension ****************d*g**/
122: /**#@+ FirePHP log priority */
124: const INFO =
'INFO';
125: const WARN =
'WARN';
126: const ERROR =
'ERROR';
127: const TRACE =
'TRACE';
128: const EXCEPTION =
'EXCEPTION';
129: const GROUP_START =
'GROUP_START';
130: const GROUP_END =
'GROUP_END';
136: * Static class - cannot be instantiated.
140: throw new LogicException("Cannot instantiate static class " .
get_class($this));
146: * Static class constructor.
149: public static function _init()
151: self::$time =
microtime(TRUE);
152: self::$consoleMode =
PHP_SAPI ===
'cli';
153: self::$productionMode =
self::DETECT;
154: self::$firebugDetected =
isset($_SERVER['HTTP_USER_AGENT']) &&
strpos($_SERVER['HTTP_USER_AGENT'], 'FirePHP/');
155: self::$ajaxDetected =
isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
$_SERVER['HTTP_X_REQUESTED_WITH'] ===
'XMLHttpRequest';
156: register_shutdown_function(array(__CLASS__
, '_shutdownHandler'));
162: * Shutdown handler to execute of the planned activities.
166: public static function _shutdownHandler()
168: // 1) fatal error handler
169: static $types =
array(
172: E_COMPILE_ERROR =>
1,
176: $error =
error_get_last();
177: if (self::$enabled &&
isset($types[$error['type']])) {
178: if (!headers_sent()) { // for PHP < 5.2.4
179: header('HTTP/1.1 500 Internal Server Error');
182: if (ini_get('html_errors')) {
183: $error['message'] =
html_entity_decode(strip_tags($error['message']));
186: self::processException(new FatalErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'], NULL), TRUE);
190: // other activities require HTML & development mode
191: if (self::$productionMode) {
194: foreach (headers_list() as $header) {
196: if (substr($header, 14, 9) ===
'text/html') {
204: if (self::$enabledProfiler) {
205: if (self::$firebugDetected) {
206: self::fireLog('Nette profiler', self::GROUP_START);
207: foreach (self::$colophons as $callback) {
208: foreach ((array)
call_user_func($callback, 'profiler') as $line) self::fireLog(strip_tags($line));
210: self::fireLog(NULL, self::GROUP_END);
213: if (!self::$ajaxDetected) {
214: $colophons =
self::$colophons;
215: require dirname(__FILE__) .
'/Debug.templates/profiler.phtml';
221: if (self::$consoleData) {
222: $payload =
self::$consoleData;
223: require dirname(__FILE__) .
'/Debug.templates/console.phtml';
229: /********************* useful tools ****************d*g**/
234: * Dumps information about a variable in readable format.
236: * @param mixed variable to dump
237: * @param bool return output instead of printing it? (bypasses $productionMode)
238: * @return mixed variable itself or dump
240: public static function dump($var, $return =
FALSE)
242: if (!$return &&
self::$productionMode) {
246: $output =
"<pre class=\"dump\">" .
self::_dump($var, 0) .
"</pre>\n";
248: if (self::$showLocation) {
249: $trace =
debug_backtrace();
250: if (isset($trace[0]['file'], $trace[0]['line'])) {
255: if (self::$consoleMode) {
256: $output =
htmlspecialchars_decode(strip_tags($output), ENT_NOQUOTES);
271: * Dumps information about a variable in Nette Debug Console.
273: * @param mixed variable to dump
274: * @param string optional title
275: * @return mixed variable itself
279: if (!self::$productionMode) {
280: self::$consoleData[] =
array('title' =>
$title, 'var' =>
$var);
288: * Internal dump() implementation.
290: * @param mixed variable to dump
291: * @param int current recursion level
294: private static function _dump(&$var, $level)
296: static $tableUtf, $tableBin, $re =
'#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]#u';
297: if ($tableUtf ===
NULL) {
298: foreach (range("\x00", "\xFF") as $ch) {
299: if (ord($ch) <
32 &&
strpos("\r\n\t", $ch) ===
FALSE) $tableUtf[$ch] =
$tableBin[$ch] =
'\\x' .
str_pad(dechex(ord($ch)), 2, '0', STR_PAD_LEFT);
300: elseif (ord($ch) <
127) $tableUtf[$ch] =
$tableBin[$ch] =
$ch;
301: else { $tableUtf[$ch] =
$ch; $tableBin[$ch] =
'\\x' .
dechex(ord($ch)); }
303: $tableUtf['\\x'] =
$tableBin['\\x'] =
'\\\\x';
307: return "<span>bool</span>(" .
($var ?
'TRUE' :
'FALSE') .
")\n";
309: } elseif ($var ===
NULL) {
310: return "<span>NULL</span>\n";
313: return "<span>int</span>($var)\n";
316: return "<span>float</span>($var)\n";
319: if (self::$maxLen &&
strlen($var) >
self::$maxLen) {
320: $s =
htmlSpecialChars(substr($var, 0, self::$maxLen), ENT_NOQUOTES) .
' ... ';
322: $s =
htmlSpecialChars($var, ENT_NOQUOTES);
325: return "<span>string</span>(" .
strlen($var) .
") \"$s\"\n";
328: $s =
"<span>array</span>(" .
count($var) .
") ";
332: if ($marker ===
NULL) $marker =
uniqid("\x00", TRUE);
335: } elseif (isset($var[$marker])) {
336: $s .=
"{\n$space$space1*RECURSION*\n$space}";
338: } elseif ($level <
self::$maxDepth ||
!self::$maxDepth) {
341: foreach ($var as $k =>
&$v) {
342: if ($k ===
$marker) continue;
344: $s .=
"$space$space1$k => " .
self::_dump($v, $level +
1);
346: unset($var[$marker]);
347: $s .=
"$space}</code>";
350: $s .=
"{\n$space$space1...\n$space}";
355: $arr = (array)
$var;
359: static $list =
array();
364: $s .=
"{\n$space$space1*RECURSION*\n$space}";
366: } elseif ($level <
self::$maxDepth ||
!self::$maxDepth) {
369: foreach ($arr as $k =>
&$v) {
371: if ($k[0] ===
"\x00") {
372: $m =
$k[1] ===
'*' ?
' <span>protected</span>' :
' <span>private</span>';
376: $s .=
"$space$space1\"$k\"$m => " .
self::_dump($v, $level +
1);
379: $s .=
"$space}</code>";
382: $s .=
"{\n$space$space1...\n$space}";
390: return "<span>unknown type</span>\n";
397: * Starts/stops stopwatch.
398: * @param string name
399: * @return elapsed seconds
401: public static function timer($name =
NULL)
403: static $time =
array();
404: $now =
microtime(TRUE);
405: $delta =
isset($time[$name]) ?
$now -
$time[$name] :
0;
406: $time[$name] =
$now;
412: /********************* errors and exceptions reporing ****************d*g**/
417: * Enables displaying or logging errors and exceptions.
418: * @param mixed production, development mode or autodetection
419: * @param string error log file (FALSE disables logging in production mode)
420: * @param array|string administrator email or email headers; enables email sending in production mode
423: public static function enable($mode =
NULL, $logFile =
NULL, $email =
NULL)
427: // production/development mode detection
429: self::$productionMode =
$mode;
431: if (self::$productionMode ===
self::DETECT) {
433: self::$productionMode =
Environment::isProduction();
435: } elseif (isset($_SERVER['SERVER_ADDR']) ||
isset($_SERVER['LOCAL_ADDR'])) { // IP address based detection
436: $addr =
isset($_SERVER['SERVER_ADDR']) ?
$_SERVER['SERVER_ADDR'] :
$_SERVER['LOCAL_ADDR'];
438: self::$productionMode =
$addr !==
'::1' &&
(count($oct) !==
4 ||
($oct[0] !==
'10' &&
$oct[0] !==
'127' &&
($oct[0] !==
'172' ||
$oct[1] <
16 ||
$oct[1] >
31)
439: &&
($oct[0] !==
'169' ||
$oct[1] !==
'254') &&
($oct[0] !==
'192' ||
$oct[1] !==
'168')));
442: self::$productionMode =
!self::$consoleMode;
446: // logging configuration
447: if (self::$productionMode &&
$logFile !==
FALSE) {
448: self::$logFile =
'log/php_error.log';
450: if (class_exists('Environment')) {
452: self::$logFile =
Environment::expand($logFile);
455: self::$logFile =
Environment::expand('%logDir%/php_error.log');
461: self::$logFile =
$logFile;
464: ini_set('error_log', self::$logFile);
467: // php configuration
468: if (function_exists('ini_set')) {
469: ini_set('display_errors', !self::$productionMode); // or 'stderr'
470: ini_set('html_errors', !self::$logFile &&
!self::$consoleMode);
471: ini_set('log_errors', (bool)
self::$logFile);
473: } elseif (ini_get('log_errors') != (bool)
self::$logFile ||
// intentionally ==
474: (ini_get('display_errors') !=
!self::$productionMode &&
ini_get('display_errors') !==
(self::$productionMode ?
'stderr' :
'stdout'))) {
475: throw new NotSupportedException('Function ini_set() must be enabled.');
478: self::$sendEmails =
self::$logFile &&
$email;
479: if (self::$sendEmails) {
480: if (is_string($email)) {
481: self::$emailHeaders['To'] =
$email;
483: } elseif (is_array($email)) {
484: self::$emailHeaders =
$email +
self::$emailHeaders;
488: if (!defined('E_DEPRECATED')) {
498: self::$enabled =
TRUE;
504: * Unregister error handler routine.
509: return self::$enabled;
515: * Debug exception handler.
521: public static function _exceptionHandler(Exception $exception)
524: header('HTTP/1.1 500 Internal Server Error');
527: self::processException($exception, TRUE);
534: * Own error handler.
536: * @param int level of the error raised
537: * @param string error message
538: * @param string file that the error was raised in
539: * @param int line number the error was raised at
540: * @param array an array of variables that existed in the scope the error was triggered in
541: * @return bool FALSE to call normal error handler, NULL otherwise
542: * @throws FatalErrorException
545: public static function _errorHandler($severity, $message, $file, $line, $context)
547: if ($severity ===
E_RECOVERABLE_ERROR ||
$severity ===
E_USER_ERROR) {
551: return NULL; // nothing to do
553: } elseif (self::$strictMode) {
554: if (!headers_sent()) {
555: header('HTTP/1.1 500 Internal Server Error');
561: static $types =
array(
562: E_WARNING =>
'Warning',
563: E_USER_WARNING =>
'Warning',
564: E_NOTICE =>
'Notice',
565: E_USER_NOTICE =>
'Notice',
566: E_STRICT =>
'Strict standards',
567: E_DEPRECATED =>
'Deprecated',
568: E_USER_DEPRECATED =>
'Deprecated',
571: $type =
isset($types[$severity]) ?
$types[$severity] :
'Unknown error';
573: if (self::$logFile) {
574: if (self::$sendEmails) {
575: self::sendEmail("$type:
$message in
$file on line
$line");
577: return FALSE; // call normal error handler
579: } elseif (!self::$productionMode &&
self::$firebugDetected &&
!headers_sent()) {
581: self::fireLog("$type: $message in $file on line $line", self::ERROR);
585: return FALSE; // call normal error handler
591: * Logs or displays exception.
593: * @param bool is writing to standard output buffer allowed?
598: if (!self::$enabled) {
601: } elseif (self::$logFile) {
602: error_log("PHP Fatal error: Uncaught $exception");
604: $file =
dirname(self::$logFile) .
"/exception $file.html";
605: self::$logHandle =
@fopen($file, 'x');
606: if (self::$logHandle) {
607: ob_start(array(__CLASS__
, '_writeFile'), 1);
608: self::_paintBlueScreen($exception);
612: if (self::$sendEmails) {
613: self::sendEmail((string)
$exception);
616: } elseif (self::$productionMode) {
619: } elseif (self::$consoleMode) { // dump to console
620: if ($outputAllowed) {
621: echo "$exception\n";
622: foreach (self::$colophons as $callback) {
623: foreach ((array)
call_user_func($callback, 'bluescreen') as $line) echo strip_tags($line) .
"\n";
627: } elseif (self::$firebugDetected &&
self::$ajaxDetected &&
!headers_sent()) { // AJAX mode
628: self::fireLog($exception, self::EXCEPTION);
630: } elseif ($outputAllowed) { // dump to browser
636: self::_paintBlueScreen($exception);
638: } elseif (self::$firebugDetected &&
!headers_sent()) {
639: self::fireLog($exception, self::EXCEPTION);
642: foreach (self::$onFatalError as $handler) {
643: fixCallback($handler);
651: * Paint blue screen.
656: public static function _paintBlueScreen(Exception $exception)
658: $internals =
array();
659: foreach (array('Object', 'ObjectMixin') as $class) {
661: $rc =
new ReflectionClass($class);
662: $internals[$rc->getFileName()] =
TRUE;
670: $colophons =
self::$colophons;
671: require dirname(__FILE__) .
'/Debug.templates/bluescreen.phtml';
677: * Redirects output to file.
682: public static function _writeFile($buffer)
690: * Sends e-mail notification.
694: private static function sendEmail($message)
696: $monitorFile =
self::$logFile .
'.monitor';
697: if (@filemtime($monitorFile) +
self::$emailSnooze <
time()
710: private static function defaultMailer($message)
712: $host =
isset($_SERVER['HTTP_HOST']) ?
$_SERVER['HTTP_HOST'] :
713: (isset($_SERVER['SERVER_NAME']) ?
$_SERVER['SERVER_NAME'] :
'');
716: array('%host%', '%date%', '%message%'),
717: array($host, @date('Y-m-d H:i:s', Debug::$time), $message), // intentionally @
721: $subject =
$headers['Subject'];
722: $to =
$headers['To'];
723: $body =
$headers['Body'];
724: unset($headers['Subject'], $headers['To'], $headers['Body']);
726: foreach ($headers as $key =>
$value) {
727: $header .=
"$key: $value\r\n";
730: // we need to change \r\n to \n because Unix mailer changes it back to \r\n
731: $body =
str_replace("\r\n", "\n", $body);
734: mail($to, $subject, $body, $header);
739: /********************* profiler ****************d*g**/
749: self::$enabledProfiler =
TRUE;
755: * Disables profiler.
760: self::$enabledProfiler =
FALSE;
765: /********************* colophons ****************d*g**/
770: * Add custom descriptions.
779: throw new InvalidArgumentException("Colophon handler '$textual' is not " .
($able ?
'callable.' :
'valid PHP callback.'));
783: self::$colophons[] =
$callback;
790: * Returns default colophons.
791: * @param string profiler | bluescreen
794: private static function getDefaultColophons($sender)
796: if ($sender ===
'profiler') {
799: foreach ((array)
self::$counters as $name =>
$value) {
800: if (is_array($value)) $value =
implode(', ', $value);
801: $arr[] =
htmlSpecialChars($name) .
' = <strong>' .
htmlSpecialChars($value) .
'</strong>';
807: $exclude =
array('stdClass', 'Exception', 'ErrorException', 'Traversable', 'IteratorAggregate', 'Iterator', 'ArrayAccess', 'Serializable', 'Closure');
809: $ref =
new ReflectionExtension($ext);
815: $func = (array)
@$func['user'];
818: foreach (array('classes', 'intf', 'func', 'consts') as $item) {
819: $s .=
'<span ' .
($
$item ?
'title="' .
implode(", ", $
$item) .
'"' :
'') .
'>' .
count($
$item) .
' ' .
$item .
'</span>, ';
824: if ($sender ===
'bluescreen') {
825: $arr[] =
'Report generated at ' .
@date('Y/m/d H:i:s', Debug::$time); // intentionally @
826: if (isset($_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'])) {
827: $url =
(isset($_SERVER['HTTPS']) &&
strcasecmp($_SERVER['HTTPS'], 'off') ?
'https://' :
'http://') .
htmlSpecialChars($_SERVER['HTTP_HOST'] .
$_SERVER['REQUEST_URI']);
828: $arr[] =
'<a href="' .
$url .
'">' .
$url .
'</a>';
830: $arr[] =
'PHP ' .
htmlSpecialChars(PHP_VERSION);
831: if (isset($_SERVER['SERVER_SOFTWARE'])) $arr[] =
htmlSpecialChars($_SERVER['SERVER_SOFTWARE']);
839: /********************* Firebug extension ****************d*g**/
844: * Sends variable dump to Firebug tab request/server.
845: * @param mixed variable to dump
846: * @param string unique key
847: * @return mixed variable itself
851: self::fireSend(2, array((string)
$key =>
$var));
858: * Sends message to Firebug console.
859: * @param mixed message to log
860: * @param string priority of message (LOG, INFO, WARN, ERROR, GROUP_START, GROUP_END)
861: * @param string optional label
862: * @return bool was successful?
864: public static function fireLog($message, $priority =
self::LOG, $label =
NULL)
866: if ($message instanceof
Exception) {
867: if ($priority !==
self::EXCEPTION &&
$priority !==
self::TRACE) {
868: $priority =
self::TRACE;
872: 'Message' =>
$message->getMessage(),
873: 'File' =>
$message->getFile(),
874: 'Line' =>
$message->getLine(),
875: 'Trace' =>
$message->getTrace(),
879: foreach ($message['Trace'] as & $row) {
880: if (empty($row['file'])) $row['file'] =
'?';
881: if (empty($row['line'])) $row['line'] =
'?';
883: } elseif ($priority ===
self::GROUP_START) {
887: return self::fireSend(1, self::replaceObjects(array(array('Type' =>
$priority, 'Label' =>
$label), $message)));
893: * Performs Firebug output.
894: * @see http://www.firephp.org
895: * @param int structure index
896: * @param array payload
897: * @return bool was successful?
899: private static function fireSend($index, $payload)
901: if (self::$productionMode) return NULL;
905: header('X-Wf-Protocol-nette: http://meta.wildfirehq.org/Protocol/JsonStream/0.2');
906: header('X-Wf-nette-Plugin-1: http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/0.2.0');
909: header('X-Wf-nette-Structure-1: http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1');
911: } elseif ($index ===
2) {
912: header('X-Wf-nette-Structure-2: http://meta.firephp.org/Wildfire/Structure/FirePHP/Dump/0.1');
933: static private function replaceObjects($val)
939: return @iconv('UTF-16', 'UTF-8//IGNORE', iconv('UTF-8', 'UTF-16//IGNORE', $val)); // intentionally @
942: foreach ($val as $k =>
$v) {
944: $k =
@iconv('UTF-16', 'UTF-8//IGNORE', iconv('UTF-8', 'UTF-16//IGNORE', $k)); // intentionally @
945: $val[$k] =
self::replaceObjects($v);
959: // if (!function_exists('dump')) { function dump($var, $return = FALSE) { return Debug::dump($var, $return); } }