diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index b6c531c..ed15c60 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -41,6 +41,16 @@ define('ERROR_REPORTING_DISPLAY_SOME', 1); define('ERROR_REPORTING_DISPLAY_ALL', 2); /** + * Error reporting type of debug information: Add stacktrace or backtrace information to logs. + */ +define('ERROR_REPORTING_DISPLAY_LOGS', 1); // Do not start at zero. + +/** + * Error reporting type of debug information: Add stacktrace or backtrace information to messages on page. + */ +define('ERROR_REPORTING_DISPLAY_MESSAGES', 2); + +/** * 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..71266cf 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,14 +114,15 @@ 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), - '%function' => $caller['function'], - '%file' => $caller['file'], - '%line' => $caller['line'], - 'severity_level' => WATCHDOG_ERROR, + '!message' => check_plain($message), + '%function' => $caller['function'], + '%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(); + } + // 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,30 @@ 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_stacktrace_display', array()); + if (array_sum($st_opts)) { + $error['!stacktrace'] = format_stacktrace($backtrace); + } + $bt_opts=variable_get('error_backtrace_display', array()); + if (array_sum($bt_opts)) { + $error['!backtrace'] = format_backtrace($backtrace); + } + + if (!empty($st_opts[ERROR_REPORTING_DISPLAY_LOGS]) || !empty($bt_opts[ERROR_REPORTING_DISPLAY_LOGS])) { + $message_line='%type:
!message

LINE: %line
FUNCTION: %function
FILE: %file'; + if (!empty($st_opts[ERROR_REPORTING_DISPLAY_LOGS])) { + $message_line .= ' !stacktrace'; + } + if (!empty($bt_opts[ERROR_REPORTING_DISPLAY_LOGS])) { + $message_line .= ' !backtrace'; + } + } + else { + $message_line='%type: !message in %function (line %line of %file).'; + } + if (!array_has_PDO_connection_exception($error)) { + watchdog('php', $message_line, $error, $error['severity_level']); + } if ($fatal) { drupal_add_http_header('Status', '500 Service unavailable (with message)'); @@ -229,13 +263,39 @@ 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 (!empty($st_opts[ERROR_REPORTING_DISPLAY_MESSAGES])) { + $message .= $error['!stacktrace']; + } + if (!empty($bt_opts[ERROR_REPORTING_DISPLAY_MESSAGES])) { + // First trace is the error itself, already contained in the message. + // While the second trace is the error source and also contained in the + // message, the message doesn't contain argument values, so we output it + // once more in the backtrace. + array_shift($backtrace); + // Generate a backtrace containing only scalar argument values. + $message .= '
' . format_backtrace($backtrace) . '
'; + } + } + drupal_set_message($message, $class); } if ($fatal) { @@ -252,12 +312,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 +344,131 @@ function _drupal_get_last_caller($backtrace) { } return $call; } + +/** + * We don't want to call watchdog() if anywhere in the array there is a + * PDOException because the database has gone away, in order to avoid an endless + * loop. + * + * 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_PDO_connection_exception($array) { + if ( array_key_exists('%type', $array) + && stripos($array['%type'], 'PDOException') !== FALSE + && array_key_exists('', $array) + && stripos($array['!message'], 'gone away') !== FALSE + ) { + return TRUE; + } + foreach ($array as $value) { + if (is_array($value) && array_has_PDO_connection_exception($value)) { + return TRUE; + } + } + return FALSE; +} + +/** + * Formats a backtrace into a plain-text string. + * + * The calls show values for scalar arguments and type names for complex ones. + * + * @param array $backtrace + * A standard PHP backtrace. + * + * @return string + * A plain-text line-wrapped string ready to be put inside
.
+ */
+function format_backtrace(array $backtrace) {
+  $return = '';
+  foreach ($backtrace as $trace) {
+    $call = array('function' => '', 'args' => array());
+    if (isset($trace['class'])) {
+      $call['function'] = $trace['class'] . $trace['type'] . $trace['function'];
+    }
+    elseif (isset($trace['function'])) {
+      $call['function'] = $trace['function'];
+    }
+    else {
+      $call['function'] = 'main';
+    }
+    if (isset($trace['args'])) {
+      foreach ($trace['args'] as $arg) {
+        if (is_scalar($arg)) {
+          $call['args'][] = is_string($arg) ? '\'' . filter_xss($arg) . '\'' : $arg;
+        }
+        else {
+          $call['args'][] = ucfirst(gettype($arg));
+        }
+      }
+    }
+    $return .= $call['function'] . '(' . implode(', ', $call['args']) . ")\n";
+  }
+  return '

BACKTRACE:
' . $return; +} + +/** + * Formats a stacktrace into an HTML table. + * + * @param array $backtrace + * A standard PHP backtrace. + * + * @return string + * An HTML string. + */ +function format_stacktrace(array $backtrace) { + $callstack = array_reverse($backtrace, TRUE); + // TODO: Styling should be in CSS or should make use of drupal's existing CSS. + $cs =<< + .stacktrace td, .stacktrace th {padding: 0 0.5em;} + .stacktrace .row-bunch {border-top: 1px solid;} + pre.stacktrace {font-family: "Andale Mono","Courier New",Courier,Lucidatypewriter,Fixed,monospace;} + +
+  
+    
+      
+        
+        
+        
+        
+      
+    
+    
+EOT
+  ;
+  $row_bunching=3;
+  foreach ($callstack AS $k => &$v) {
+    $row_bunching++;
+    if ($row_bunching >= 3) {
+      $row_class = 'class="row-bunch"';
+      $row_bunching=0;
+    } else {
+      $row_class = '';
+    }
+    $cs .= "";
+    foreach (array('function'=>0, 'line'=>0, 'file'=>0) as $k2 => $v2) {
+      if (isset($v[$k2])) {
+        $data = str_replace(DRUPAL_ROOT . '/', '', $v[$k2]);
+        $data = htmlentities($data);
+        $cs .= "";
+      }
+    }
+    $cs .= '';
+  }
+  $cs .=<<
+  
IndexFunction calledCaller lineCaller file
$k$data
+
+EOT + ; + return '

STACKTRACE:' . $cs; +} diff --git a/modules/dblog/dblog.module b/modules/dblog/dblog.module index 9183eed..36c3958 100644 --- a/modules/dblog/dblog.module +++ b/modules/dblog/dblog.module @@ -144,20 +144,32 @@ 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(); + $log_msg = TRUE; + if (function_exists('array_has_PDO_connection_exception')) { + if (array_has_PDO_connection_exception($log_entry)) { + // If it was a DB connection error then do not write to the DB. + $log_msg = FALSE; + } + } + if ($log_msg) { + 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..1aa34f8 100644 --- a/modules/system/system.admin.inc +++ b/modules/system/system.admin.inc @@ -1676,6 +1676,28 @@ function system_logging_settings() { '#description' => t('It is recommended that sites running on production environments do not display any errors.'), ); + $form['error_stacktrace_display'] = array( + '#type' => 'checkboxes', + '#title' => t('Choose how to monitor stacktrace information.'), + '#default_value' => variable_get('error_stacktrace_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 only use "Add to log" and then only when needed.'), + ); + + $form['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 only use "Add to log" and then only when needed.'), + ); + return system_settings_form($form); }