diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc
index e081d3a..384774e 100644
--- a/includes/bootstrap.inc
+++ b/includes/bootstrap.inc
@@ -41,6 +41,56 @@ define('ERROR_REPORTING_DISPLAY_SOME', 1);
define('ERROR_REPORTING_DISPLAY_ALL', 2);
/**
+ * Error reporting type of debug information: Add backtrace information to logs.
+ */
+define('ERROR_REPORTING_DISPLAY_LOGS', 1); // Do not start at zero.
+
+/**
+ * Error reporting type of debug information: Add backtrace information to messages on page.
+ */
+define('ERROR_REPORTING_DISPLAY_MESSAGES', 2);
+
+/**
+ * Error reporting type of debug information: Include scalar variables
+ */
+define('ERROR_REPORTING_DISPLAY_SCALAR', 3);
+
+/**
+ * Error reporting type of debug information: Include non-scalar variables
+ */
+define('ERROR_REPORTING_DISPLAY_NON_SCALAR', 4);
+
+/**
+ * Error reporting type of debug information: How may calls deep from the error line to show scalar variables
+ */
+define('ERROR_REPORTING_DISPLAY_SCALAR_CALL_START', 1);
+
+/**
+ * Error reporting type of debug information: How may calls deep from the error line to show scalar variables
+ */
+define('ERROR_REPORTING_DISPLAY_SCALAR_CALL_DEPTH', 0);
+
+/**
+ * Error reporting type of debug information: How may calls deep from the error line to show non-scalar variables
+ */
+define('ERROR_REPORTING_DISPLAY_NON_SCALAR_CALL_START', 1);
+
+/**
+ * Error reporting type of debug information: How may calls deep from the error line to show non-scalar variables
+ */
+define('ERROR_REPORTING_DISPLAY_NON_SCALAR_CALL_DEPTH', 0);
+
+/**
+ * Error reporting type of debug information: How Non-scalar values can become too long to practically display. Limit the output size with this setting.
+ */
+define('ERROR_REPORTING_DISPLAY_SCALAR_MAX_LENGTH', 1024);
+
+/**
+ * Error reporting type of debug information: How Non-scalar values can become too long to practically display. Limit the output size with this setting.
+ */
+define('ERROR_REPORTING_DISPLAY_NON_SCALAR_MAX_LENGTH', 1024);
+
+/**
* Indicates that the item should never be removed unless explicitly selected.
*
* The item may be removed using cache_clear_all() with a cache ID.
diff --git a/includes/errors.inc b/includes/errors.inc
index a9b7b5b..8f423d7 100644
--- a/includes/errors.inc
+++ b/includes/errors.inc
@@ -56,7 +56,8 @@ function _drupal_error_handler_real($error_level, $message, $filename, $line, $c
if ($error_level & error_reporting()) {
$types = drupal_error_levels();
list($severity_msg, $severity_level) = $types[$error_level];
- $caller = _drupal_get_last_caller(debug_backtrace());
+ $backtrace = debug_backtrace();
+ $caller = _drupal_get_last_caller($backtrace);
if (!function_exists('filter_xss_admin')) {
require_once DRUPAL_ROOT . '/includes/common.inc';
@@ -72,6 +73,7 @@ function _drupal_error_handler_real($error_level, $message, $filename, $line, $c
'%file' => $caller['file'],
'%line' => $caller['line'],
'severity_level' => $severity_level,
+ '!backtrace' => $backtrace,
), $error_level == E_RECOVERABLE_ERROR);
}
}
@@ -112,13 +114,14 @@ function _drupal_decode_exception($exception) {
$caller = _drupal_get_last_caller($backtrace);
return array(
- '%type' => get_class($exception),
+ '!backtrace' => $backtrace,
+ '%type' => get_class($exception),
// The standard PHP exception handler considers that the exception message
// is plain-text. We mimick this behavior here.
- '!message' => check_plain($message),
+ '!message' => check_plain($message),
'%function' => $caller['function'],
- '%file' => $caller['file'],
- '%line' => $caller['line'],
+ '%file' => $caller['file'],
+ '%line' => $caller['line'],
'severity_level' => WATCHDOG_ERROR,
);
}
@@ -162,9 +165,10 @@ function error_displayable($error = NULL) {
* Logs a PHP error or exception and displays an error page in fatal cases.
*
* @param $error
- * An array with the following keys: %type, !message, %function, %file, %line
- * and severity_level. All the parameters are plain-text, with the exception
- * of !message, which needs to be a safe HTML string.
+ * An array with the following keys: %type, !message, %function, %file,
+ * %line, severity_level, and backtrace. All the parameters are plain-text,
+ * with the exception of !message, which needs to be a safe HTML string, and
+ * backtrace, which is a standard PHP backtrace.
* @param $fatal
* TRUE if the error is fatal.
*/
@@ -179,6 +183,13 @@ function _drupal_log_error($error, $fatal = FALSE) {
drupal_maintenance_theme();
}
+ // Backtrace array is not a valid replacement value for t().
+ $backtrace = isset($error['!backtrace']) ? $error['!backtrace'] : '';
+ unset($error['!backtrace']);
+ if (!$backtrace) {
+ $backtrace = debug_backtrace(TRUE);
+ }
+
// When running inside the testing framework, we relay the errors
// to the tested site by the way of HTTP headers.
$test_info = &$GLOBALS['drupal_test_info'];
@@ -199,7 +210,22 @@ function _drupal_log_error($error, $fatal = FALSE) {
$number++;
}
- watchdog('php', '%type: !message in %function (line %line of %file).', $error, $error['severity_level']);
+ $st_opts = variable_get('error_backtrace_display', array());
+ if (array_sum($st_opts)) {
+ $error['!backtrace'] = format_backtrace($backtrace);
+ }
+
+ if ($st_opts[ERROR_REPORTING_DISPLAY_LOGS]) {
+ $message_line='%type:
!message
LINE: %line
FUNCTION: %function
FILE: %file';
+ if ($st_opts[ERROR_REPORTING_DISPLAY_LOGS]) {
+ $message_line .= ' !backtrace';
+ }
+ } else {
+ $message_line='%type: !message in %function (line %line of %file).';
+ }
+ if (!array_has_PDOException($error)) { // If it was a DB error don't write to the DB.
+ watchdog('php', $message_line, $error, $error['severity_level']);
+ }
if ($fatal) {
drupal_add_http_header('Status', '500 Service unavailable (with message)');
@@ -229,13 +255,30 @@ function _drupal_log_error($error, $fatal = FALSE) {
$class = 'error';
// If error type is 'User notice' then treat it as debug information
- // instead of an error message, see dd().
+ // instead of an error message.
+ // @see debug()
if ($error['%type'] == 'User notice') {
$error['%type'] = 'Debug';
$class = 'status';
}
- drupal_set_message(t('%type: !message in %function (line %line of %file).', $error), $class);
+ // Attempt to reduce verbosity by removing DRUPAL_ROOT from the file path
+ // in the message. This does not happen for (false) security.
+ $root_length = strlen(DRUPAL_ROOT);
+ if (substr($error['%file'], 0, $root_length) == DRUPAL_ROOT) {
+ $error['%file'] = substr($error['%file'], $root_length + 1);
+ }
+ $message = t('%type: !message in %function (line %line of %file).', $error);
+
+ // Check if verbose error reporting is on.
+ $error_level = variable_get('error_level', ERROR_REPORTING_DISPLAY_ALL);
+
+ if ($error_level != ERROR_REPORTING_HIDE) {
+ if ($st_opts[ERROR_REPORTING_DISPLAY_MESSAGES]) {
+ $message .= $error['!backtrace'];
+ }
+ }
+ drupal_set_message($message, $class);
}
if ($fatal) {
@@ -252,12 +295,12 @@ function _drupal_log_error($error, $fatal = FALSE) {
* Gets the last caller from a backtrace.
*
* @param $backtrace
- * A standard PHP backtrace.
+ * A standard PHP backtrace. Passed by reference.
*
* @return
* An associative array with keys 'file', 'line' and 'function'.
*/
-function _drupal_get_last_caller($backtrace) {
+function _drupal_get_last_caller(&$backtrace) {
// Errors that occur inside PHP internal functions do not generate
// information about file and line. Ignore black listed functions.
$blacklist = array('debug', '_drupal_error_handler', '_drupal_exception_handler');
@@ -284,3 +327,180 @@ function _drupal_get_last_caller($backtrace) {
}
return $call;
}
+
+/**
+ * We don't want to call watchdog if anywhere in the array there is a PDOException since PHP won't let us log a PDOException back to the DB.
+ * The function will check for the exception and return TRUE if it finds it in an array. It checks recursively through the array.
+ *
+ * @param type $array
+ * The array to check, will likely be $error from calling function
+ *
+ * @return boolean
+ * TRUE if found.
+ */
+function array_has_PDOException($array) {
+ if (array_key_exists('%type', $array) && stripos($array['%type'], 'PDOException') !== FALSE) {
+ return TRUE;
+ }
+ foreach ($array as $value) {
+ if (is_array($value) && array_has_PDOException($value)) {
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+/**
+ * Formats a backtrace into an HTML table.
+ *
+ * @param array $backtrace
+ * A standard PHP backtrace.
+ *
+ * @return string
+ * An HTML string.
+ */
+function format_backtrace(array $backtrace) {
+ $st_opts = variable_get('error_backtrace_display', array());
+
+ $show_vars = (variable_get('error_backtrace_display_scalar', ERROR_REPORTING_DISPLAY_SCALAR) ||
+ variable_get('error_backtrace_display_non_scalar', ERROR_REPORTING_DISPLAY_NON_SCALAR));
+
+ $callstack = array_reverse($backtrace, TRUE);
+ // TODO: Styling should be in CSS or should make use of drupal's existing CSS.
+ $cs1 = $show_vars ? 'Passed variables | ' : '';
+ $cs =<<
+ .backtrace td, .backtrace th {padding: 0 0.5em;}
+ .backtrace .row-bunch {border-top: 1px solid;}
+ .backtrace .var-cell {border: 1px dotted; width: 30%;};
+ pre.backtrace {font-family: "Andale Mono","Courier New",Courier,Lucidatypewriter,Fixed,monospace;}
+
+
+
+
+
+ Index |
+ Function called |
+ Caller line |
+ Caller file |
+ $cs1
+
+
+
+EOT
+ ;
+ $row_bunching = 3;
+ $data_to_show = array('function' => 0, 'line' => 0, 'file' => 0);
+ if ($show_vars) {
+ $data_to_show += array('args' => 0);
+ }
+ foreach ($callstack AS $call_depth => &$v) {
+ $row_bunching++;
+ if ($row_bunching >= 3) {
+ $row_class = 'class="row-bunch"';
+ $row_bunching = 0;
+ }
+ else {
+ $row_class = '';
+ }
+ $cs .= "$call_depth | ";
+
+ /*
+ * Let's set this locally since they'll be called many times in the loop
+ * and this function could be called many times.
+ */
+ static $static_set = FALSE;
+ static $show_scalar;
+ static $max_len_scalar;
+ static $start_vars_scalar;
+ static $stop_vars_scalar;
+ static $show_non_scalar;
+ static $max_len_non_scalar;
+ static $start_vars_non_scalar;
+ static $stop_vars_non_scalar;
+ static $output_buffering;
+
+ if (!$static_set) {
+ $show_scalar = variable_get('error_backtrace_display_scalar', ERROR_REPORTING_DISPLAY_SCALAR);
+ $max_len_scalar = variable_get('error_backtrace_display_scalar_max_length', ERROR_REPORTING_DISPLAY_SCALAR_MAX_LENGTH);
+ $start_vars_scalar = variable_get('error_backtrace_display_scalar_call_start', ERROR_REPORTING_DISPLAY_SCALAR_CALL_START);
+ $stop_vars_scalar = variable_get('error_backtrace_display_scalar_call_start', ERROR_REPORTING_DISPLAY_SCALAR_CALL_START)
+ + variable_get('error_backtrace_display_scalar_call_depth', ERROR_REPORTING_DISPLAY_SCALAR_CALL_DEPTH);
+
+ $show_non_scalar = variable_get('error_backtrace_display_non_scalar', ERROR_REPORTING_DISPLAY_NON_SCALAR);
+ $max_len_non_scalar = variable_get('error_backtrace_display_non_scalar_max_length', ERROR_REPORTING_DISPLAY_NON_SCALAR_MAX_LENGTH);
+ $start_vars_non_scalar = variable_get('error_backtrace_display_non_scalar_call_start', ERROR_REPORTING_DISPLAY_NON_SCALAR_CALL_START);
+ $stop_vars_non_scalar = variable_get('error_backtrace_display_non_scalar_call_start', ERROR_REPORTING_DISPLAY_NON_SCALAR_CALL_START)
+ + variable_get('error_backtrace_display_non_scalar_call_depth', ERROR_REPORTING_DISPLAY_NON_SCALAR_CALL_DEPTH);
+
+ $output_buffering = ini_get('output_buffering');
+
+ $static_set = TRUE;
+ }
+
+ foreach ($data_to_show as $k2 => $v2) {
+ if (isset($v[$k2])) {
+ switch ($k2) {
+ case 'args':
+ $data = '';
+ foreach ($v[$k2] as $index => $arg) {
+ if (is_scalar($arg)) {
+ if ($show_scalar && $call_depth >= $start_vars_scalar && $call_depth <= $stop_vars_scalar) {
+ $data .= _format_backtrace_fmt_var($arg, $max_len_scalar, gettype($arg));
+ }
+ }
+ else {
+ if ($show_non_scalar && $call_depth >= $start_vars_non_scalar && $call_depth <= $stop_vars_non_scalar) {
+ if (!empty($output_buffering)) {
+ ob_start();
+ var_dump($arg); // Handles recursion so do it this way if possible.
+ $var = ob_get_clean();
+ }
+ else {
+ $var = @var_export($arg, TRUE); // '@' = Suppress the "does not handle recursion" warnings.
+ }
+ $data .= _format_backtrace_fmt_var($var, $max_len_non_scalar);
+ }
+ }
+ }
+ $data = strlen($data) ? substr($data, 0, -4) : ' '; // Remove
off end of last var.
+ $cs .= "$data | ";
+ break;
+
+ default:
+ $data = str_replace(DRUPAL_ROOT . '/', '', $v[$k2]);
+ $data = htmlentities($data);
+ $cs .= "$data | ";
+ break;
+ }
+ }
+ else {
+ $cs .= " | ";
+ }
+ }
+ $cs .= '
';
+ }
+ $cs .=<<
+
+
+EOT
+ ;
+ return '
BACKTRACE:' . $cs;
+}
+
+function _format_backtrace_fmt_var($arg, $max_len, $type = FALSE) {
+ $var = str_replace(DRUPAL_ROOT . '/', '', $arg);
+ $diff = (strlen($var) - $max_len);
+ if ($diff > 0) {
+ $var = substr($var, 0, $max_len);
+ }
+ if ($type !== FALSE) {
+ $var = $type . ': ' . $var;
+ }
+ $var = htmlentities($var) . '
';
+ if ($diff > 0) {
+ $var .= '...(' . number_format($diff) . ' chars truncated)
';
+ }
+ return $var;
+}
diff --git a/modules/dblog/dblog.module b/modules/dblog/dblog.module
index 9183eed..4d4d9f5 100644
--- a/modules/dblog/dblog.module
+++ b/modules/dblog/dblog.module
@@ -144,20 +144,24 @@ function _dblog_get_message_types() {
* Note: Some values may be truncated to meet database column size restrictions.
*/
function dblog_watchdog(array $log_entry) {
- Database::getConnection('default', 'default')->insert('watchdog')
- ->fields(array(
- 'uid' => $log_entry['uid'],
- 'type' => substr($log_entry['type'], 0, 64),
- 'message' => $log_entry['message'],
- 'variables' => serialize($log_entry['variables']),
- 'severity' => $log_entry['severity'],
- 'link' => substr($log_entry['link'], 0, 255),
- 'location' => $log_entry['request_uri'],
- 'referer' => $log_entry['referer'],
- 'hostname' => substr($log_entry['ip'], 0, 128),
- 'timestamp' => $log_entry['timestamp'],
- ))
- ->execute();
+ if (!array_has_PDOException($log_entry)) { // If it was a DB error don't write to the DB.
+ Database::getConnection('default', 'default')->insert('watchdog')
+ ->fields(array(
+ 'uid' => $log_entry['uid'],
+ 'type' => substr($log_entry['type'], 0, 64),
+ 'message' => $log_entry['message'],
+ 'variables' => serialize($log_entry['variables']),
+ 'severity' => $log_entry['severity'],
+ 'link' => substr($log_entry['link'], 0, 255),
+ 'location' => $log_entry['request_uri'],
+ 'referer' => $log_entry['referer'],
+ 'hostname' => substr($log_entry['ip'], 0, 128),
+ 'timestamp' => $log_entry['timestamp'],
+ ))
+ ->execute();
+ } else {
+ _drupal_log_error($log_entry, TRUE);
+ }
}
/**
diff --git a/modules/system/system.admin.inc b/modules/system/system.admin.inc
index 22c202c..d87a6c7 100644
--- a/modules/system/system.admin.inc
+++ b/modules/system/system.admin.inc
@@ -1676,6 +1676,98 @@ function system_logging_settings() {
'#description' => t('It is recommended that sites running on production environments do not display any errors.'),
);
+ $form['backtrace_options'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Backtrace Options'),
+ );
+
+ $form['backtrace_options']['error_backtrace_display'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Choose how to monitor backtrace information.'),
+ '#default_value' => variable_get('error_backtrace_display', array()),
+ '#options' => array(
+ ERROR_REPORTING_DISPLAY_MESSAGES => t('Show on page'),
+ ERROR_REPORTING_DISPLAY_LOGS => t('Add to log'),
+ ),
+ '#description' => t('On production environments do not use "Show on page".'),
+ );
+
+ $form['backtrace_options']['scalar'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Scalar Variables Options'),
+ );
+
+ $form['backtrace_options']['scalar']['error_backtrace_display_scalar'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Include passed values'),
+ '#default_value' => variable_get('error_backtrace_display_scalar', ERROR_REPORTING_DISPLAY_SCALAR),
+ );
+
+ $form['backtrace_options']['scalar']['error_backtrace_display_scalar_call_start'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Backtrace start'),
+ '#default_value' => variable_get('error_backtrace_display_scalar_call_start', ERROR_REPORTING_DISPLAY_SCALAR_CALL_START),
+ '#size' => 4,
+ '#maxlength' => 4,
+ '#description' => t('From what depth to start showing scalar variables. 1 = the place the error occured.'),
+ );
+
+ $form['backtrace_options']['scalar']['error_backtrace_display_scalar_call_depth'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Backtrace depth'),
+ '#default_value' => variable_get('error_backtrace_display_scalar_call_depth', ERROR_REPORTING_DISPLAY_SCALAR_CALL_DEPTH),
+ '#size' => 4,
+ '#maxlength' => 4,
+ '#description' => t('How may calls deep from the starting point to show scalar variables.'),
+ );
+
+ $form['backtrace_options']['scalar']['error_backtrace_display_scalar_max_length'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Max length'),
+ '#default_value' => variable_get('error_backtrace_display_scalar_max_length', ERROR_REPORTING_DISPLAY_SCALAR_MAX_LENGTH),
+ '#size' => 10,
+ '#maxlength' => 10,
+ '#description' => t('Limit the output size in characters of scalar values.'),
+ );
+
+ $form['backtrace_options']['non_scalar'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Non-scalar Variables Options'),
+ );
+
+ $form['backtrace_options']['non_scalar']['error_backtrace_display_non_scalar'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Include passed values'),
+ '#default_value' => variable_get('error_backtrace_display_non_scalar', ERROR_REPORTING_DISPLAY_NON_SCALAR),
+ );
+
+ $form['backtrace_options']['non_scalar']['error_backtrace_display_non_scalar_call_start'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Backtrace start'),
+ '#default_value' => variable_get('error_backtrace_display_non_scalar_call_start', ERROR_REPORTING_DISPLAY_NON_SCALAR_CALL_START),
+ '#size' => 4,
+ '#maxlength' => 4,
+ '#description' => t('From what depth to start showing scalar variables. 1 = the place the error occured.'),
+ );
+
+ $form['backtrace_options']['non_scalar']['error_backtrace_display_non_scalar_call_depth'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Backtrace depth'),
+ '#default_value' => variable_get('error_backtrace_display_non_scalar_call_depth', ERROR_REPORTING_DISPLAY_NON_SCALAR_CALL_DEPTH),
+ '#size' => 4,
+ '#maxlength' => 4,
+ '#description' => t('How may calls deep from the starting point to show non-scalar variables.'),
+ );
+
+ $form['backtrace_options']['non_scalar']['error_backtrace_display_non_scalar_max_length'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Max length'),
+ '#default_value' => variable_get('error_backtrace_display_non_scalar_max_length', ERROR_REPORTING_DISPLAY_NON_SCALAR_MAX_LENGTH),
+ '#size' => 10,
+ '#maxlength' => 10,
+ '#description' => t('Limit the output size in characters of non-scalar values.'),
+ );
+
return system_settings_form($form);
}