diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index da3a277..ce4933b 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -64,9 +64,36 @@ const ERROR_REPORTING_DISPLAY_ALL = 'all'; /** - * Error reporting level: display all messages, plus backtrace information. + * Error reporting: Add stacktrace information to logs. */ -const ERROR_REPORTING_DISPLAY_VERBOSE = 'verbose'; +const ERROR_REPORTING_DISPLAY_LOG_EXTRA = 1; // Do not start at zero. + +/** + * Error reporting: Add stacktrace information to messages on page. + */ +const ERROR_REPORTING_DISPLAY_OUTPUT_EXTRA = 2; + +/** + * Error reporting: Include passed parameter types. + */ +const ERROR_REPORTING_DISPLAY_PASSED_PARAMS = 3; + +/** + * Error reporting: If ERROR_REPORTING_DISPLAY_PASSED_PARAMS + * then this setting will show data in the scalars. + */ +const ERROR_REPORTING_DISPLAY_PASSED_SCALARS = 4; + +/** + * Error reporting: Args can be comma or line separated. + */ +const ERROR_REPORTING_DISPLAY_LINE_PER_ARG = 5; + +/** + * Error reporting: If ERROR_REPORTING_DISPLAY_PASSED_SCALARS + * then this setting will shorten string with DRUPAL_ROOT substrings. + */ +const ERROR_REPORTING_DISPLAY_STRIP_DRUPAL_ROOT_STRING_ARGS = 6; /** * @defgroup logging_severity_levels Logging severity levels diff --git a/core/includes/errors.inc b/core/includes/errors.inc index 3fe23b7..9413745 100644 --- a/core/includes/errors.inc +++ b/core/includes/errors.inc @@ -56,7 +56,7 @@ 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]; - $backtrace = debug_backtrace(); + $backtrace = debug_backtrace(TRUE); $caller = _drupal_get_last_caller($backtrace); if (!function_exists('filter_xss_admin')) { @@ -73,7 +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, + '!backtrace' => $backtrace, ), $error_level == E_RECOVERABLE_ERROR); } } @@ -118,11 +118,11 @@ function _drupal_decode_exception($exception) { // The standard PHP exception handler considers that the exception message // is plain-text. We mimick this behavior here. '!message' => check_plain($message), + '!backtrace' => $backtrace, '%function' => $caller['function'], '%file' => $caller['file'], '%line' => $caller['line'], 'severity_level' => WATCHDOG_ERROR, - 'backtrace' => $backtrace, ); } @@ -150,6 +150,10 @@ function _drupal_render_exception_safe($exception) { * * @param $error * Optional error to examine for ERROR_REPORTING_DISPLAY_SOME. + * 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. * * @return * TRUE if an error should be displayed. @@ -157,12 +161,15 @@ function _drupal_render_exception_safe($exception) { function error_displayable($error = NULL) { $error_level = _drupal_get_error_level(); $updating = (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update'); - $all_errors_displayed = ($error_level == ERROR_REPORTING_DISPLAY_ALL) || - ($error_level == ERROR_REPORTING_DISPLAY_VERBOSE); - $error_needs_display = ($error_level == ERROR_REPORTING_DISPLAY_SOME && - isset($error) && $error['%type'] != 'Notice' && $error['%type'] != 'Strict warning'); - - return ($updating || $all_errors_displayed || $error_needs_display); + $all_errors_displayed = ($error_level == ERROR_REPORTING_DISPLAY_ALL); + $error_needs_display = ($error_level == ERROR_REPORTING_DISPLAY_SOME + && isset($error) && !in_array($error['%type'], array('Notice', 'Strict warning'))); + + // We always make displayable in the case of PDOException because we cannot + // write this type of error to the DB (PHP won't let us). Therefore, if we + // don't make it displayable we will lose the error information and may never + // track it down. + return ($updating || $all_errors_displayed || $error_needs_display || array_has_PDOException($error)); } /** @@ -187,9 +194,12 @@ function _drupal_log_error($error, $fatal = FALSE) { drupal_maintenance_theme(); } - // Backtrace array is not a valid replacement value for t(). - $backtrace = $error['backtrace']; - unset($error['backtrace']); + // Just in case somehow we get here without backtrace information added such + $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. @@ -211,13 +221,43 @@ function _drupal_log_error($error, $fatal = FALSE) { $number++; } - watchdog('php', '%type: !message in %function (line %line of %file).', $error, $error['severity_level']); + // Formatted function calls + $stacktrace_options = Drupal::config('system.logging')->get('stacktrace_display'); + + if (array_sum($stacktrace_options)) { + $error['!stacktrace'] = format_stacktrace($backtrace); + } + + $show_more = array(ERROR_REPORTING_DISPLAY_LOG_EXTRA, ERROR_REPORTING_DISPLAY_OUTPUT_EXTRA); + if (count(array_intersect($stacktrace_options, $show_more))) { + $message_log = $message_output = '%type:
!message
' . format_backtrace($backtrace) . ''; - } - drupal_set_message($message, $class, TRUE); + + drupal_set_message($message_output, $class); } if ($fatal) { @@ -279,9 +303,7 @@ function _drupal_log_error($error, $fatal = FALSE) { $output = theme('maintenance_page', array('content' => 'The website has encountered an error. Please try again later.')); $response = new Response($output, 500); - if ($fatal) { - $response->setStatusCode(500, '500 Service unavailable (with message)'); - } + $response->setStatusCode(500, '500 Service unavailable (with message)'); return $response; } @@ -349,38 +371,153 @@ function _drupal_get_last_caller(&$backtrace) { } /** - * Formats a backtrace into a plain-text string. + * 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. * - * The calls show values for scalar arguments and type names for complex ones. + * @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 stacktrace into an HTML table. * * @param array $backtrace * A standard PHP backtrace. * * @return string - * A plain-text line-wrapped string ready to be put inside
. + * An HTML string. */ -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'; +function format_stacktrace($backtrace) { + + $report_type = 'STACKTRACE:'; + + $callstack = array_reverse($backtrace, TRUE); + + $stacktrace_options = Drupal::config('system.logging')->get('stacktrace_display'); + $show_params = $stacktrace_options[ERROR_REPORTING_DISPLAY_PASSED_PARAMS]; + $show_scalar = $stacktrace_options[ERROR_REPORTING_DISPLAY_PASSED_SCALARS]; + $cleaner_string_args = $stacktrace_options[ERROR_REPORTING_DISPLAY_STRIP_DRUPAL_ROOT_STRING_ARGS]; + $args_sep = $stacktrace_options[ERROR_REPORTING_DISPLAY_STRIP_DRUPAL_ROOT_STRING_ARGS] ? '+EOT + ; + return '
' : ', '; + + if ($show_params) { + $params_table_header1 = 'Caller file '; + $params_table_header2 = 'Passed types '; + } + else { + $params_table_header1 = 'Caller file '; + $params_table_header2 = ''; + } + + // 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 th {border-width: 2px 0;} + .stacktrace .row-bunch {border-top: 2px solid #BFBFBA; border-bottom: 0;} + .stacktrace .first_column {border-left: 2px solid #BFBFBA;} + .stacktrace .last_column {border-right: 2px solid #BFBFBA;} + .stacktrace .row-bunch-last {border-bottom: 2px solid #BFBFBA;} + pre.stacktrace {font-family: "Andale Mono","Courier New",Courier,Lucidatypewriter,Fixed,monospace;} + + ++ +
++ + + +EOT + ; + + $column_names = array('function', 'line', 'file'); + if ($show_params) { + $column_names[] = 'args'; + } + $last_col_name = end($column_names); + + $first_col_class = 'class="first_column"'; + $last_col_classes[] = 'last_column'; + + $row_bunching = $row_bunching_max = 3; + foreach ($callstack AS $raw_data_no => &$raw_data) { + $classes = array(); + $row_bunching++; + if ($row_bunching >= $row_bunching_max) { + $classes[] = 'row-bunch'; + $row_bunching = 0; + } elseif ($raw_data_no == 0) { + $classes[] = 'row-bunch-last'; } - foreach ($trace['args'] as $arg) { - if (is_scalar($arg)) { - $call['args'][] = is_string($arg) ? '\'' . filter_xss($arg) . '\'' : $arg; - } - else { - $call['args'][] = ucfirst(gettype($arg)); + $row_class = 'class="'.implode(' ', $classes).'"'; + $cs .= "Index +Function called +Caller line + $params_table_header1 + $params_table_header2 +'; } - return $return; + $cs .=<< $raw_data_no "; + foreach ($column_names as $column_name) { + if (isset($raw_data[$column_name])) { + switch ($column_name) { + case 'args': + $last_col_classes[] = 'row-bunch'; + $cs .= ''; + $data = $raw_data[$column_name]; + $args = array(); + foreach ($raw_data as $arg) { + if ($show_scalar && is_scalar($arg)) { + if (is_string($arg)) { + if ($cleaner_string_args) { + $arg = str_replace(DRUPAL_ROOT, '...', $arg); + $arg = htmlentities($arg); + } + $args[] = '\'' . filter_xss($arg) . '\''; + } + else { + $args[] = $arg; + } + $args[] = is_string($arg) ? '\'' . filter_xss($arg) . '\'' : $arg; + } + else { + $args[] = gettype($arg); + } + } + $data = implode($args_sep, $args); + break; + + default: + $cs .= $column_name == $last_col_name ? ' ' : ' '; + $data = str_replace(DRUPAL_ROOT . '/', '', $raw_data[$column_name]); + $data = htmlentities($data); + } + $cs .= $data; + } else { + $cs .= $column_name == $last_col_name ? ' ' : ' '; } + $cs .= ' '; } - $return .= $call['function'] . '(' . implode(', ', $call['args']) . ")\n"; + $cs .= '+
' . format_backtrace($verbose_backtrace) . ''; + $message .= format_stacktrace($verbose_backtrace); } $this->error($message, $error_map[$severity], _drupal_get_last_caller($backtrace)); @@ -1152,10 +1152,12 @@ protected function exceptionHandler($exception) { // The exception message is run through check_plain() // by _drupal_decode_exception(). $decoded_exception = _drupal_decode_exception($exception); - unset($decoded_exception['backtrace']); - $message = format_string('%type: !message in %function (line %line of %file).
!backtrace', $decoded_exception + array( - '!backtrace' => format_backtrace($verbose_backtrace), - )); + unset($decoded_exception['!backtrace']); + $message = format_string('%type: !message in %function (line %line of %file). !backtrace', + $decoded_exception + array( + '!backtrace' => format_stacktrace($verbose_backtrace), + ) + ); $this->error($message, 'Uncaught exception', _drupal_get_last_caller($backtrace)); } diff --git a/core/modules/system/config/system.logging.yml b/core/modules/system/config/system.logging.yml index 3ecc76c..6bee54b 100644 --- a/core/modules/system/config/system.logging.yml +++ b/core/modules/system/config/system.logging.yml @@ -1 +1,9 @@ error_level: all + +stacktrace_display: + 1: 0 + 2: 0 + 3: 0 + 4: 0 + 5: 0 + 6: 6 diff --git a/core/modules/system/lib/Drupal/system/Form/LoggingForm.php b/core/modules/system/lib/Drupal/system/Form/LoggingForm.php index bc74529..917c6bd 100644 --- a/core/modules/system/lib/Drupal/system/Form/LoggingForm.php +++ b/core/modules/system/lib/Drupal/system/Form/LoggingForm.php @@ -26,6 +26,7 @@ public function getFormID() { */ public function buildForm(array $form, array &$form_state) { $config = $this->configFactory->get('system.logging'); + $form['error_level'] = array( '#type' => 'radios', '#title' => t('Error messages to display'), @@ -34,11 +35,25 @@ public function buildForm(array $form, array &$form_state) { ERROR_REPORTING_HIDE => t('None'), ERROR_REPORTING_DISPLAY_SOME => t('Errors and warnings'), ERROR_REPORTING_DISPLAY_ALL => t('All messages'), - ERROR_REPORTING_DISPLAY_VERBOSE => t('All messages, with backtrace information'), ), '#description' => t('It is recommended that sites running on production environments do not display any errors.'), ); + $form['stacktrace_display'] = array( + '#type' => 'checkboxes', + '#title' => t('Choose how to monitor stacktrace information.'), + '#default_value' => $config->get('stacktrace_display'), + '#options' => array( + ERROR_REPORTING_DISPLAY_LOG_EXTRA => t('Add to log'), + ERROR_REPORTING_DISPLAY_OUTPUT_EXTRA => t('Show on page'), + ERROR_REPORTING_DISPLAY_PASSED_PARAMS => t('Show types of passed parameters'), + ERROR_REPORTING_DISPLAY_PASSED_SCALARS => t('Show scalar content if showing types of passed parameters'), + ERROR_REPORTING_DISPLAY_LINE_PER_ARG => t('List passed parameters on per line'), + ERROR_REPORTING_DISPLAY_STRIP_DRUPAL_ROOT_STRING_ARGS => t('Strip DRUPAL_ROOT from paths and replace with "..." on string parameters (looks cleaner).'), + ), + '#description' => t('On production environments only use "Add to log" and then only when needed, not "Show on page".'), + ); + return parent::buildForm($form, $form_state); } @@ -48,6 +63,7 @@ public function buildForm(array $form, array &$form_state) { public function submitForm(array &$form, array &$form_state) { $this->configFactory->get('system.logging') ->set('error_level', $form_state['values']['error_level']) + ->set('stacktrace_display', $form_state['values']['stacktrace_display']) ->save(); parent::submitForm($form, $form_state);