#195416: Prefix should be per-connection. From: = --- bootstrap.inc | 71 +++++++++++--- common.inc | 13 +-- database/database.inc | 131 ++++++++++++++++++-------- database/mysql/database.inc | 2 database/mysql/schema.inc | 2 database/pgsql/database.inc | 2 database/schema.inc | 4 - database/sqlite/database.inc | 2 errors.inc | 2 install.core.inc | 16 +-- simpletest/drupal_web_test_case.php | 176 +++++++++++++++++++---------------- simpletest/simpletest.test | 2 default/default.settings.php | 33 ++++--- 13 files changed, 282 insertions(+), 174 deletions(-) diff --git includes/bootstrap.inc includes/bootstrap.inc index d3e5cd5..8533587 100644 --- includes/bootstrap.inc +++ includes/bootstrap.inc @@ -560,7 +560,7 @@ function drupal_settings_initialize() { global $base_url, $base_path, $base_root; // Export the following settings.php variables to the global namespace - global $databases, $db_prefix, $cookie_domain, $conf, $installed_profile, $update_free_access, $db_url, $drupal_hash_salt, $is_https, $base_secure_url, $base_insecure_url; + global $databases, $cookie_domain, $conf, $installed_profile, $update_free_access, $db_url, $drupal_hash_salt, $is_https, $base_secure_url, $base_insecure_url; $conf = array(); if (file_exists(DRUPAL_ROOT . '/' . conf_path() . '/settings.php')) { @@ -640,6 +640,17 @@ function drupal_settings_initialize() { } /** + * Retrieve (and optionally set) information about the current test being run. + */ +function drupal_test_info($value = FALSE) { + static $cache; + if ($value !== FALSE) { + $cache = $value; + } + return $cache; +} + +/** * Returns and optionally sets the filename for a system item (module, * theme, etc.). The filename, whether provided, cached, or retrieved * from the database, is only returned if the file exists. @@ -2134,14 +2145,6 @@ function _drupal_bootstrap_page_cache() { * Bootstrap database: Initialize database system and register autoload functions. */ function _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. - if (isset($_SERVER['HTTP_USER_AGENT']) && (strpos($_SERVER['HTTP_USER_AGENT'], "simpletest") !== FALSE) && !drupal_valid_test_ua($_SERVER['HTTP_USER_AGENT'])) { - header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden'); - exit; - } - // Redirect the user to the installation script if Drupal has not been // installed yet (i.e., if no $databases array has been defined in the // settings.php file) and we are not already installing. @@ -2150,6 +2153,40 @@ function _drupal_bootstrap_database() { install_goto('install.php'); } + // 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. + 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; + } + + // The first part of the user agent is the prefix itself. + $test_prefix = $matches[1]; + + // Set the test run id for use in other parts of Drupal. + drupal_test_info(array('test_run_id' => $test_prefix, 'in_child_site' => TRUE)); + + foreach ($GLOBALS['databases']['default'] as &$value) { + // Extract the current default database prefix. + if (!isset($value['prefix'])) { + $current_prefix = ''; + } + else if (is_array($value['prefix'])) { + $current_prefix = $value['prefix']['default']; + } + else { + $current_prefix = $value['prefix']; + } + + // Remove the current database prefix and replace it by our own. + $value['prefix'] = array( + 'default' => $current_prefix . $test_prefix, + ); + } + } + // Initialize the database system. Note that the connection // won't be initialized until it is actually requested. require_once DRUPAL_ROOT . '/includes/database/database.inc'; @@ -2207,15 +2244,15 @@ function drupal_get_bootstrap_phase() { * Validate the HMAC and timestamp of a user agent header from simpletest. */ function drupal_valid_test_ua($user_agent) { - global $databases; + global $drupal_hash_salt; 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 + // We use the salt 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'; - $key = serialize($databases) . filectime($filepath) . fileinode($filepath); + $key = $drupal_hash_salt . filectime($filepath) . fileinode($filepath); // The HMAC must match. return $hmac == drupal_hmac_base64($check_string, $key); } @@ -2224,15 +2261,15 @@ function drupal_valid_test_ua($user_agent) { * Generate a user agent string with a HMAC and timestamp for simpletest. */ function drupal_generate_test_ua($prefix) { - global $databases; + global $drupal_hash_salt; 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. + // We use the salt 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'; - $key = serialize($databases) . filectime($filepath) . fileinode($filepath); + $key = $drupal_hash_salt . filectime($filepath) . fileinode($filepath); } // Generate a moderately secure HMAC based on the database credentials. $salt = uniqid('', TRUE); diff --git includes/common.inc includes/common.inc index 5aeabd5..e09c35d 100644 --- includes/common.inc +++ includes/common.inc @@ -765,8 +765,6 @@ function drupal_access_denied() { * A string containing the response body that was received. */ function drupal_http_request($url, array $options = array()) { - global $db_prefix; - $result = new stdClass(); // Parse the URL and make sure we can handle the schema. @@ -866,8 +864,8 @@ function drupal_http_request($url, array $options = array()) { // user-agent is used to ensure that multiple testing sessions running at the // 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)) { - $options['headers']['User-Agent'] = drupal_generate_test_ua($matches[0]); + if ($test_info = drupal_test_info()) { + $options['headers']['User-Agent'] = drupal_generate_test_ua($test_info['test_run_id']); } $request = $options['method'] . ' ' . $path . " HTTP/1.0\r\n"; @@ -4488,13 +4486,14 @@ function _drupal_bootstrap_full() { module_load_all(); // Make sure all stream wrappers are registered. file_get_stream_wrappers(); - if (isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'simpletest') !== FALSE) { + + if (drupal_test_info()) { // 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. + // directory. ini_set('log_errors', 1); ini_set('error_log', file_directory_path() . '/error.log'); } + // Initialize $_GET['q'] prior to invoking hook_init(). drupal_path_initialize(); // Set a custom theme for the current page, if there is one. We need to run diff --git includes/database/database.inc includes/database/database.inc index d772094..6097840 100644 --- includes/database/database.inc +++ includes/database/database.inc @@ -259,6 +259,22 @@ abstract class DatabaseConnection extends PDO { */ protected $schema = NULL; + /** + * The default prefix used by this database connection. + * + * Separated from the other prefixes for performance reasons. + * + * @var string + */ + protected $defaultPrefix = ''; + + /** + * The non-default prefixes used by this database connection. + * + * @var array + */ + protected $prefixes = array(); + function __construct($dsn, $username, $password, $driver_options = array()) { // Because the other methods don't seem to work right. $driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION; @@ -343,6 +359,25 @@ abstract class DatabaseConnection extends PDO { } /** + * Preprocess the prefixes used by this database connection. + * + * @param $prefix + * The prefixes, in any of the multiple forms documented in + * default.settings.php. + */ + protected function setPrefix($prefix) { + if (is_array($prefix)) { + $this->defaultPrefix = isset($prefix['default']) ? $prefix['default'] : ''; + unset($prefix['default']); + $this->prefixes = $prefix; + } + else { + $this->defaultPrefix = $prefix; + $this->prefixes = array(); + } + } + + /** * Appends a database prefix to all tables in a query. * * Queries sent to Drupal should wrap all table names in curly brackets. This @@ -357,27 +392,12 @@ abstract class DatabaseConnection extends PDO { * The properly-prefixed string. */ public function prefixTables($sql) { - global $db_prefix; - - if (is_array($db_prefix)) { - if (array_key_exists('default', $db_prefix)) { - $tmp = $db_prefix; - unset($tmp['default']); - foreach ($tmp as $key => $val) { - $sql = strtr($sql, array('{' . $key . '}' => $val . $key)); - } - return strtr($sql, array('{' => $db_prefix['default'] , '}' => '')); - } - else { - foreach ($db_prefix as $key => $val) { - $sql = strtr($sql, array('{' . $key . '}' => $val . $key)); - } - return strtr($sql, array('{' => '' , '}' => '')); - } - } - else { - return strtr($sql, array('{' => $db_prefix , '}' => '')); + // Replace specific table prefixes first. + foreach ($this->prefixes as $key => $val) { + $sql = strtr($sql, array('{' . $key . '}' => $val . $key)); } + // Then replace remaining tables with the default prefix. + return strtr($sql, array('{' => $this->defaultPrefix , '}' => '')); } /** @@ -387,17 +407,12 @@ abstract class DatabaseConnection extends PDO { * is not used in prefixTables due to performance reasons. */ public function tablePrefix($table = 'default') { - global $db_prefix; - if (is_array($db_prefix)) { - if (isset($db_prefix[$table])) { - return $db_prefix[$table]; - } - elseif (isset($db_prefix['default'])) { - return $db_prefix['default']; - } - return ''; + if (isset($this->prefixes[$table])) { + return $this->prefixes[$table]; + } + else { + return $this->defaultPrefix; } - return $db_prefix; } /** @@ -1314,6 +1329,20 @@ abstract class Database { if (empty($value['driver'])) { $database_info[$index][$target] = $database_info[$index][$target][mt_rand(0, count($database_info[$index][$target]) - 1)]; } + + // Parse the prefix information. + if (!isset($database_info[$index][$target]['prefix'])) { + // Default to an empty prefix. + $database_info[$index][$target]['prefix'] = array( + 'default' => '', + ); + } + else if (!is_array($database_info[$index][$target]['prefix'])) { + // Transform the flat form into an array form. + $database_info[$index][$target]['prefix'] = array( + 'default' => $database_info[$index][$target]['prefix'], + ); + } } } @@ -1373,7 +1402,40 @@ abstract class Database { if (!empty(self::$databaseInfo[$key])) { return self::$databaseInfo[$key]; } + } + /** + * Rename a connection and its corresponding connection information. + * + * @param $old_key + * The old connection key. + * @param $new_key + * The new connection key. + */ + final public static function renameConnection($old_key, $new_key) { + if (empty(self::$databaseInfo)) { + self::parseConnectionInfo(); + } + + if (!empty(self::$databaseInfo[$old_key]) && empty(self::$databaseInfo[$new_key])) { + self::$databaseInfo[$new_key] = self::$databaseInfo[$old_key]; + unset(self::$databaseInfo[$old_key]); + if (isset(self::$connections[$old_key])) { + self::$connections[$new_key] = self::$connections[$old_key]; + unset(self::$connections[$old_key]); + } + } + } + + /** + * Remove a connection and its corresponding connection information. + * + * @param $key + * The connection key. + */ + final public static function removeConnection($key) { + unset(self::$databaseInfo[$key]); + unset(self::$connections[$key]); } /** @@ -1386,8 +1448,6 @@ abstract class Database { * The database target to open. */ final protected static function openConnection($key, $target) { - global $db_prefix; - if (empty(self::$databaseInfo)) { self::parseConnectionInfo(); } @@ -1415,13 +1475,6 @@ abstract class Database { $new_connection->setLogger(self::$logs[$key]); } - // We need to pass around the simpletest database prefix in the request - // and we put that in the user_agent header. The header HMAC was already - // validated in bootstrap.inc. - if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^(simpletest\d+);/", $_SERVER['HTTP_USER_AGENT'], $matches)) { - $db_prefix_string = is_array($db_prefix) ? $db_prefix['default'] : $db_prefix; - $db_prefix = $db_prefix_string . $matches[1]; - } return $new_connection; } diff --git includes/database/mysql/database.inc includes/database/mysql/database.inc index bddeb07..3b0de3a 100644 --- includes/database/mysql/database.inc +++ includes/database/mysql/database.inc @@ -47,6 +47,8 @@ class DatabaseConnection_mysql extends DatabaseConnection { PDO::ATTR_CASE => PDO::CASE_LOWER, )); + $this->setPrefix(isset($connection_options['prefix']) ? $connection_options['prefix'] : ''); + // Force MySQL to use the UTF-8 character set by default. $this->exec('SET NAMES "utf8"'); diff --git includes/database/mysql/schema.inc includes/database/mysql/schema.inc index 56fca57..60fe5c7 100644 --- includes/database/mysql/schema.inc +++ includes/database/mysql/schema.inc @@ -25,7 +25,7 @@ class DatabaseSchema_mysql extends DatabaseSchema { const COMMENT_MAX_COLUMN = 255; /** - * Get information about the table and database name from the db_prefix. + * Get information about the table and database name from the prefix. * * @return * A keyed array with information about the database, table name and prefix. diff --git includes/database/pgsql/database.inc includes/database/pgsql/database.inc index a46c16a..6b39685 100644 --- includes/database/pgsql/database.inc +++ includes/database/pgsql/database.inc @@ -31,6 +31,8 @@ class DatabaseConnection_pgsql extends DatabaseConnection { $connection_options['port'] = 5432; } + $this->setPrefix(isset($connection_options['prefix']) ? $connection_options['prefix'] : ''); + // PostgreSQL in trust mode doesn't require a password to be supplied. if (empty($connection_options['password'])) { $connection_options['password'] = null; diff --git includes/database/schema.inc includes/database/schema.inc index 8f3b806..40f7c34 100644 --- includes/database/schema.inc +++ includes/database/schema.inc @@ -170,11 +170,11 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface { } /** - * Get information about the table name and schema from the db_prefix. + * Get information about the table name and schema from the prefix. * * @param * Name of table to look prefix up for. Defaults to 'default' because thats - * default key for db_prefix. + * default key for prefix. * @return * A keyed array with information about the schema, table name and prefix. */ diff --git includes/database/sqlite/database.inc includes/database/sqlite/database.inc index d9ba8ae..d2a3935 100644 --- includes/database/sqlite/database.inc +++ includes/database/sqlite/database.inc @@ -44,6 +44,8 @@ class DatabaseConnection_sqlite extends DatabaseConnection { $this->connectionOptions = $connection_options; + $this->setPrefix(isset($connection_options['prefix']) ? $connection_options['prefix'] : ''); + parent::__construct('sqlite:' . $connection_options['database'], '', '', array( // Force column names to lower case. PDO::ATTR_CASE => PDO::CASE_LOWER, diff --git includes/errors.inc includes/errors.inc index 23a8d64..8d6f172 100644 --- includes/errors.inc +++ includes/errors.inc @@ -182,7 +182,7 @@ function _drupal_log_error($error, $fatal = FALSE) { // 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)) { + if (($test_info = drupal_test_info()) && $test_info['in_child_site'] && !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; diff --git includes/install.core.inc includes/install.core.inc index 87ed825..695f434 100644 --- includes/install.core.inc +++ includes/install.core.inc @@ -800,7 +800,7 @@ function install_verify_completed_task() { * Verifies the existing settings in settings.php. */ function install_verify_settings() { - global $db_prefix, $databases; + global $databases; // Verify existing settings (if any). if (!empty($databases) && install_verify_pdo()) { @@ -834,7 +834,7 @@ function install_verify_pdo() { * The form API definition for the database configuration form. */ function install_settings_form($form, &$form_state, &$install_state) { - global $databases, $db_prefix; + global $databases; $profile = $install_state['parameters']['profile']; $install_locale = $install_state['parameters']['locale']; @@ -945,6 +945,8 @@ function install_settings_form($form, &$form_state, &$install_state) { * Form API validate for install_settings form. */ function install_settings_form_validate($form, &$form_state) { + // TODO: remove. + $form_state['values']['prefix'] = $form_state['values']['db_prefix']; form_set_value($form['_database'], $form_state['values'], $form_state); $errors = install_database_errors($form_state['values'], $form_state['values']['settings_file']); foreach ($errors as $name => $message) { @@ -959,8 +961,8 @@ function install_database_errors($database, $settings_file) { global $databases; $errors = array(); // Verify the table prefix. - if (!empty($database['db_prefix']) && is_string($database['db_prefix']) && !preg_match('/^[A-Za-z0-9_.]+$/', $database['db_prefix'])) { - $errors['db_prefix'] = st('The database table prefix you have entered, %db_prefix, is invalid. The table prefix can only contain alphanumeric characters, periods, or underscores.', array('%db_prefix' => $database['db_prefix'])); + if (!empty($database['prefix']) && is_string($database['prefix']) && !preg_match('/^[A-Za-z0-9_.]+$/', $database['prefix'])) { + $errors['prefix'] = st('The database table prefix you have entered, %prefix, is invalid. The table prefix can only contain alphanumeric characters, periods, or underscores.', array('%prefix' => $database['prefix'])); } if (!empty($database['port']) && !is_numeric($database['port'])) { @@ -1000,16 +1002,12 @@ function install_database_errors($database, $settings_file) { function install_settings_form_submit($form, &$form_state) { global $install_state; - $database = array_intersect_key($form_state['values']['_database'], array_flip(array('driver', 'database', 'username', 'password', 'host', 'port'))); + $database = array_intersect_key($form_state['values']['_database'], array_flip(array('driver', 'database', 'username', 'password', 'host', 'port', 'prefix'))); // Update global settings array and save. $settings['databases'] = array( 'value' => array('default' => array('default' => $database)), 'required' => TRUE, ); - $settings['db_prefix'] = array( - 'value' => $form_state['values']['db_prefix'], - 'required' => TRUE, - ); $settings['drupal_hash_salt'] = array( 'value' => drupal_hash_base64(drupal_random_bytes(55)), 'required' => TRUE, diff --git modules/simpletest/drupal_web_test_case.php modules/simpletest/drupal_web_test_case.php index 10da398..15493fc 100644 --- modules/simpletest/drupal_web_test_case.php +++ modules/simpletest/drupal_web_test_case.php @@ -15,11 +15,11 @@ abstract class DrupalTestCase { protected $testId; /** - * The original database prefix, before it was changed for testing purposes. + * The database prefix of this test run. * * @var string */ - protected $originalPrefix = NULL; + protected $databasePrefix = NULL; /** * The original file directory, before it was changed for testing purposes. @@ -90,8 +90,6 @@ abstract class DrupalTestCase { * is the caller function itself. */ protected function assert($status, $message = '', $group = 'Other', array $caller = NULL) { - global $db_prefix; - // Convert boolean status to string status. if (is_bool($status)) { $status = $status ? 'pass' : 'fail'; @@ -105,10 +103,6 @@ abstract class DrupalTestCase { $caller = $this->getAssertionCall(); } - // Switch to non-testing database to store results in. - $current_db_prefix = $db_prefix; - $db_prefix = $this->originalPrefix; - // Creation assertion array that can be displayed while tests are running. $this->assertions[] = $assertion = array( 'test_id' => $this->testId, @@ -122,12 +116,11 @@ abstract class DrupalTestCase { ); // Store assertion for display after the test has completed. - db_insert('simpletest') + Database::getConnection('default', 'simpletest_original_default') + ->insert('simpletest') ->fields($assertion) ->execute(); - // Return to testing prefix. - $db_prefix = $current_db_prefix; // We do not use a ternary operator here to allow a breakpoint on // test failure. if ($status == 'pass') { @@ -560,10 +553,9 @@ class DrupalUnitTestCase extends DrupalTestCase { } protected function setUp() { - global $db_prefix, $conf; + global $conf; - // Store necessary current values before switching to prefixed database. - $this->originalPrefix = $db_prefix; + // Store necessary current values before switching to the test environment. $this->originalFileDirectory = file_directory_path(); spl_autoload_register('db_autoload'); @@ -572,11 +564,21 @@ class DrupalUnitTestCase extends DrupalTestCase { drupal_static_reset(); // Generate temporary prefixed database to ensure that tests have a clean starting point. - $db_prefix = Database::getConnection()->prefixTables('{simpletest' . mt_rand(1000, 1000000) . '}'); - $conf['file_public_path'] = $this->originalFileDirectory . '/' . $db_prefix; + $this->databasePrefix = Database::getConnection()->prefixTables('{simpletest' . mt_rand(1000, 1000000) . '}'); + $conf['file_public_path'] = $this->originalFileDirectory . '/' . $this->databasePrefix; + + // Clone the current connection and replace the current prefix. + $connection_info = Database::getConnectionInfo('default'); + Database::renameConnection('default', 'simpletest_original_default'); + foreach ($connection_info as $target => $value) { + $connection_info[$target]['prefix'] = array( + 'default' => $value['prefix']['default'] . $this->databasePrefix, + ); + } + Database::addConnectionInfo('default', 'default', $connection_info['default']); // Set user agent to be consistent with web test case. - $_SERVER['HTTP_USER_AGENT'] = $db_prefix; + $_SERVER['HTTP_USER_AGENT'] = $this->databasePrefix; // If locale is enabled then t() will try to access the database and // subsequently will fail as the database is not accessible. @@ -589,15 +591,16 @@ class DrupalUnitTestCase extends DrupalTestCase { } protected function tearDown() { - global $db_prefix, $conf; - if (preg_match('/simpletest\d+/', $db_prefix)) { - $conf['file_public_path'] = $this->originalFileDirectory; - // Return the database prefix to the original. - $db_prefix = $this->originalPrefix; - // Restore modules if necessary. - if (isset($this->originalModuleList)) { - module_list(TRUE, FALSE, FALSE, $this->originalModuleList); - } + global $conf; + + // Get back to the original connection. + Database::removeConnection('default'); + Database::renameConnection('simpletest_original_default', 'default'); + + $conf['file_public_path'] = $this->originalFileDirectory; + // Restore modules if necessary. + if (isset($this->originalModuleList)) { + module_list(TRUE, FALSE, FALSE, $this->originalModuleList); } } } @@ -1107,12 +1110,28 @@ class DrupalWebTestCase extends DrupalTestCase { * either a single array or a variable number of string arguments. */ protected function setUp() { - global $db_prefix, $user, $language, $conf; + global $user, $language, $conf; + + // Generate a temporary prefixed database to ensure that tests have a clean starting point. + $this->databasePrefix = 'simpletest' . mt_rand(1000, 1000000); + db_update('simpletest_test_id') + ->fields(array('last_prefix' => $this->databasePrefix)) + ->condition('test_id', $this->testId) + ->execute(); + + // Clone the current connection and replace the current prefix. + $connection_info = Database::getConnectionInfo('default'); + Database::renameConnection('default', 'simpletest_original_default'); + foreach ($connection_info as $target => $value) { + $connection_info[$target]['prefix'] = array( + 'default' => $value['prefix']['default'] . $this->databasePrefix, + ); + } + Database::addConnectionInfo('default', 'default', $connection_info['default']); // Store necessary current values before switching to prefixed database. $this->originalLanguage = $language; $this->originalLanguageDefault = variable_get('language_default'); - $this->originalPrefix = $db_prefix; $this->originalFileDirectory = file_directory_path(); $this->originalProfile = drupal_get_profile(); $clean_url_original = variable_get('clean_url', 0); @@ -1125,18 +1144,13 @@ class DrupalWebTestCase extends DrupalTestCase { $this->originalShutdownCallbacks = $callbacks; $callbacks = array(); - // Generate temporary prefixed database to ensure that tests have a clean starting point. - $db_prefix_new = Database::getConnection()->prefixTables('{simpletest' . mt_rand(1000, 1000000) . '}'); - db_update('simpletest_test_id') - ->fields(array('last_prefix' => $db_prefix_new)) - ->condition('test_id', $this->testId) - ->execute(); - $db_prefix = $db_prefix_new; + // Set the simpletest id for use in other parts of Drupal. + drupal_test_info(array('test_run_id' => $this->databasePrefix, 'in_child_site' => FALSE)); // Create test directory ahead of installation so fatal errors and debug // information can be logged during installation process. // Use temporary files directory with the same prefix as the database. - $public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($db_prefix, 10); + $public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10); $private_files_directory = $public_files_directory . '/private'; $temp_files_directory = $private_files_directory . '/temp'; @@ -1230,8 +1244,9 @@ class DrupalWebTestCase extends DrupalTestCase { * setup a clean environment for the current test run. */ protected function preloadRegistry() { - db_query('INSERT INTO {registry} SELECT * FROM ' . $this->originalPrefix . 'registry'); - db_query('INSERT INTO {registry_file} SELECT * FROM ' . $this->originalPrefix . 'registry_file'); + $original_connection = Database::getConnection('default', 'simpletest_original_default'); + db_query('INSERT INTO {registry} SELECT * FROM ' . $original_connection->prefixTables('{registry}')); + db_query('INSERT INTO {registry_file} SELECT * FROM ' . $original_connection->prefixTables('{registry_file}')); } /** @@ -1257,14 +1272,11 @@ class DrupalWebTestCase extends DrupalTestCase { * and reset the database prefix. */ protected function tearDown() { - global $db_prefix, $user, $language; + global $user, $language; // In case a fatal error occured that was not in the test process read the // log to pick up any fatal errors. - $db_prefix_temp = $db_prefix; - $db_prefix = $this->originalPrefix; - simpletest_log_read($this->testId, $db_prefix, get_class($this), TRUE); - $db_prefix = $db_prefix_temp; + simpletest_log_read($this->testId, $this->databasePrefix, get_class($this), TRUE); $emailCount = count(variable_get('drupal_test_email_collector', array())); if ($emailCount) { @@ -1272,53 +1284,52 @@ class DrupalWebTestCase extends DrupalTestCase { $this->pass($message, t('E-mail')); } - if (preg_match('/simpletest\d+/', $db_prefix)) { - // Delete temporary files directory. - file_unmanaged_delete_recursive($this->originalFileDirectory . '/simpletest/' . substr($db_prefix, 10)); - - // Remove all prefixed tables (all the tables in the schema). - $schema = drupal_get_schema(NULL, TRUE); - $ret = array(); - foreach ($schema as $name => $table) { - db_drop_table($name); - } + // Delete temporary files directory. + file_unmanaged_delete_recursive($this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10)); - // Return the database prefix to the original. - $db_prefix = $this->originalPrefix; + // Remove all prefixed tables (all the tables in the schema). + $schema = drupal_get_schema(NULL, TRUE); + $ret = array(); + foreach ($schema as $name => $table) { + db_drop_table($name); + } - // Restore original shutdown callbacks array to prevent original - // environment of calling handlers from test run. - $callbacks = &drupal_register_shutdown_function(); - $callbacks = $this->originalShutdownCallbacks; + // Get back to the original connection. + Database::removeConnection('default'); + Database::renameConnection('simpletest_original_default', 'default'); - // Return the user to the original one. - $user = $this->originalUser; - drupal_save_session(TRUE); + // Restore original shutdown callbacks array to prevent original + // environment of calling handlers from test run. + $callbacks = &drupal_register_shutdown_function(); + $callbacks = $this->originalShutdownCallbacks; - // Ensure that internal logged in variable and cURL options are reset. - $this->loggedInUser = FALSE; - $this->additionalCurlOptions = array(); + // Return the user to the original one. + $user = $this->originalUser; + drupal_save_session(TRUE); - // Reload module list and implementations to ensure that test module hooks - // aren't called after tests. - module_list(TRUE); - module_implements('', FALSE, TRUE); + // Ensure that internal logged in variable and cURL options are reset. + $this->loggedInUser = FALSE; + $this->additionalCurlOptions = array(); - // Reset the Field API. - field_cache_clear(); + // Reload module list and implementations to ensure that test module hooks + // aren't called after tests. + module_list(TRUE); + module_implements('', FALSE, TRUE); - // Rebuild caches. - $this->refreshVariables(); + // Reset the Field API. + field_cache_clear(); - // Reset language. - $language = $this->originalLanguage; - if ($this->originalLanguageDefault) { - $GLOBALS['conf']['language_default'] = $this->originalLanguageDefault; - } + // Rebuild caches. + $this->refreshVariables(); - // Close the CURL handler. - $this->curlClose(); + // Reset language. + $language = $this->originalLanguage; + if ($this->originalLanguageDefault) { + $GLOBALS['conf']['language_default'] = $this->originalLanguageDefault; } + + // Close the CURL handler. + $this->curlClose(); } /** @@ -1330,7 +1341,7 @@ class DrupalWebTestCase extends DrupalTestCase { * See the description of $curl_options for other options. */ protected function curlInitialize() { - global $base_url, $db_prefix; + global $base_url; if (!isset($this->curlHandle)) { $this->curlHandle = curl_init(); @@ -1342,6 +1353,7 @@ class DrupalWebTestCase extends DrupalTestCase { CURLOPT_SSL_VERIFYPEER => FALSE, // Required to make the tests run on https. CURLOPT_SSL_VERIFYHOST => FALSE, // Required to make the tests run on https. CURLOPT_HEADERFUNCTION => array(&$this, 'curlHeaderCallback'), + CURLOPT_USERAGENT => $this->databasePrefix, ); if (isset($this->httpauth_credentials)) { $curl_options[CURLOPT_HTTPAUTH] = $this->httpauth_method; @@ -1354,7 +1366,7 @@ class DrupalWebTestCase extends DrupalTestCase { } // We set the user agent header on each request so as to use the current // time and a new uniqid. - if (preg_match('/simpletest\d+/', $db_prefix, $matches)) { + if (preg_match('/simpletest\d+/', $this->databasePrefix, $matches)) { curl_setopt($this->curlHandle, CURLOPT_USERAGENT, drupal_generate_test_ua($matches[0])); } } diff --git modules/simpletest/simpletest.test modules/simpletest/simpletest.test index e1a9046..abddcdc 100644 --- modules/simpletest/simpletest.test +++ modules/simpletest/simpletest.test @@ -276,7 +276,7 @@ class SimpleTestFunctionalTest extends DrupalWebTestCase { * @return The test is being run from inside a CURL request. */ function inCURL() { - return isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^simpletest\d+/", $_SERVER['HTTP_USER_AGENT']); + return ($test_info = drupal_test_info()) && $test_info['in_child_site']; } } diff --git sites/default/default.settings.php sites/default/default.settings.php index f158aa2..1f50770 100644 --- sites/default/default.settings.php +++ sites/default/default.settings.php @@ -61,6 +61,7 @@ * 'password' => 'password', * 'host' => 'localhost', * 'port' => 3306, + * 'prefix' => 'myprefix_', * ); * * The "driver" property indicates what Drupal database driver the @@ -106,44 +107,45 @@ * 'username' => 'username', * 'password' => 'password', * 'host' => 'localhost', + * 'prefix' => 'main_', * ); * * You can optionally set prefixes for some or all database table names - * by using the $db_prefix setting. If a prefix is specified, the table + * by using the 'prefix' setting. If a prefix is specified, the table * name will be prepended with its value. Be sure to use valid database * characters only, usually alphanumeric and underscore. If no prefixes * are desired, leave it as an empty string ''. * - * To have all database names prefixed, set $db_prefix as a string: + * To have all database names prefixed, set 'prefix' as a string: * - * $db_prefix = 'main_'; + * 'prefix' => 'main_', * - * To provide prefixes for specific tables, set $db_prefix as an array. + * To provide prefixes for specific tables, set 'prefix' as an array. * The array's keys are the table names and the values are the prefixes. - * The 'default' element holds the prefix for any tables not specified - * elsewhere in the array. Example: + * The 'default' element is mandatory and holds the prefix for any tables + * not specified elsewhere in the array. Example: * - * $db_prefix = array( + * 'prefix' => array( * 'default' => 'main_', - * 'users' => 'shared_', + * 'users' => 'shared_', * 'sessions' => 'shared_', * 'role' => 'shared_', * 'authmap' => 'shared_', - * ); + * ), * - * You can also use db_prefix as a reference to a schema/database. This maybe + * You can also use a reference to a schema/database as a prefix. This maybe * useful if your Drupal installation exists in a schema that is not the default * or you want to access several databases from the same code base at the same * time. * Example: * - * $db_prefix = array( - * 'default' => 'main.', - * 'users' => 'shared.', + * 'prefix' => array( + * 'default' => 'main.', + * 'users' => 'shared.', * 'sessions' => 'shared.', * 'role' => 'shared.', * 'authmap' => 'shared.', - * ); + * ); * * NOTE: MySQL and SQLite's definition of a schema is a database. * @@ -154,6 +156,7 @@ * 'username' => 'username', * 'password' => 'password', * 'host' => 'localhost', + * 'prefix' => '', * ); * $databases['default']['default'] = array( * 'driver' => 'pgsql', @@ -161,6 +164,7 @@ * 'username' => 'username', * 'password' => 'password', * 'host' => 'localhost', + * 'prefix' => '', * ); * $databases['default']['default'] = array( * 'driver' => 'sqlite', @@ -168,7 +172,6 @@ * ); */ $databases = array(); -$db_prefix = ''; /** * Access control for update.php script.