#304924: Add an exception handler to Drupal. From: Damien Tournoud --- includes/common.inc | 86 ++++++++++++++++++++++++++++++++++++--------------- 1 files changed, 60 insertions(+), 26 deletions(-) diff --git includes/common.inc includes/common.inc index 3a00bee..b0040fc 100644 --- includes/common.inc +++ includes/common.inc @@ -595,36 +595,63 @@ function drupal_error_handler($errno, $message, $filename, $line, $context) { } if ($errno & (E_ALL)) { - $types = array(1 => 'error', 2 => 'warning', 4 => 'parse error', 8 => 'notice', 16 => 'core error', 32 => 'core warning', 64 => 'compile error', 128 => 'compile warning', 256 => 'user error', 512 => 'user warning', 1024 => 'user notice', 2048 => 'strict warning', 4096 => 'recoverable fatal error'); - - // For database errors, we want the line number/file name of the place that - // the query was originally called, not _db_query(). - if (isset($context[DB_ERROR])) { - $backtrace = array_reverse(debug_backtrace()); - - // List of functions where SQL queries can originate. - $query_functions = array('db_query', 'pager_query', 'db_query_range', 'db_query_temporary', 'update_sql'); - - // Determine where query function was called, and adjust line/file - // accordingly. - foreach ($backtrace as $index => $function) { - if (in_array($function['function'], $query_functions)) { - $line = $backtrace[$index]['line']; - $filename = $backtrace[$index]['file']; - break; - } - } - } + $types = array(1 => 'Error', 2 => 'Warning', 4 => 'Parse error', 8 => 'Notice', 16 => 'Core error', 32 => 'Core warning', 64 => 'Compile error', 128 => 'Compile warning', 256 => 'User error', 512 => 'User warning', 1024 => 'User notice', 2048 => 'Strict warning', 4096 => 'Recoverable fatal error'); + $backtrace = debug_backtrace(); + _drupal_log_error($types[$errno], $message, $backtrace); + } +} - $entry = $types[$errno] . ': ' . $message . ' in ' . $filename . ' on line ' . $line . '.'; +/** + * Log uncaught exceptions and output a meaningful error message to the user. + */ +function drupal_exception_handler($exception) { + // This is an exception handler. + $errno = -1; + + $backtrace = $exception->getTrace(); + // Add the line throwing the exception to the backtrace. + array_unshift($backtrace, array('line' => $exception->getLine(), 'file' => $exception->getFile())); - // Force display of error messages in update.php. - if (variable_get('error_level', 1) == 1 || strstr($_SERVER['SCRIPT_NAME'], 'update.php')) { - drupal_set_message($entry, 'error'); + // For PDOException errors, we try to return the initial caller, + // not internal functions of the database layer. + if ($exception instanceof PDOException) { + // The first element is the call. The second element is the caller. + // We skip calls that occured in one of the classes of the database layer + // or in one of its global functions. + $db_functions = array('db_query', 'pager_query', 'db_query_range', 'db_query_temporary', 'update_sql'); + while (($caller = $backtrace[1]) && + ((isset($caller['class']) && (strpos($caller['class'], 'Query') !== FALSE || strpos($caller['class'], 'Database') !== FALSE)) || + in_array($caller['function'], $db_functions))) { + // We remove that call. + array_shift($backtrace); } + } + + _drupal_log_error('uncaught exception', $exception->getMessage(), $backtrace); - watchdog('php', '%message in %file on line %line.', array('%error' => $types[$errno], '%message' => $message, '%file' => $filename, '%line' => $line), WATCHDOG_ERROR); + // Uncaught exceptions are always fatal in PHP. + // We try to display an error message. + if (!isset($GLOBALS['theme'])) { + drupal_maintenance_theme(); + $type = 'maintenance_page'; } + else { + $type = 'page'; + } + drupal_set_header('HTTP/1.1 503 Service unavailable'); + drupal_set_title(t('Error')); + print theme($type, t('The website encountered an unexpected error. Please try again later.')); +} + +function _drupal_log_error($type, $message, $backtrace) { + $caller = _drupal_get_last_caller($backtrace); + + // Force display of error messages in update.php. + if (variable_get('error_level', 1) == 1 || strstr($_SERVER['SCRIPT_NAME'], 'update.php')) { + drupal_set_message(t('@type: %message in %function (line %line of %file).', array('@type' => $type, '%message' => $message, '%function' => $caller['function'], '%line' => $caller['line'], '%file' => $caller['file'])), 'error'); + } + + watchdog('php', '%error: %message in %function (line %line of %file).', array('%error' => $type, '%message' => $message, '%function' => $caller['function'], '%file' => $caller['file'], '%line' => $caller['line']), WATCHDOG_ERROR); } /** @@ -637,10 +664,16 @@ function drupal_error_handler($errno, $message, $filename, $line, $context) { * An associative array with keys 'file', 'line' and 'function'. */ function _drupal_get_last_caller($backtrace) { + // Errors that occurs inside PHP internal functions + // does not generate informations about file and line. + while (!isset($backtrace[0]['line'])) { + array_shift($backtrace); + } + // The first trace is the call itself. // It gives us the line and the file of the last call. $call = $backtrace[0]; - + // The second call give us the function where the call originated. if (isset($backtrace[1])) { if (isset($backtrace[1]['class'])) { @@ -2508,6 +2541,7 @@ function _drupal_bootstrap_full() { require_once './includes/actions.inc'; // Set the Drupal custom error handler. set_error_handler('drupal_error_handler'); + set_exception_handler('drupal_exception_handler'); // Emit the correct charset HTTP header. drupal_set_header('Content-Type: text/html; charset=utf-8'); // Detect string handling method