From 967d8f67ac957924b9cb6855cb650f8f67d58cdc Mon Sep 17 00:00:00 2001 From: Dries Buytaert Date: Wed, 15 Oct 2008 16:05:51 +0000 Subject: [PATCH] - Patch #304924 by Damien Tournoud: extend error handler to manage exceptions. I have one exception and one fail. --- includes/bootstrap.inc | 4 + includes/common.inc | 157 ++++++++++++++------ includes/database/database.inc | 7 - modules/simpletest/drupal_web_test_case.php | 3 +- modules/simpletest/tests/common.test | 49 ++++++ modules/simpletest/tests/system_test.info | 1 - modules/simpletest/tests/system_test.module | 48 ++++++ 7 files changed, 215 insertions(+), 54 deletions(-) diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index fed5fe4afa3..c8ac6e7a0cf 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -371,6 +371,10 @@ function drupal_initialize_variables() { if (!isset($_SERVER['SERVER_PROTOCOL']) || ($_SERVER['SERVER_PROTOCOL'] != 'HTTP/1.0' && $_SERVER['SERVER_PROTOCOL'] != 'HTTP/1.1')) { $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.0'; } + // Enforce E_ALL, but allow users to set levels not part of E_ALL. + error_reporting(E_ALL | error_reporting()); + // Prevent PHP from generating HTML errors messages. + ini_set('html_errors', 0); } /** diff --git a/includes/common.inc b/includes/common.inc index 7830e4015d0..326d6a09d71 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -583,55 +583,122 @@ function drupal_http_request($url, $headers = array(), $method = 'GET', $data = */ /** - * Log errors as defined by administrator. + * Custom PHP error handler. * - * Error levels: - * - 0 = Log errors to database. - * - 1 = Log errors to database and to screen. + * @param $error_level + * The level of the error raised. + * @param $message + * The error message. + * @param $filename + * The filename that the error was raised in. + * @param $line + * The line number the error was raised at. + * @param $context + * An array that points to the active symbol table at the point the error occurred. */ -function drupal_error_handler($errno, $message, $filename, $line, $context) { - // If the @ error suppression operator was used, error_reporting will have - // been temporarily set to 0. - if (error_reporting() == 0) { - return; - } - - 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; - } - } - } - - $entry = $types[$errno] . ': ' . $message . ' in ' . $filename . ' on line ' . $line . '.'; - - // 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'); - } - - watchdog('php', '%message in %file on line %line.', array('%error' => $types[$errno], '%message' => $message, '%file' => $filename, '%line' => $line), WATCHDOG_ERROR); +function _drupal_error_handler($error_level, $message, $filename, $line, $context) { + if ($error_level & error_reporting()) { + // All these constants are documented at http://php.net/manual/en/errorfunc.constants.php + $types = array( + E_ERROR => 'Error', + E_WARNING => 'Warning', + E_PARSE => 'Parse error', + E_NOTICE => 'Notice', + E_CORE_ERROR => 'Core error', + E_CORE_WARNING => 'Core warning', + E_COMPILE_ERROR => 'Compile error', + E_COMPILE_WARNING => 'Compile warning', + E_USER_ERROR => 'User error', + E_USER_WARNING => 'User warning', + E_USER_NOTICE => 'User notice', + E_STRICT => 'Strict warning', + E_RECOVERABLE_ERROR => 'Recoverable fatal error' + ); + $backtrace = debug_backtrace(); + // We treat recoverable errors as fatal. + _drupal_log_error(isset($types[$error_level]) ? $types[$error_level] : 'Unknown error', $message, $backtrace, $error_level == E_RECOVERABLE_ERROR); } } /** - * Gets the last caller (file name and line of the call, function in which the - * call originated) from a backtrace. + * Custom PHP exception handler. + * + * Uncaught exceptions are those not enclosed in a try/catch block. They are + * always fatal: the execution of the script will stop as soon as the exception + * handler exits. + * + * @param $exception + * The exception object that was thrown. + */ +function _drupal_exception_handler($exception) { + $backtrace = $exception->getTrace(); + // Add the line throwing the exception to the backtrace. + array_unshift($backtrace, array('line' => $exception->getLine(), 'file' => $exception->getFile())); + + // For PDOException errors, we try to return the initial caller, + // skipping internal functions of the database layer. + if ($exception instanceof PDOException) { + // The first element in the stack is the call, the second element gives us the caller. + // We skip calls that occurred 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); + } + } + + // Log the message to the watchdog and return an error page to the user. + _drupal_log_error(get_class($exception), $exception->getMessage(), $backtrace, TRUE); +} + +/** + * Log a PHP error or exception, display an error page in fatal cases. + * + * @param $type + * The type of the error (Error, Warning, ...). + * @param $message + * The message associated to the error. + * @param $backtrace + * The backtrace of function calls that led to this error. + * @param $fatal + * TRUE if the error is fatal. + */ +function _drupal_log_error($type, $message, $backtrace, $fatal) { + $caller = _drupal_get_last_caller($backtrace); + + // Initialize a maintenance theme early if the boostrap was not complete. + // Do it early because drupal_set_message() triggers an init_theme(). + if ($fatal && (drupal_get_bootstrap_phase() != DRUPAL_BOOTSTRAP_FULL)) { + unset($GLOBALS['theme']); + define('MAINTENANCE_MODE', 'error'); + drupal_maintenance_theme(); + } + + // Force display of error messages in update.php. + if (variable_get('error_level', 1) == 1 || (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update')) { + 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', '%type: %message in %function (line %line of %file).', array('%type' => $type, '%message' => $message, '%function' => $caller['function'], '%file' => $caller['file'], '%line' => $caller['line']), WATCHDOG_ERROR); + + if ($fatal) { + drupal_set_header($_SERVER['SERVER_PROTOCOL'] . ' Service unavailable'); + drupal_set_title(t('Error')); + if (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL) { + print theme('page', t('The website encountered an unexpected error. Please try again later.'), FALSE); + } + else { + print theme('maintenance_page', t('The website encountered an unexpected error. Please try again later.'), FALSE); + } + exit; + } +} + +/** + * Gets the last caller from a backtrace. * * @param $backtrace * A standard PHP backtrace. @@ -2514,7 +2581,9 @@ function _drupal_bootstrap_full() { require_once DRUPAL_ROOT . '/includes/mail.inc'; require_once DRUPAL_ROOT . '/includes/actions.inc'; // Set the Drupal custom error handler. - set_error_handler('drupal_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 diff --git a/includes/database/database.inc b/includes/database/database.inc index f0525e800a5..91cb9d7d7e9 100644 --- a/includes/database/database.inc +++ b/includes/database/database.inc @@ -6,13 +6,6 @@ * Base classes for the database layer. */ -/** - * A hash value to check when outputting database errors, md5('DB_ERROR'). - * - * @see drupal_error_handler() - */ -define('DB_ERROR', 'a515ac9c2796ca0e23adbe92c68fc9fc'); - /** * @defgroup database Database abstraction layer * @{ diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php index eba232b8612..988f1592c6a 100644 --- a/modules/simpletest/drupal_web_test_case.php +++ b/modules/simpletest/drupal_web_test_case.php @@ -322,8 +322,7 @@ class DrupalWebTestCase { * @see set_error_handler */ function errorHandler($severity, $message, $file = NULL, $line = NULL) { - $severity = $severity & error_reporting(); - if ($severity) { + if ($severity & error_reporting()) { $error_map = array( E_STRICT => 'Run-time notice', E_WARNING => 'Warning', diff --git a/modules/simpletest/tests/common.test b/modules/simpletest/tests/common.test index 43e7b02160e..0d7ad985c2f 100644 --- a/modules/simpletest/tests/common.test +++ b/modules/simpletest/tests/common.test @@ -251,3 +251,52 @@ class DrupalSetContentTestCase extends DrupalWebTestCase { } } } + +/** + * Tests Drupal error and exception handlers. + */ +class DrupalErrorHandlerUnitTest extends DrupalWebTestCase { + function getInfo() { + return array( + 'name' => t('Drupal error handlers'), + 'description' => t("Performs tests on the Drupal error and exception handler."), + 'group' => t('System'), + ); + } + + function setUp() { + parent::setUp('system_test'); + } + + /** + * Test the error handler. + */ + function testErrorHandler() { + $this->drupalGet('system-test/generate-warnings'); + + $this->assertErrorMessage('Notice', 'system_test.module', 'system_test_generate_warnings() ', 'Undefined variable'); + $this->assertErrorMessage('Warning', 'system_test.module', 'system_test_generate_warnings() ', 'Division by zero'); + $this->assertErrorMessage('User notice', 'system_test.module', 'system_test_generate_warnings() ', 'Drupal is awesome'); + } + + /** + * Test the exception handler. + */ + function testExceptionHandler() { + $this->drupalGet('system-test/trigger-exception'); + $this->assertErrorMessage('Exception', 'system_test.module', 'system_test_trigger_exception()', 'Drupal is awesome'); + + $this->drupalGet('system-test/trigger-pdo-exception'); + $this->assertErrorMessage('PDOException', 'system_test.module', 'system_test_trigger_pdo_exception()', 'Base table or view not found'); + } + + /** + * Helper function: assert that the logged message is correct. + */ + function assertErrorMessage($type, $file, $function, $message) { + $this->assertText($type, t("Found '%type' in error page.", array('%type' => $type))); + $this->assertText($file, t("Found '%file' in error page.", array('%file' => $file))); + $this->assertText($function, t("Found '%function' in error page.", array('%function' => $function))); + $this->assertText($message, t("Found '%message' in error page.", array('%message' => $message))); + } +} diff --git a/modules/simpletest/tests/system_test.info b/modules/simpletest/tests/system_test.info index f70a1a79fa0..1910e87ae08 100644 --- a/modules/simpletest/tests/system_test.info +++ b/modules/simpletest/tests/system_test.info @@ -5,4 +5,3 @@ package = Testing version = VERSION core = 7.x files[] = system_test.module -hidden = TRUE diff --git a/modules/simpletest/tests/system_test.module b/modules/simpletest/tests/system_test.module index 188facc96e2..bd3eff6c581 100644 --- a/modules/simpletest/tests/system_test.module +++ b/modules/simpletest/tests/system_test.module @@ -48,6 +48,27 @@ function system_test_menu() { 'type' => MENU_CALLBACK, ); + $items['system-test/generate-warnings'] = array( + 'title' => 'Generate warnings', + 'page callback' => 'system_test_generate_warnings', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + $items['system-test/trigger-exception'] = array( + 'title' => 'Trigger an exception', + 'page callback' => 'system_test_trigger_exception', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + + $items['system-test/trigger-pdo-exception'] = array( + 'title' => 'Trigger a PDO exception', + 'page callback' => 'system_test_trigger_pdo_exception', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + return $items; } @@ -120,3 +141,30 @@ function system_test_modules_uninstalled($modules) { drupal_set_message(t('hook_modules_uninstalled fired for aggregator')); } } + +/** + * Menu callback; generate warnings to test the error handler. + */ +function system_test_generate_warnings() { + // This will generate a notice. + $monkey_love = $bananas; + // This will generate a warning. + $awesomely_big = 1/0; + // This will generate a user error. + trigger_error("Drupal is awesome", E_USER_NOTICE); + return ""; +} + +/** + * Menu callback; trigger an exception to test the exception handler. + */ +function system_test_trigger_exception() { + throw new Exception("Drupal is awesome"); +} + +/** + * Menu callback; trigger an exception to test the exception handler. + */ +function system_test_trigger_pdo_exception() { + db_query("SELECT * FROM bananas_are_awesome"); +}