Index: includes/bootstrap.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/bootstrap.inc,v retrieving revision 1.206.2.13 diff -u -r1.206.2.13 bootstrap.inc --- includes/bootstrap.inc 14 Sep 2009 13:33:39 -0000 1.206.2.13 +++ includes/bootstrap.inc 25 Sep 2009 20:32:42 -0000 @@ -991,7 +991,7 @@ } function _drupal_bootstrap($phase) { - global $conf; + global $conf, $db_prefix; switch ($phase) { @@ -1017,6 +1017,18 @@ break; case DRUPAL_BOOTSTRAP_DATABASE: + // The user agent header is used to pass a database prefix in the request when + // running tests. However, for security reasons, it is imperative that we + // validate we ourselves made the request. + $GLOBALS['simpletest_installed'] = TRUE; + if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^(simpletest\d+);/", $_SERVER['HTTP_USER_AGENT'], $matches)) { + if (!drupal_valid_test_ua($_SERVER['HTTP_USER_AGENT'])) { + header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden'); + exit; + } + $db_prefix .= $matches[1]; + } + // Initialize the default database. require_once './includes/database.inc'; db_set_active(); @@ -1205,3 +1217,46 @@ return $ip_address; } + +/** + * Validate the HMAC and timestamp of a user agent header from simpletest. + */ +function drupal_valid_test_ua($user_agent) { +// global $dbatabases; + global $db_url; + + list($prefix, $time, $salt, $hmac) = explode(';', $user_agent); + $check_string = $prefix . ';' . $time . ';' . $salt; + // We use the database credentials from settings.php to make the HMAC key, since + // the database is not yet initialized and we can't access any Drupal variables. + // The file properties add more entropy not easily accessible to others. +// $filepath = DRUPAL_ROOT . '/includes/bootstrap.inc'; + $filepath = './includes/bootstrap.inc'; +// $key = sha1(serialize($databases) . filectime($filepath) . fileinode($filepath), TRUE); + $key = sha1(serialize($db_url) . filectime($filepath) . fileinode($filepath), TRUE); + // The HMAC must match. + return $hmac == base64_encode(hash_hmac('sha1', $check_string, $key, TRUE)); +} + +/** + * Generate a user agent string with a HMAC and timestamp for simpletest. + */ +function drupal_generate_test_ua($prefix) { +// global $dbatabases; + global $db_url; + static $key; + + if (!isset($key)) { + // We use the database credentials to make the HMAC key, since we + // check the HMAC before the database is initialized. filectime() + // and fileinode() are not easily determined from remote. +// $filepath = DRUPAL_ROOT . '/includes/bootstrap.inc'; + $filepath = './includes/bootstrap.inc'; +// $key = sha1(serialize($databases) . filectime($filepath) . fileinode($filepath), TRUE); + $key = sha1(serialize($db_url) . filectime($filepath) . fileinode($filepath), TRUE); + } + // Generate a moderately secure HMAC based on the database credentials. + $salt = uniqid('', TRUE); + $check_string = $prefix . ';' . time() . ';' . $salt; + return $check_string . ';' . base64_encode(hash_hmac('sha1', $check_string, $key, TRUE)); +} Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.756.2.71 diff -u -r1.756.2.71 common.inc --- includes/common.inc 23 Sep 2009 09:09:29 -0000 1.756.2.71 +++ includes/common.inc 25 Sep 2009 20:32:42 -0000 @@ -520,7 +520,7 @@ // same time won't interfere with each other as they would if the database // prefix were stored statically in a file or database variable. if (is_string($db_prefix) && preg_match("/^simpletest\d+$/", $db_prefix, $matches)) { - $defaults['User-Agent'] = 'User-Agent: ' . $matches[0]; + $defaults['User-Agent'] = 'User-Agent: ' . drupal_generate_test_ua($matches[0]); } foreach ($headers as $header => $value) { @@ -2605,6 +2605,15 @@ unicode_check(); // Undo magic quotes fix_gpc_magic(); + + if (isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'simpletest') !== FALSE) { + // Valid SimpleTest user-agent, log fatal errors to test specific file + // directory. The user-agent is validated in DRUPAL_BOOTSTRAP_DATABASE + // phase so as long as it is a SimpleTest user-agent it is valid. + ini_set('log_errors', 1); + ini_set('error_log', file_directory_path() . '/error.log'); + } + // Load all enabled modules module_load_all(); // Let all modules take action before menu system handles the request @@ -3722,3 +3731,262 @@ } variable_set('css_js_query_string', $new_character . substr($string_history, 0, 19)); } + +/** + * Error reporting level: display no errors. + */ +define('ERROR_REPORTING_HIDE', 0); + +/** + * Error reporting level: display errors and warnings. + */ +define('ERROR_REPORTING_DISPLAY_SOME', 1); + +/** + * Error reporting level: display all messages. + */ +define('ERROR_REPORTING_DISPLAY_ALL', 2); + +/** + * Custom PHP error handler. + * + * @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($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' + ); + $caller = _drupal_get_last_caller(debug_backtrace()); + + // We treat recoverable errors as fatal. + _drupal_log_error(array( + '%type' => isset($types[$error_level]) ? $types[$error_level] : 'Unknown error', + '%message' => $message, + '%function' => $caller['function'], + '%file' => $caller['file'], + '%line' => $caller['line'], + ), $error_level == E_RECOVERABLE_ERROR); + } +} + +/** + * 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) { + // Log the message to the watchdog and return an error page to the user. + _drupal_log_error(_drupal_decode_exception($exception), TRUE); +} + +/** + * Decode an exception, especially to retrive the correct caller. + * + * @param $exception + * The exception object that was thrown. + * @return An error in the format expected by _drupal_log_error(). + */ +function _drupal_decode_exception($exception) { + $message = $exception->getMessage(); + + $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 (!empty($backtrace[1]) && ($caller = $backtrace[1]) && + ((isset($caller['class']) && (strpos($caller['class'], 'Query') !== FALSE || strpos($caller['class'], 'Database') !== FALSE || strpos($caller['class'], 'PDO') !== FALSE)) || + in_array($caller['function'], $db_functions))) { + // We remove that call. + array_shift($backtrace); + } + if (isset($exception->query_string, $exception->args)) { + $message .= ": " . $exception->query_string . "; " . print_r($exception->args, TRUE); + } + } + $caller = _drupal_get_last_caller($backtrace); + + return array( + '%type' => get_class($exception), + '%message' => $message, + '%function' => $caller['function'], + '%file' => $caller['file'], + '%line' => $caller['line'], + ); +} + +/** + * Log a PHP error or exception, display an error page in fatal cases. + * + * @param $error + * An array with the following keys: %type, %message, %function, %file, %line. + * @param $fatal + * TRUE if the error is fatal. + */ +function _drupal_log_error($error, $fatal = FALSE) { + // Initialize a maintenance theme if the boostrap was not complete. + // Do it early because drupal_set_message() triggers a drupal_theme_initialize(). + if ($fatal && (drupal_get_bootstrap_phase() != DRUPAL_BOOTSTRAP_FULL)) { + unset($GLOBALS['theme']); + if (!defined('MAINTENANCE_MODE')) { + define('MAINTENANCE_MODE', 'error'); + } + drupal_maintenance_theme(); + } + + // When running inside the testing framework, we relay the errors + // to the tested site by the way of HTTP headers. + if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^simpletest\d+;/", $_SERVER['HTTP_USER_AGENT']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) { + // $number does not use drupal_static as it should not be reset + // as it uniquely identifies each PHP error. + static $number = 0; + $assertion = array( + $error['%message'], + $error['%type'], + array( + 'function' => $error['%function'], + 'file' => $error['%file'], + 'line' => $error['%line'], + ), + ); + header('X-Drupal-Assertion-' . $number . ': ' . rawurlencode(serialize($assertion))); + $number++; + } + + try { + watchdog('php', '%type: %message in %function (line %line of %file).', $error, WATCHDOG_ERROR); + } + catch (Exception $e) { + // Ignore any additional watchdog exception, as that probably means + // that the database was not initialized correctly. + } + + if ($fatal) { + drupal_set_header('500 Service unavailable (with message)'); + } + + if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') { + if ($fatal) { + // When called from JavaScript, simply output the error message. + print t('%type: %message in %function (line %line of %file).', $error); + exit; + } + } + else { + // Display the message if the current error reporting level allows this type + // of message to be displayed, and unconditionnaly in update.php. + $error_level = variable_get('error_level', ERROR_REPORTING_DISPLAY_ALL); + $display_error = $error_level == ERROR_REPORTING_DISPLAY_ALL || ($error_level == ERROR_REPORTING_DISPLAY_SOME && $error['%type'] != 'Notice'); + if ($display_error || (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update')) { + $class = 'error'; + + // If error type is 'User notice' then treat it as debug information + // instead of an error message, see dd(). + if ($error['%type'] == 'User notice') { + $error['%type'] = 'Debug'; + $class = 'status'; + } + + drupal_set_message(t('%type: %message in %function (line %line of %file).', $error), $class); + } + + if ($fatal) { + drupal_set_title(t('Error')); + // We fallback to a maintenance page at this point, because the page generation + // itself can generate errors. + print theme('maintenance_page', t('The website encountered an unexpected error. Please try again later.')); + exit; + } + } +} + +/** + * Gets the last caller from a backtrace. + * + * @param $backtrace + * A standard PHP backtrace. + * @return + * An associative array with keys 'file', 'line' and 'function'. + */ +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'); + while (($backtrace && !isset($backtrace[0]['line'])) || + (isset($backtrace[1]['function']) && in_array($backtrace[1]['function'], $blacklist))) { + 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'])) { + $call['function'] = $backtrace[1]['class'] . $backtrace[1]['type'] . $backtrace[1]['function'] . '()'; + } + else { + $call['function'] = $backtrace[1]['function'] . '()'; + } + } + else { + $call['function'] = 'main()'; + } + return $call; +} + +/** + * Debug function used for outputting debug information. + * + * The debug information is passed on to trigger_error() after being converted + * to a string using _drupal_debug_message(). + * + * @param $data + * Data to be output. + * @param $label + * Label to prefix the data. + * @param $print_r + * Flag to switch between print_r() and var_export() for data conversion to + * string. Set $print_r to TRUE when dealing with a recursive data structure + * as var_export() will generate an error. + */ +function debug($data, $label = NULL, $print_r = FALSE) { + // Print $data contents to string. + $string = $print_r ? print_r($data, TRUE) : var_export($data, TRUE); + trigger_error(trim($label ? "$label: $string" : $string)); +} Index: install.php =================================================================== RCS file: /cvs/drupal/drupal/install.php,v retrieving revision 1.113.2.9 diff -u -r1.113.2.9 install.php --- install.php 27 Apr 2009 10:50:35 -0000 1.113.2.9 +++ install.php 25 Sep 2009 20:32:42 -0000 @@ -20,6 +20,14 @@ require_once './includes/bootstrap.inc'; drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION); + // The user agent header is used to pass a database prefix in the request when + // running tests. However, for security reasons, it is imperative that no + // installation be permitted using such a prefix. + if (isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], "simpletest") !== FALSE) { + header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden'); + exit; + } + // This must go after drupal_bootstrap(), which unsets globals! global $profile, $install_locale, $conf;