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 https://nette.org
13: * @copyright Copyright (c) 2004, 2009 David Grudl
14: * @license https://nette.org/license Nette license
15: * @link https://nette.org
23: require_once dirname(__FILE__) .
'/compatibility.php';
25: require_once dirname(__FILE__) .
'/exceptions.php';
27: require_once dirname(__FILE__) .
'/Framework.php';
32: * Debug static class.
34: * @author David Grudl
35: * @copyright Copyright (c) 2004, 2009 David Grudl
40: /**#@+ server modes {@link Debug::enable()} */
41: const DEVELOPMENT =
FALSE;
42: const PRODUCTION =
TRUE;
46: /** @var array free counters for your usage */
47: public static $counters =
array();
49: /** @deprecated {@link Debug::$consoleMode} */
52: /** @var bool determines whether a server is running in production mode */
53: public static $productionMode;
55: /** @var bool determines whether a server is running in console mode */
56: public static $consoleMode;
58: /** @var int how many nested levels of array/object properties display {@link Debug::dump()} */
59: public static $maxDepth =
3;
61: /** @var int how long strings display {@link Debug::dump()} */
62: public static $maxLen =
150;
64: /** @var int sensitive keys not displayed by {@link Debug::dump()} when {@link Debug::$productionMode} in on */
65: public static $keysToHide =
array('password', 'passwd', 'pass', 'pwd', 'creditcard', 'credit card', 'cc', 'pin');
67: /** @var bool {@link Debug::enable()} */
68: private static $enabled =
FALSE;
70: /** @var bool {@link Debug::enableProfiler()} */
71: private static $enabledProfiler =
FALSE;
73: /** @var bool is Firebug & FirePHP detected? */
74: private static $firebugDetected;
76: /** @var bool is AJAX request detected? */
77: private static $ajaxDetected;
79: /** @var string name of the file where script errors should be logged */
80: private static $logFile;
83: private static $logHandle;
85: /** @var bool send e-mail notifications of errors? */
86: private static $sendEmails;
88: /** @var string e-mail headers & body */
89: private static $emailHeaders =
array(
91: 'From' =>
'noreply@%host%',
92: 'X-Mailer' =>
'Nette Framework',
93: 'Subject' =>
'PHP: An error occurred on the server %host%',
94: 'Body' =>
'[%date%] %message%',
98: public static $mailer =
array(__CLASS__
, 'defaultMailer');
101: public static $emailProbability;
104: private static $colophons =
array(array(__CLASS__
, 'getDefaultColophons'));
107: private static $keyFilter =
array();
110: public static $time;
112: /**#@+ FirePHP log priority */
114: const INFO =
'INFO';
115: const WARN =
'WARN';
116: const ERROR =
'ERROR';
117: const TRACE =
'TRACE';
118: const EXCEPTION =
'EXCEPTION';
119: const GROUP_START =
'GROUP_START';
120: const GROUP_END =
'GROUP_END';
126: * Static class - cannot be instantiated.
130: throw new LogicException("Cannot instantiate static class " .
get_class($this));
136: * Static class constructor.
140: self::$time =
microtime(TRUE);
141: self::$consoleMode =
PHP_SAPI ===
'cli';
142: self::$productionMode =
self::DETECT;
143: self::$firebugDetected =
isset($_SERVER['HTTP_USER_AGENT']) &&
strpos($_SERVER['HTTP_USER_AGENT'], 'FirePHP/');
144: self::$ajaxDetected =
isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
$_SERVER['HTTP_X_REQUESTED_WITH'] ===
'XMLHttpRequest';
149: /********************* useful tools ****************d*g**/
154: * Dumps information about a variable in readable format.
156: * @param mixed variable to dump.
157: * @param bool return output instead of printing it? (bypasses $productionMode)
158: * @return mixed variable or dump
160: public static function dump($var, $return =
FALSE)
162: if (!$return &&
self::$productionMode) {
166: //self::$keyFilter = self::$productionMode ? array_change_key_case(array_flip(self::$keysToHide), CASE_LOWER) : NULL;
168: $output =
"<pre class=\"dump\">" .
self::_dump($var, 0) .
"</pre>\n";
170: if (self::$consoleMode) {
171: $output =
htmlspecialchars_decode(strip_tags($output), ENT_NOQUOTES);
186: * Internal dump() implementation.
188: * @param mixed variable to dump
189: * @param int current recursion level
192: private static function _dump(&$var, $level)
195: return "<span>bool</span>(" .
($var ?
'TRUE' :
'FALSE') .
")\n";
197: } elseif ($var ===
NULL) {
198: return "<span>NULL</span>\n";
201: return "<span>int</span>($var)\n";
204: return "<span>float</span>($var)\n";
207: if (self::$maxLen &&
strlen($var) >
self::$maxLen) {
208: $s =
htmlSpecialChars(substr($var, 0, self::$maxLen), ENT_NOQUOTES) .
' ... ';
210: $s =
htmlSpecialChars($var, ENT_NOQUOTES);
212: return "<span>string</span>(" .
strlen($var) .
") \"$s\"\n";
215: $s =
"<span>array</span>(" .
count($var) .
") {\n";
219: if ($marker ===
NULL) $marker =
uniqid("\x00", TRUE);
220: if (isset($var[$marker])) {
221: $s .=
"$space *RECURSION*\n";
223: } elseif ($level <
self::$maxDepth ||
!self::$maxDepth) {
225: foreach ($var as $k =>
&$v) {
226: if ($k ===
$marker) continue;
227: $s .=
"$space " .
(is_int($k) ?
$k :
"\"$k\"") .
" => ";
228: if (self::$keyFilter &&
is_string($v) &&
isset(self::$keyFilter[strtolower($k)])) {
229: $s .=
"<span>string</span>(?) <i>*** hidden ***</i>\n";
231: $s .=
self::_dump($v, $level +
1);
234: unset($var[$marker]);
236: $s .=
"$space ...\n";
238: return $s .
"$space}\n";
241: $arr = (array)
$var;
245: static $list =
array();
246: if (in_array($var, $list, TRUE)) {
247: $s .=
"$space *RECURSION*\n
";
249: } elseif ($level <
self::$maxDepth ||
!self::$maxDepth) {
251: foreach ($arr as $k =>
&$v) {
253: if ($k[0] ===
"\x00") {
254: $m =
$k[1] ===
'*' ?
' <span>protected</span>' :
' <span>private</span>';
257: $s .=
"$space \"$k\"$m => ";
258: if (self::$keyFilter &&
is_string($v) &&
isset(self::$keyFilter[strtolower($k)])) {
259: $s .=
"<span>string</span>(?) <i>*** hidden ***</i>\n";
261: $s .=
self::_dump($v, $level +
1);
266: $s .=
"$space ...\n";
268: return $s .
"$space}\n";
270: } elseif (is_resource($var)) {
271: return "<span>resource of type</span>(" .
get_resource_type($var) .
")\n";
274: return "<span>unknown type</span>\n";
281: * Starts/stops stopwatch.
282: * @param string name
283: * @return elapsed seconds
285: public static function timer($name =
NULL)
287: static $time =
array();
288: $now =
microtime(TRUE);
289: $delta =
isset($time[$name]) ?
$now -
$time[$name] :
0;
290: $time[$name] =
$now;
296: /********************* errors and exceptions reporing ****************d*g**/
301: * Enables displaying or logging errors and exceptions.
302: * @param bool enable production mode? (NULL means autodetection)
303: * @param string error log file (FALSE disables logging in production mode)
304: * @param array|string administrator email or email headers; enables email sending in production mode
307: public static function enable($productionMode =
NULL, $logFile =
NULL, $email =
NULL)
315: // production/development mode detection
317: self::$productionMode =
$productionMode;
319: if (self::$productionMode ===
self::DETECT) {
321: self::$productionMode =
Environment::isProduction();
323: } elseif (isset($_SERVER['SERVER_ADDR']) ||
isset($_SERVER['LOCAL_ADDR'])) { // IP address based detection
324: $addr =
isset($_SERVER['SERVER_ADDR']) ?
$_SERVER['SERVER_ADDR'] :
$_SERVER['LOCAL_ADDR'];
326: self::$productionMode =
$addr !==
'::1' &&
(count($oct) !==
4 ||
($oct[0] !==
'10' &&
$oct[0] !==
'127' &&
($oct[0] !==
'172' ||
$oct[1] <
16 ||
$oct[1] >
31)
327: &&
($oct[0] !==
'169' ||
$oct[1] !==
'254') &&
($oct[0] !==
'192' ||
$oct[1] !==
'168')));
330: self::$productionMode =
!self::$consoleMode;
334: // logging configuration
335: if (self::$productionMode &&
$logFile !==
FALSE) {
336: self::$logFile =
'log/php_error.log';
338: if (class_exists('Environment')) {
340: self::$logFile =
Environment::expand($logFile);
343: self::$logFile =
Environment::expand('%logDir%/php_error.log');
349: self::$logFile =
$logFile;
352: ini_set('error_log', self::$logFile);
355: // php configuration
356: if (function_exists('ini_set')) {
357: ini_set('display_errors', !self::$productionMode); // or 'stderr'
358: ini_set('html_errors', !self::$consoleMode);
359: ini_set('log_errors', (bool)
self::$logFile);
361: } elseif (ini_get('log_errors') != (bool)
self::$logFile ||
// intentionally ==
362: (ini_get('display_errors') !=
!self::$productionMode &&
ini_get('display_errors') !==
(self::$productionMode ?
'stderr' :
'stdout'))) {
363: throw new NotSupportedException('Function ini_set() must be enabled.');
366: self::$sendEmails =
$logFile &&
$email;
367: if (self::$sendEmails) {
368: if (is_string($email)) {
369: self::$emailHeaders['To'] =
$email;
371: } elseif (is_array($email)) {
372: self::$emailHeaders =
$email +
self::$emailHeaders;
376: if (!defined('E_DEPRECATED')) {
387: self::$enabled =
TRUE;
389: if (is_int($productionMode)) { // back compatibility
390: //trigger_error('Debug::enable($errorLevel) is deprecated; Remove $errorLevel parameter.', E_USER_WARNING);
397: * Unregister error handler routine.
402: return self::$enabled;
408: * Debug exception handler.
414: public static function exceptionHandler(Exception $exception)
417: header('HTTP/1.1 500 Internal Server Error');
420: self::processException($exception, TRUE);
427: * Own error handler.
429: * @param int level of the error raised
430: * @param string error message
431: * @param string file that the error was raised in
432: * @param int line number the error was raised at
433: * @param array an array of variables that existed in the scope the error was triggered in
434: * @return bool FALSE to call normal error handler, NULL otherwise
435: * @throws FatalErrorException
438: public static function errorHandler($severity, $message, $file, $line, $context)
440: static $fatals =
array(
442: E_RECOVERABLE_ERROR =>
1, // since PHP 5.2
445: if (isset($fatals[$severity])) {
446: throw new FatalErrorException($message, 0, $severity, $file, $line, $context);
449: return NULL; // nothing to do
452: static $types =
array(
453: E_WARNING =>
'Warning',
454: E_USER_WARNING =>
'Warning',
455: E_NOTICE =>
'Notice',
456: E_USER_NOTICE =>
'Notice',
457: E_STRICT =>
'Strict standards',
458: E_DEPRECATED =>
'Deprecated',
459: E_USER_DEPRECATED =>
'Deprecated',
462: $type =
isset($types[$severity]) ?
$types[$severity] :
'Unknown error';
464: if (self::$logFile) {
465: if (self::$sendEmails) {
466: self::sendEmail("$type: $message in $file on line $line");
468: return FALSE; // call normal error handler
470: } elseif (!self::$productionMode &&
self::$firebugDetected &&
!headers_sent()) {
472: self::fireLog("$type: $message in $file on line $line", self::ERROR);
476: return FALSE; // call normal error handler
482: * Shutdown handler to process fatal errors.
486: public static function shutdownHandler()
488: static $types =
array(
491: E_COMPILE_ERROR =>
1,
495: $error =
error_get_last();
497: if (isset($types[$error['type']]) &&
($error['type'] & error_reporting())) {
498: if (!headers_sent()) { // for PHP < 5.2.4
499: header('HTTP/1.1 500 Internal Server Error');
506: self::processException(new FatalErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'], NULL), TRUE);
513: * Logs or displays exception.
515: * @param bool is writing to standard output buffer allowed?
520: if (self::$logFile) {
521: error_log("PHP Fatal error: Uncaught $exception");
523: $file =
dirname(self::$logFile) .
"/exception $file.html";
524: self::$logHandle =
@fopen($file, 'x');
525: if (self::$logHandle) {
526: ob_start(array(__CLASS__
, 'writeFile'), 1);
527: self::paintBlueScreen($exception);
531: if (self::$sendEmails) {
532: self::sendEmail((string)
$exception);
535: } elseif (self::$productionMode) {
538: } elseif (self::$consoleMode) { // dump to console
539: if ($outputAllowed) {
540: echo "$exception\n";
541: foreach (self::$colophons as $callback) {
542: foreach ((array)
call_user_func($callback, 'bluescreen') as $line) echo strip_tags($line) .
"\n";
546: } elseif (self::$firebugDetected &&
self::$ajaxDetected &&
!headers_sent()) { // AJAX mode
547: self::fireLog($exception, self::EXCEPTION);
549: } elseif ($outputAllowed) { // dump to browser
550: self::paintBlueScreen($exception);
552: } elseif (self::$firebugDetected &&
!headers_sent()) {
553: self::fireLog($exception, self::EXCEPTION);
560: * Paint blue screen.
565: public static function paintBlueScreen(Exception $exception)
567: $internals =
array();
568: foreach (array('Object', 'ObjectMixin') as $class) {
570: $rc =
new ReflectionClass($class);
571: $internals[$rc->getFileName()] =
TRUE;
574: $colophons =
self::$colophons;
575: require dirname(__FILE__) .
'/Debug.templates/bluescreen.phtml';
581: * Redirects output to file.
586: public static function writeFile($buffer)
594: * Sends e-mail notification.
598: private static function sendEmail($message)
600: $monitorFile =
self::$logFile .
'.monitor';
601: $saved =
@file_get_contents($monitorFile); // intentionally @
616: private static function defaultMailer($message)
618: $host =
isset($_SERVER['HTTP_HOST']) ?
$_SERVER['HTTP_HOST'] :
619: (isset($_SERVER['SERVER_NAME']) ?
$_SERVER['SERVER_NAME'] :
'');
622: array('%host%', '%date%', '%message%'),
623: array($host, @date('Y-m-d H:i:s', Debug::$time), $message), // intentionally @
627: $subject =
$headers['Subject'];
628: $to =
$headers['To'];
629: $body =
$headers['Body'];
630: unset($headers['Subject'], $headers['To'], $headers['Body']);
632: foreach ($headers as $key =>
$value) {
633: $header .=
"$key: $value\r\n";
636: // we need to change \r\n to \n because Unix mailer changes it back to \r\n
637: $body =
str_replace("\r\n", "\n", $body);
640: mail($to, $subject, $body, $header);
645: /********************* profiler ****************d*g**/
655: self::$enabledProfiler =
TRUE;
662: * Disables profiler.
667: self::$enabledProfiler =
FALSE;
673: * Paint profiler window.
677: public static function paintProfiler()
679: if (!self::$enabledProfiler ||
self::$productionMode) {
682: self::$enabledProfiler =
FALSE;
684: if (self::$firebugDetected) {
685: self::fireLog('Nette profiler', self::GROUP_START);
686: foreach (self::$colophons as $callback) {
687: foreach ((array)
call_user_func($callback, 'profiler') as $line) self::fireLog(strip_tags($line));
689: self::fireLog(NULL, self::GROUP_END);
692: if (!self::$ajaxDetected) {
693: $colophons =
self::$colophons;
694: require dirname(__FILE__) .
'/Debug.templates/profiler.phtml';
700: /********************* colophons ****************d*g**/
705: * Add custom descriptions.
714: throw new InvalidArgumentException("Colophon handler '$textual' is not " .
($able ?
'callable.' :
'valid PHP callback.'));
718: self::$colophons[] =
$callback;
725: * Returns default colophons.
726: * @param string profiler | bluescreen
729: public static function getDefaultColophons($sender)
731: if ($sender ===
'profiler') {
734: foreach ((array)
self::$counters as $name =>
$value) {
735: if (is_array($value)) $value =
implode(', ', $value);
736: $arr[] =
htmlSpecialChars($name) .
' = <strong>' .
htmlSpecialChars($value) .
'</strong>';
742: $exclude =
array('stdClass', 'Exception', 'ErrorException', 'Traversable', 'IteratorAggregate', 'Iterator', 'ArrayAccess', 'Serializable', 'Closure');
744: $ref =
new ReflectionExtension($ext);
750: $func = (array)
@$func['user'];
753: foreach (array('classes', 'intf', 'func', 'consts') as $item) {
754: $s .=
'<span ' .
($
$item ?
'title="' .
implode(", ", $
$item) .
'"' :
'') .
'>' .
count($
$item) .
' ' .
$item .
'</span>, ';
759: if ($sender ===
'bluescreen') {
760: $arr[] =
'Report generated at ' .
@date('Y/m/d H:i:s', Debug::$time); // intentionally @
761: if (isset($_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'])) {
762: $url =
(isset($_SERVER['HTTPS']) &&
strcasecmp($_SERVER['HTTPS'], 'off') ?
'https://' :
'http://') .
htmlSpecialChars($_SERVER['HTTP_HOST'] .
$_SERVER['REQUEST_URI']);
763: $arr[] =
'<a href="' .
$url .
'">' .
$url .
'</a>';
765: $arr[] =
'PHP ' .
htmlSpecialChars(PHP_VERSION);
766: if (isset($_SERVER['SERVER_SOFTWARE'])) $arr[] =
htmlSpecialChars($_SERVER['SERVER_SOFTWARE']);
774: /********************* Firebug extension ****************d*g**/
779: * Sends variable dump to Firebug tab request/server.
780: * @param mixed variable to dump
781: * @param string unique key
782: * @return bool was successful?
786: return self::fireSend(2, array((string)
$key =>
$var));
792: * Sends message to Firebug console.
793: * @param mixed message to log
794: * @param string priority of message (LOG, INFO, WARN, ERROR, GROUP_START, GROUP_END)
795: * @param string optional label
796: * @return bool was successful?
798: public static function fireLog($message, $priority =
self::LOG, $label =
NULL)
800: if ($message instanceof
Exception) {
801: if ($priority !==
self::EXCEPTION &&
$priority !==
self::TRACE) {
802: $priority =
self::TRACE;
806: 'Message' =>
$message->getMessage(),
807: 'File' =>
$message->getFile(),
808: 'Line' =>
$message->getLine(),
809: 'Trace' =>
$message->getTrace(),
813: foreach ($message['Trace'] as & $row) {
814: if (empty($row['file'])) $row['file'] =
'?';
815: if (empty($row['line'])) $row['line'] =
'?';
817: } elseif ($priority ===
self::GROUP_START) {
821: return self::fireSend(1, self::replaceObjects(array(array('Type' =>
$priority, 'Label' =>
$label), $message)));
827: * Performs Firebug output.
828: * @see http://www.firephp.org
829: * @param int structure index
830: * @param array payload
831: * @return bool was successful?
833: private static function fireSend($index, $payload)
835: if (self::$productionMode) return NULL;
839: header('X-Wf-Protocol-nette: http://meta.wildfirehq.org/Protocol/JsonStream/0.2');
840: header('X-Wf-nette-Plugin-1: http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/0.2.0');
843: header('X-Wf-nette-Structure-1: http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1');
845: } elseif ($index ===
2) {
846: header('X-Wf-nette-Structure-2: http://meta.firephp.org/Wildfire/Structure/FirePHP/Dump/0.1');
867: static private function replaceObjects($val)
873: return $val =
@iconv('UTF-16', 'UTF-8//IGNORE', iconv('UTF-8', 'UTF-16//IGNORE', $val)); // intentionally @
876: foreach ($val as $k =>
$v) {
878: $k =
@iconv('UTF-16', 'UTF-8//IGNORE', iconv('UTF-8', 'UTF-16//IGNORE', $k)); // intentionally @
879: $val[$k] =
self::replaceObjects($v);
893: // if (!function_exists('dump')) { function dump($var, $return = FALSE) { return Debug::dump($var, $return); } }