Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

688 lines (547 sloc) 20.659 kb
<?php
/**
* This file is part of the Nette Framework (http://nette.org)
*
* Copyright (c) 2004 David Grudl (http://davidgrudl.com)
*
* For the full copyright and license information, please view
* the file license.txt that was distributed with this source code.
*/
namespace Nette\Diagnostics;
use Nette;
/**
* Debugger: displays and logs errors.
*
* Behavior is determined by two factors: mode & output
* - modes: production / development
* - output: HTML / AJAX / CLI / other (e.g. XML)
*
* @author David Grudl
*/
final class Debugger
{
/** @var bool in production mode is suppressed any debugging output */
public static $productionMode;
/** @var bool in console mode is omitted HTML output */
public static $consoleMode;
/** @var int timestamp with microseconds of the start of the request */
public static $time;
/** @var bool is AJAX request detected? */
private static $ajaxDetected;
/** @var string requested URI or command line */
public static $source;
/** @var string URL pattern mask to open editor */
public static $editor = 'editor://open/?file=%file&line=%line';
/** @var string command to open browser (use 'start ""' in Windows) */
public static $browser;
/********************* Debugger::dump() ****************d*g**/
/** @var int how many nested levels of array/object properties display {@link Debugger::dump()} */
public static $maxDepth = 3;
/** @var int how long strings display {@link Debugger::dump()} */
public static $maxLen = 150;
/** @var bool display location? {@link Debugger::dump()} */
public static $showLocation = FALSE;
/** @var array */
public static $consoleColors = array(
'bool' => '1;33',
'null' => '1;33',
'int' => '1;36',
'float' => '1;36',
'string' => '1;32',
'array' => '1;31',
'key' => '1;37',
'object' => '1;31',
'visibility' => '1;30',
'resource' => '1;37',
);
/********************* errors and exceptions reporting ****************d*g**/
/** server modes {@link Debugger::enable()} */
const DEVELOPMENT = FALSE,
PRODUCTION = TRUE,
DETECT = NULL;
/** @var BlueScreen */
public static $blueScreen;
/** @var bool|int determines whether any error will cause immediate death; if integer that it's matched against error severity */
public static $strictMode = FALSE; // $immediateDeath
/** @var bool disables the @ (shut-up) operator so that notices and warnings are no longer hidden */
public static $scream = FALSE;
/** @var array of callables specifies the functions that are automatically called after fatal error */
public static $onFatalError = array();
/** @var bool {@link Debugger::enable()} */
private static $enabled = FALSE;
/** @var mixed {@link Debugger::tryError()} FALSE means catching is disabled */
private static $lastError = FALSE;
/********************* logging ****************d*g**/
/** @var Logger */
public static $logger;
/** @var FireLogger */
public static $fireLogger;
/** @var string name of the directory where errors should be logged; FALSE means that logging is disabled */
public static $logDirectory;
/** @var string email to sent error notifications */
public static $email;
/** @deprecated */
public static $mailer;
/** @deprecated */
public static $emailSnooze;
/********************* debug bar ****************d*g**/
/** @var Bar */
public static $bar;
/** @var DefaultBarPanel */
private static $errorPanel;
/** @var DefaultBarPanel */
private static $dumpPanel;
/********************* Firebug extension ****************d*g**/
/** {@link Debugger::log()} and {@link Debugger::fireLog()} */
const DEBUG = 'debug',
INFO = 'info',
WARNING = 'warning',
ERROR = 'error',
CRITICAL = 'critical';
/**
* Static class - cannot be instantiated.
*/
final public function __construct()
{
throw new Nette\StaticClassException;
}
/**
* Static class constructor.
* @internal
*/
public static function _init()
{
self::$time = isset($_SERVER['REQUEST_TIME_FLOAT']) ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime(TRUE);
self::$consoleMode = PHP_SAPI === 'cli';
self::$productionMode = self::DETECT;
if (self::$consoleMode) {
self::$source = empty($_SERVER['argv']) ? 'cli' : 'cli: ' . implode(' ', $_SERVER['argv']);
} else {
self::$ajaxDetected = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest';
if (isset($_SERVER['REQUEST_URI'])) {
self::$source = (isset($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https://' : 'http://')
. (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : ''))
. $_SERVER['REQUEST_URI'];
}
}
self::$logger = new Logger;
self::$logDirectory = & self::$logger->directory;
self::$email = & self::$logger->email;
self::$mailer = & self::$logger->mailer;
self::$emailSnooze = & Logger::$emailSnooze;
self::$fireLogger = new FireLogger;
self::$blueScreen = new BlueScreen;
self::$blueScreen->addPanel(function($e) {
if ($e instanceof Nette\Templating\FilterException) {
return array(
'tab' => 'Template',
'panel' => '<p><b>File:</b> ' . Helpers::editorLink($e->sourceFile, $e->sourceLine)
. '&nbsp; <b>Line:</b> ' . ($e->sourceLine ? $e->sourceLine : 'n/a') . '</p>'
. ($e->sourceLine ? BlueScreen::highlightFile($e->sourceFile, $e->sourceLine) : '')
);
} elseif ($e instanceof Nette\Utils\NeonException && preg_match('#line (\d+)#', $e->getMessage(), $m)) {
if ($item = Helpers::findTrace($e->getTrace(), 'Nette\Config\Adapters\NeonAdapter::load')) {
return array(
'tab' => 'NEON',
'panel' => '<p><b>File:</b> ' . Helpers::editorLink($item['args'][0], $m[1]) . '&nbsp; <b>Line:</b> ' . $m[1] . '</p>'
. BlueScreen::highlightFile($item['args'][0], $m[1])
);
} elseif ($item = Helpers::findTrace($e->getTrace(), 'Nette\Utils\Neon::decode')) {
return array(
'tab' => 'NEON',
'panel' => BlueScreen::highlightPhp($item['args'][0], $m[1])
);
}
}
});
self::$bar = new Bar;
self::$bar->addPanel(new DefaultBarPanel('time'));
self::$bar->addPanel(new DefaultBarPanel('memory'));
self::$bar->addPanel(self::$errorPanel = new DefaultBarPanel('errors')); // filled by _errorHandler()
self::$bar->addPanel(self::$dumpPanel = new DefaultBarPanel('dumps')); // filled by barDump()
}
/********************* errors and exceptions reporting ****************d*g**/
/**
* Enables displaying or logging errors and exceptions.
* @param mixed production, development mode, autodetection or IP address(es) whitelist.
* @param string error log directory; enables logging in production mode, FALSE means that logging is disabled
* @param string administrator email; enables email sending in production mode
* @return void
*/
public static function enable($mode = NULL, $logDirectory = NULL, $email = NULL)
{
error_reporting(E_ALL | E_STRICT);
// production/development mode detection
if (is_bool($mode)) {
self::$productionMode = $mode;
} elseif ($mode !== self::DETECT || self::$productionMode === NULL) { // IP addresses or computer names whitelist detection
$list = is_string($mode) ? preg_split('#[,\s]+#', $mode) : (array) $mode;
if (!isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$list[] = '127.0.0.1';
$list[] = '::1';
}
self::$productionMode = !in_array(isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : php_uname('n'), $list, TRUE);
}
// logging configuration
if (is_string($logDirectory)) {
self::$logDirectory = realpath($logDirectory);
if (self::$logDirectory === FALSE) {
die(__METHOD__ . "() error: Log directory is not found or is not directory.");
}
} elseif ($logDirectory === FALSE) {
self::$logDirectory = FALSE;
} elseif (self::$logDirectory === NULL) {
self::$logDirectory = defined('APP_DIR') ? APP_DIR . '/../log' : getcwd() . '/log';
}
if (self::$logDirectory) {
ini_set('error_log', self::$logDirectory . '/php_error.log');
}
// php configuration
if (function_exists('ini_set')) {
ini_set('display_errors', !self::$productionMode); // or 'stderr'
ini_set('html_errors', FALSE);
ini_set('log_errors', FALSE);
} elseif (ini_get('display_errors') != !self::$productionMode && ini_get('display_errors') !== (self::$productionMode ? 'stderr' : 'stdout')) { // intentionally ==
die(__METHOD__ . "() error: Unable to set 'display_errors' because function ini_set() is disabled.");
}
if ($email) {
if (!is_string($email)) {
die(__METHOD__ . '() error: Email address must be a string.');
}
self::$email = $email;
}
if (!self::$enabled) {
register_shutdown_function(array(__CLASS__, '_shutdownHandler'));
set_exception_handler(array(__CLASS__, '_exceptionHandler'));
set_error_handler(array(__CLASS__, '_errorHandler'));
self::$enabled = TRUE;
}
}
/**
* Is Debug enabled?
* @return bool
*/
public static function isEnabled()
{
return self::$enabled;
}
/**
* Logs message or exception to file (if not disabled) and sends email notification (if enabled).
* @param string|Exception
* @param int one of constant Debugger::INFO, WARNING, ERROR (sends email), CRITICAL (sends email)
* @return string logged error filename
*/
public static function log($message, $priority = self::INFO)
{
if (self::$logDirectory === FALSE) {
return;
} elseif (!self::$logDirectory) {
throw new Nette\InvalidStateException('Logging directory is not specified in Nette\Diagnostics\Debugger::$logDirectory.');
}
if ($message instanceof \Exception) {
$exception = $message;
$message = ($message instanceof Nette\FatalErrorException
? 'Fatal error: ' . $exception->getMessage()
: get_class($exception) . ": " . $exception->getMessage())
. " in " . $exception->getFile() . ":" . $exception->getLine();
$hash = md5($exception /*5.2*. (method_exists($exception, 'getPrevious') ? $exception->getPrevious() : (isset($exception->previous) ? $exception->previous : ''))*/);
$exceptionFilename = "exception-" . @date('Y-m-d-H-i-s') . "-$hash.html";
foreach (new \DirectoryIterator(self::$logDirectory) as $entry) {
if (strpos($entry, $hash)) {
$exceptionFilename = $entry;
$saved = TRUE;
break;
}
}
} elseif (!is_string($message)) {
$message = Helpers::textDump($message);
}
self::$logger->log(array(
@date('[Y-m-d H-i-s]'),
trim($message),
self::$source ? ' @ ' . self::$source : NULL,
!empty($exceptionFilename) ? ' @@ ' . $exceptionFilename : NULL
), $priority);
if (!empty($exceptionFilename)) {
$exceptionFilename = self::$logDirectory . '/' . $exceptionFilename;
if (empty($saved) && $logHandle = @fopen($exceptionFilename, 'w')) {
ob_start(); // double buffer prevents sending HTTP headers in some PHP
ob_start(function($buffer) use ($logHandle) { fwrite($logHandle, $buffer); }, 4096);
self::$blueScreen->render($exception);
ob_end_flush();
ob_end_clean();
fclose($logHandle);
}
return strtr($exceptionFilename, '\\/', DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR);
}
}
/**
* Shutdown handler to catch fatal errors and execute of the planned activities.
* @return void
* @internal
*/
public static function _shutdownHandler()
{
if (!self::$enabled) {
return;
}
// fatal error handler
static $types = array(
E_ERROR => 1,
E_CORE_ERROR => 1,
E_COMPILE_ERROR => 1,
E_PARSE => 1,
);
$error = error_get_last();
if (isset($types[$error['type']])) {
self::_exceptionHandler(new Nette\FatalErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'], NULL, NULL, isset($error['trace']) ? $error['trace'] : NULL));
}
// debug bar (require HTML & development mode)
if (!connection_aborted() && self::$bar && !self::$productionMode && self::isHtmlMode()) {
self::$bar->render();
}
}
/**
* Handler to catch uncaught exception.
* @param \Exception
* @return void
* @internal
*/
public static function _exceptionHandler(\Exception $exception)
{
if (!headers_sent()) { // for PHP < 5.2.4
$protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1';
header($protocol . ' 500', TRUE, 500);
}
try {
if (self::$productionMode) {
try {
self::log($exception, self::ERROR);
} catch (\Exception $e) {
echo 'FATAL ERROR: unable to log error';
}
if (self::$consoleMode) {
echo "ERROR: the server encountered an internal error and was unable to complete your request.\n";
} elseif (self::isHtmlMode()) {
require __DIR__ . '/templates/error.phtml';
}
} else {
if (self::$consoleMode) { // dump to console
echo "$exception\n";
if ($file = self::log($exception)) {
echo "(stored in $file)\n";
if (self::$browser) {
exec(self::$browser . ' ' . escapeshellarg($file));
}
}
} elseif (!connection_aborted() && self::isHtmlMode()) { // dump to browser
self::$blueScreen->render($exception);
if (self::$bar) {
self::$bar->render();
}
} elseif (!self::fireLog($exception, self::ERROR)) { // AJAX or non-HTML mode
$file = basename(self::log($exception));
if (!headers_sent()) {
header("X-Nette-Error-Log: $file");
}
}
}
foreach (self::$onFatalError as $handler) {
call_user_func($handler, $exception);
}
} catch (\Exception $e) {
if (self::$productionMode) {
echo self::isHtmlMode() ? '<meta name=robots content=noindex>FATAL ERROR' : 'FATAL ERROR';
} else {
echo "FATAL ERROR: thrown ", get_class($e), ': ', $e->getMessage(),
"\nwhile processing ", get_class($exception), ': ', $exception->getMessage(), "\n";
}
}
self::$enabled = FALSE; // un-register shutdown function
exit(255);
}
/**
* Handler to catch warnings and notices.
* @param int level of the error raised
* @param string error message
* @param string file that the error was raised in
* @param int line number the error was raised at
* @param array an array of variables that existed in the scope the error was triggered in
* @return bool FALSE to call normal error handler, NULL otherwise
* @throws Nette\FatalErrorException
* @internal
*/
public static function _errorHandler($severity, $message, $file, $line, $context)
{
if (self::$scream) {
error_reporting(E_ALL | E_STRICT);
}
if (self::$lastError !== FALSE && ($severity & error_reporting()) === $severity) { // tryError mode
self::$lastError = new \ErrorException($message, 0, $severity, $file, $line);
return NULL;
}
if ($severity === E_RECOVERABLE_ERROR || $severity === E_USER_ERROR) {
if (Helpers::findTrace(/*5.2*PHP_VERSION_ID < 50205 ? debug_backtrace() : */debug_backtrace(FALSE), '*::__toString')) {
$previous = isset($context['e']) && $context['e'] instanceof \Exception ? $context['e'] : NULL;
self::_exceptionHandler(new Nette\FatalErrorException($message, 0, $severity, $file, $line, $context, $previous));
}
throw new Nette\FatalErrorException($message, 0, $severity, $file, $line, $context);
} elseif (($severity & error_reporting()) !== $severity) {
return FALSE; // calls normal error handler to fill-in error_get_last()
} elseif (!self::$productionMode && (is_bool(self::$strictMode) ? self::$strictMode : ((self::$strictMode & $severity) === $severity))) {
self::_exceptionHandler(new Nette\FatalErrorException($message, 0, $severity, $file, $line, $context));
}
static $types = array(
E_WARNING => 'Warning',
E_COMPILE_WARNING => 'Warning', // currently unable to handle
E_USER_WARNING => 'Warning',
E_NOTICE => 'Notice',
E_USER_NOTICE => 'Notice',
E_STRICT => 'Strict standards',
);
if (PHP_VERSION_ID >= 50300) {
$types += array(
E_DEPRECATED => 'Deprecated',
E_USER_DEPRECATED => 'Deprecated',
);
}
$message = 'PHP ' . (isset($types[$severity]) ? $types[$severity] : 'Unknown error') . ": $message";
$count = & self::$errorPanel->data["$message|$file|$line"];
if ($count++) { // repeated error
return NULL;
} elseif (self::$productionMode) {
self::log("$message in $file:$line", self::ERROR);
return NULL;
} else {
$ok = self::fireLog(new \ErrorException($message, 0, $severity, $file, $line), self::WARNING);
return !self::isHtmlMode() || (!self::$bar && !$ok) ? FALSE : NULL;
}
return FALSE; // call normal error handler
}
/** @deprecated */
public static function toStringException(\Exception $exception)
{
if (self::$enabled) {
self::_exceptionHandler($exception);
} else {
trigger_error($exception->getMessage(), E_USER_ERROR);
}
}
/** @deprecated */
public static function tryError()
{
trigger_error(__METHOD__ . '() is deprecated; use own error handler instead.', E_USER_DEPRECATED);
if (!self::$enabled && self::$lastError === FALSE) {
set_error_handler(array(__CLASS__, '_errorHandler'));
}
self::$lastError = NULL;
}
/** @deprecated */
public static function catchError(& $error)
{
trigger_error(__METHOD__ . '() is deprecated; use own error handler instead.', E_USER_DEPRECATED);
if (!self::$enabled && self::$lastError !== FALSE) {
restore_error_handler();
}
$error = self::$lastError;
self::$lastError = FALSE;
return (bool) $error;
}
/********************* useful tools ****************d*g**/
/**
* Dumps information about a variable in readable format.
* @param mixed variable to dump
* @param bool return output instead of printing it? (bypasses $productionMode)
* @return mixed variable itself or dump
*/
public static function dump($var, $return = FALSE)
{
if (!$return && self::$productionMode) {
return $var;
}
$output = "<pre class=\"nette-dump\">" . Helpers::htmlDump($var) . "</pre>\n";
if (!$return) {
$trace = /*5.2*PHP_VERSION_ID < 50205 ? debug_backtrace() : */debug_backtrace(FALSE);
$item = Helpers::findTrace($trace, 'dump') ?: Helpers::findTrace($trace, __CLASS__ . '::dump');
if (isset($item['file'], $item['line']) && is_file($item['file'])) {
$lines = file($item['file']);
preg_match('#dump\((.*)\)#', $lines[$item['line'] - 1], $m);
$output = substr_replace(
$output,
' title="' . htmlspecialchars((isset($m[0]) ? "$m[0] \n" : '') . "in file {$item['file']} on line {$item['line']}") . '"',
4, 0);
if (self::$showLocation) {
$output = substr_replace(
$output,
' <small>in ' . Helpers::editorLink($item['file'], $item['line']) . ":{$item['line']}</small>",
-8, 0);
}
}
}
if (self::$consoleMode) {
if (self::$consoleColors && substr(getenv('TERM'), 0, 5) === 'xterm') {
$output = preg_replace_callback('#<span class="nette-(\w+)">|</span>#', function($m) {
return "\033[" . (isset($m[1], Debugger::$consoleColors[$m[1]]) ? Debugger::$consoleColors[$m[1]] : '0') . "m";
}, $output);
}
$output = htmlspecialchars_decode(strip_tags($output), ENT_QUOTES);
}
if ($return) {
return $output;
} else {
echo $output;
return $var;
}
}
/**
* Starts/stops stopwatch.
* @param string name
* @return float elapsed seconds
*/
public static function timer($name = NULL)
{
static $time = array();
$now = microtime(TRUE);
$delta = isset($time[$name]) ? $now - $time[$name] : 0;
$time[$name] = $now;
return $delta;
}
/**
* Dumps information about a variable in Nette Debug Bar.
* @param mixed variable to dump
* @param string optional title
* @return mixed variable itself
*/
public static function barDump($var, $title = NULL)
{
if (!self::$productionMode) {
$dump = array();
foreach ((is_array($var) ? $var : array('' => $var)) as $key => $val) {
$dump[$key] = Helpers::clickableDump($val);
}
self::$dumpPanel->data[] = array('title' => $title, 'dump' => $dump);
}
return $var;
}
/**
* Sends message to FireLogger console.
* @param mixed message to log
* @return bool was successful?
*/
public static function fireLog($message)
{
if (!self::$productionMode) {
return self::$fireLogger->log($message);
}
}
private static function isHtmlMode()
{
return !self::$ajaxDetected && !self::$consoleMode
&& !preg_match('#^Content-Type: (?!text/html)#im', implode("\n", headers_list()));
}
/** @deprecated */
public static function addPanel(IBarPanel $panel, $id = NULL)
{
return self::$bar->addPanel($panel, $id);
}
}
Jump to Line
Something went wrong with that request. Please try again.