Commit 267ebfb7 authored by Dries's avatar Dries
Browse files

- Patch #195416 by Damien Tournoud, David Strauss: table prefixes should be...

- Patch #195416 by Damien Tournoud, David Strauss: table prefixes should be per database connection.
parent 02b74638
...@@ -560,7 +560,7 @@ function drupal_settings_initialize() { ...@@ -560,7 +560,7 @@ function drupal_settings_initialize() {
global $base_url, $base_path, $base_root; global $base_url, $base_path, $base_root;
// Export the following settings.php variables to the global namespace // 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(); $conf = array();
if (file_exists(DRUPAL_ROOT . '/' . conf_path() . '/settings.php')) { if (file_exists(DRUPAL_ROOT . '/' . conf_path() . '/settings.php')) {
...@@ -2149,20 +2149,48 @@ function _drupal_bootstrap_page_cache() { ...@@ -2149,20 +2149,48 @@ function _drupal_bootstrap_page_cache() {
* Bootstrap database: Initialize database system and register autoload functions. * Bootstrap database: Initialize database system and register autoload functions.
*/ */
function _drupal_bootstrap_database() { function _drupal_bootstrap_database() {
// 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.
if (empty($GLOBALS['databases']) && !drupal_installation_attempted()) {
include_once DRUPAL_ROOT . '/includes/install.inc';
install_goto('install.php');
}
// The user agent header is used to pass a database prefix in the request when // 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 // running tests. However, for security reasons, it is imperative that we
// validate we ourselves made the request. // 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'])) { 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'); header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
exit; exit;
} }
// Redirect the user to the installation script if Drupal has not been // The first part of the user agent is the prefix itself.
// installed yet (i.e., if no $databases array has been defined in the $test_prefix = $matches[1];
// settings.php file) and we are not already installing.
if (empty($GLOBALS['databases']) && !drupal_installation_attempted()) { // Set the test run id for use in other parts of Drupal.
include_once DRUPAL_ROOT . '/includes/install.inc'; $test_info = &$GLOBALS['drupal_test_info'];
install_goto('install.php'); $test_info['test_run_id'] = $test_prefix;
$test_info['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 // Initialize the database system. Note that the connection
...@@ -2222,15 +2250,15 @@ function drupal_get_bootstrap_phase() { ...@@ -2222,15 +2250,15 @@ function drupal_get_bootstrap_phase() {
* Validate the HMAC and timestamp of a user agent header from simpletest. * Validate the HMAC and timestamp of a user agent header from simpletest.
*/ */
function drupal_valid_test_ua($user_agent) { function drupal_valid_test_ua($user_agent) {
global $databases; global $drupal_hash_salt;
list($prefix, $time, $salt, $hmac) = explode(';', $user_agent); list($prefix, $time, $salt, $hmac) = explode(';', $user_agent);
$check_string = $prefix . ';' . $time . ';' . $salt; $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 database is not yet initialized and we can't access any Drupal variables.
// The file properties add more entropy not easily accessible to others. // The file properties add more entropy not easily accessible to others.
$filepath = DRUPAL_ROOT . '/includes/bootstrap.inc'; $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. // The HMAC must match.
return $hmac == drupal_hmac_base64($check_string, $key); return $hmac == drupal_hmac_base64($check_string, $key);
} }
...@@ -2239,15 +2267,15 @@ function drupal_valid_test_ua($user_agent) { ...@@ -2239,15 +2267,15 @@ function drupal_valid_test_ua($user_agent) {
* Generate a user agent string with a HMAC and timestamp for simpletest. * Generate a user agent string with a HMAC and timestamp for simpletest.
*/ */
function drupal_generate_test_ua($prefix) { function drupal_generate_test_ua($prefix) {
global $databases; global $drupal_hash_salt;
static $key; static $key;
if (!isset($key)) { if (!isset($key)) {
// We use the database credentials to make the HMAC key, since we // We use the salt from settings.php to make the HMAC key, since
// check the HMAC before the database is initialized. filectime() // the database is not yet initialized and we can't access any Drupal variables.
// and fileinode() are not easily determined from remote. // The file properties add more entropy not easily accessible to others.
$filepath = DRUPAL_ROOT . '/includes/bootstrap.inc'; $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. // Generate a moderately secure HMAC based on the database credentials.
$salt = uniqid('', TRUE); $salt = uniqid('', TRUE);
......
...@@ -766,8 +766,6 @@ function drupal_access_denied() { ...@@ -766,8 +766,6 @@ function drupal_access_denied() {
* A string containing the response body that was received. * A string containing the response body that was received.
*/ */
function drupal_http_request($url, array $options = array()) { function drupal_http_request($url, array $options = array()) {
global $db_prefix;
$result = new stdClass(); $result = new stdClass();
// Parse the URL and make sure we can handle the schema. // Parse the URL and make sure we can handle the schema.
...@@ -867,8 +865,9 @@ function drupal_http_request($url, array $options = array()) { ...@@ -867,8 +865,9 @@ function drupal_http_request($url, array $options = array()) {
// user-agent is used to ensure that multiple testing sessions running at the // 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 // same time won't interfere with each other as they would if the database
// prefix were stored statically in a file or database variable. // prefix were stored statically in a file or database variable.
if (is_string($db_prefix) && preg_match("/simpletest\d+/", $db_prefix, $matches)) { $test_info = &$GLOBALS['drupal_test_info'];
$options['headers']['User-Agent'] = drupal_generate_test_ua($matches[0]); if (!empty($test_info['test_run_id'])) {
$options['headers']['User-Agent'] = drupal_generate_test_ua($test_info['test_run_id']);
} }
$request = $options['method'] . ' ' . $path . " HTTP/1.0\r\n"; $request = $options['method'] . ' ' . $path . " HTTP/1.0\r\n";
...@@ -4505,13 +4504,15 @@ function _drupal_bootstrap_full() { ...@@ -4505,13 +4504,15 @@ function _drupal_bootstrap_full() {
module_load_all(); module_load_all();
// Make sure all stream wrappers are registered. // Make sure all stream wrappers are registered.
file_get_stream_wrappers(); file_get_stream_wrappers();
if (isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'simpletest') !== FALSE) {
// Valid SimpleTest user-agent, log fatal errors to test specific file $test_info = &$GLOBALS['drupal_test_info'];
// directory. The user-agent is validated in DRUPAL_BOOTSTRAP_DATABASE if (!empty($test_info['in_child_site'])) {
// phase so as long as it is a SimpleTest user-agent it is valid. // Running inside the simpletest child site, log fatal errors to test
// specific file directory.
ini_set('log_errors', 1); ini_set('log_errors', 1);
ini_set('error_log', file_directory_path() . '/error.log'); ini_set('error_log', file_directory_path() . '/error.log');
} }
// Initialize $_GET['q'] prior to invoking hook_init(). // Initialize $_GET['q'] prior to invoking hook_init().
drupal_path_initialize(); drupal_path_initialize();
// Set a custom theme for the current page, if there is one. We need to run // Set a custom theme for the current page, if there is one. We need to run
......
...@@ -259,7 +259,26 @@ abstract class DatabaseConnection extends PDO { ...@@ -259,7 +259,26 @@ abstract class DatabaseConnection extends PDO {
*/ */
protected $schema = NULL; 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()) { function __construct($dsn, $username, $password, $driver_options = array()) {
// Initialize and prepare the connection prefix.
$this->setPrefix(isset($this->connectionOptions['prefix']) ? $this->connectionOptions['prefix'] : '');
// Because the other methods don't seem to work right. // Because the other methods don't seem to work right.
$driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION; $driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
...@@ -342,6 +361,25 @@ public function getConnectionOptions() { ...@@ -342,6 +361,25 @@ public function getConnectionOptions() {
return $this->connectionOptions; return $this->connectionOptions;
} }
/**
* 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. * Appends a database prefix to all tables in a query.
* *
...@@ -357,27 +395,12 @@ public function getConnectionOptions() { ...@@ -357,27 +395,12 @@ public function getConnectionOptions() {
* The properly-prefixed string. * The properly-prefixed string.
*/ */
public function prefixTables($sql) { public function prefixTables($sql) {
global $db_prefix; // Replace specific table prefixes first.
foreach ($this->prefixes as $key => $val) {
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)); $sql = strtr($sql, array('{' . $key . '}' => $val . $key));
} }
return strtr($sql, array('{' => $db_prefix['default'] , '}' => '')); // Then replace remaining tables with the default prefix.
} return strtr($sql, array('{' => $this->defaultPrefix , '}' => ''));
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 , '}' => ''));
}
} }
/** /**
...@@ -387,17 +410,12 @@ public function prefixTables($sql) { ...@@ -387,17 +410,12 @@ public function prefixTables($sql) {
* is not used in prefixTables due to performance reasons. * is not used in prefixTables due to performance reasons.
*/ */
public function tablePrefix($table = 'default') { public function tablePrefix($table = 'default') {
global $db_prefix; if (isset($this->prefixes[$table])) {
if (is_array($db_prefix)) { return $this->prefixes[$table];
if (isset($db_prefix[$table])) {
return $db_prefix[$table];
}
elseif (isset($db_prefix['default'])) {
return $db_prefix['default'];
} }
return ''; else {
return $this->defaultPrefix;
} }
return $db_prefix;
} }
/** /**
...@@ -1314,6 +1332,20 @@ final public static function parseConnectionInfo() { ...@@ -1314,6 +1332,20 @@ final public static function parseConnectionInfo() {
if (empty($value['driver'])) { if (empty($value['driver'])) {
$database_info[$index][$target] = $database_info[$index][$target][mt_rand(0, count($database_info[$index][$target]) - 1)]; $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 +1405,58 @@ final public static function getConnectionInfo($key = 'default') { ...@@ -1373,7 +1405,58 @@ final public static function getConnectionInfo($key = 'default') {
if (!empty(self::$databaseInfo[$key])) { if (!empty(self::$databaseInfo[$key])) {
return 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.
* @return
* TRUE in case of success, FALSE otherwise.
*/
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])) {
// Migrate the database connection information.
self::$databaseInfo[$new_key] = self::$databaseInfo[$old_key];
unset(self::$databaseInfo[$old_key]);
// Migrate over the DatabaseConnection object if it exists.
if (isset(self::$connections[$old_key])) {
self::$connections[$new_key] = self::$connections[$old_key];
unset(self::$connections[$old_key]);
}
return TRUE;
}
else {
return FALSE;
}
}
/**
* Remove a connection and its corresponding connection information.
*
* @param $key
* The connection key.
* @return
* TRUE in case of success, FALSE otherwise.
*/
final public static function removeConnection($key) {
if (isset(self::$databaseInfo[$key])) {
unset(self::$databaseInfo[$key]);
unset(self::$connections[$key]);
return TRUE;
}
else {
return FALSE;
}
} }
/** /**
...@@ -1386,8 +1469,6 @@ final public static function getConnectionInfo($key = 'default') { ...@@ -1386,8 +1469,6 @@ final public static function getConnectionInfo($key = 'default') {
* The database target to open. * The database target to open.
*/ */
final protected static function openConnection($key, $target) { final protected static function openConnection($key, $target) {
global $db_prefix;
if (empty(self::$databaseInfo)) { if (empty(self::$databaseInfo)) {
self::parseConnectionInfo(); self::parseConnectionInfo();
} }
...@@ -1415,13 +1496,6 @@ final protected static function openConnection($key, $target) { ...@@ -1415,13 +1496,6 @@ final protected static function openConnection($key, $target) {
$new_connection->setLogger(self::$logs[$key]); $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; return $new_connection;
} }
......
...@@ -25,7 +25,7 @@ class DatabaseSchema_mysql extends DatabaseSchema { ...@@ -25,7 +25,7 @@ class DatabaseSchema_mysql extends DatabaseSchema {
const COMMENT_MAX_COLUMN = 255; 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 * @return
* A keyed array with information about the database, table name and prefix. * A keyed array with information about the database, table name and prefix.
......
...@@ -170,11 +170,11 @@ public function nextPlaceholder() { ...@@ -170,11 +170,11 @@ public function nextPlaceholder() {
} }
/** /**
* Get information about the table name and schema from the db_prefix. * Get information about the table name and schema from the prefix.
* *
* @param * @param
* Name of table to look prefix up for. Defaults to 'default' because thats * Name of table to look prefix up for. Defaults to 'default' because thats
* default key for db_prefix. * default key for prefix.
* @return * @return
* A keyed array with information about the schema, table name and prefix. * A keyed array with information about the schema, table name and prefix.
*/ */
......
...@@ -182,7 +182,8 @@ function _drupal_log_error($error, $fatal = FALSE) { ...@@ -182,7 +182,8 @@ function _drupal_log_error($error, $fatal = FALSE) {
// When running inside the testing framework, we relay the errors // When running inside the testing framework, we relay the errors
// to the tested site by the way of HTTP headers. // 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)) { $test_info = &$GLOBALS['drupal_test_info'];
if (!empty($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 // $number does not use drupal_static as it should not be reset
// as it uniquely identifies each PHP error. // as it uniquely identifies each PHP error.
static $number = 0; static $number = 0;
......
...@@ -800,7 +800,7 @@ function install_verify_completed_task() { ...@@ -800,7 +800,7 @@ function install_verify_completed_task() {
* Verifies the existing settings in settings.php. * Verifies the existing settings in settings.php.
*/ */
function install_verify_settings() { function install_verify_settings() {
global $db_prefix, $databases; global $databases;
// Verify existing settings (if any). // Verify existing settings (if any).
if (!empty($databases) && install_verify_pdo()) { if (!empty($databases) && install_verify_pdo()) {
...@@ -834,7 +834,7 @@ function install_verify_pdo() { ...@@ -834,7 +834,7 @@ function install_verify_pdo() {
* The form API definition for the database configuration form. * The form API definition for the database configuration form.
*/ */
function install_settings_form($form, &$form_state, &$install_state) { function install_settings_form($form, &$form_state, &$install_state) {
global $databases, $db_prefix; global $databases;
$profile = $install_state['parameters']['profile']; $profile = $install_state['parameters']['profile'];
$install_locale = $install_state['parameters']['locale']; $install_locale = $install_state['parameters']['locale'];
...@@ -945,6 +945,10 @@ function install_settings_form($form, &$form_state, &$install_state) { ...@@ -945,6 +945,10 @@ function install_settings_form($form, &$form_state, &$install_state) {
* Form API validate for install_settings form. * Form API validate for install_settings form.
*/ */
function install_settings_form_validate($form, &$form_state) { function install_settings_form_validate($form, &$form_state) {
// TODO: remove when PIFR will be updated to use 'db_prefix' instead of
// 'prefix' in the database settings form.
$form_state['values']['prefix'] = $form_state['values']['db_prefix'];
form_set_value($form['_database'], $form_state['values'], $form_state); form_set_value($form['_database'], $form_state['values'], $form_state);
$errors = install_database_errors($form_state['values'], $form_state['values']['settings_file']); $errors = install_database_errors($form_state['values'], $form_state['values']['settings_file']);
foreach ($errors as $name => $message) { foreach ($errors as $name => $message) {
...@@ -959,8 +963,8 @@ function install_database_errors($database, $settings_file) { ...@@ -959,8 +963,8 @@ function install_database_errors($database, $settings_file) {
global $databases; global $databases;
$errors = array(); $errors = array();
// Verify the table prefix. // Verify the table prefix.
if (!empty($database['db_prefix']) && is_string($database['db_prefix']) && !preg_match('/^[A-Za-z0-9_.]+$/', $database['db_prefix'])) { if (!empty($database['prefix']) && is_string($database['prefix']) && !preg_match('/^[A-Za-z0-9_.]+$/', $database['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'])); $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'])) { if (!empty($database['port']) && !is_numeric($database['port'])) {
...@@ -1000,16 +1004,12 @@ function install_database_errors($database, $settings_file) { ...@@ -1000,16 +1004,12 @@ function install_database_errors($database, $settings_file) {
function install_settings_form_submit($form, &$form_state) { function install_settings_form_submit($form, &$form_state) {
global $install_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. // Update global settings array and save.
$settings['databases'] = array( $settings['databases'] = array(
'value' => array('default' => array('default' => $database)), 'value' => array('default' => array('default' => $database)),
'required' => TRUE, 'required' => TRUE,
); );
$settings['db_prefix'] = array(
'value' => $form_state['values']['db_prefix'],
'required' => TRUE,
);
$settings['drupal_hash_salt'] = array( $settings['drupal_hash_salt'] = array(
'value' => drupal_hash_base64(drupal_random_bytes(55)), 'value' => drupal_hash_base64(drupal_random_bytes(55)),
'required' => TRUE, 'required' => TRUE,
......
<?php <?php
// $Id$ // $Id$
/**
* Global variable that holds information about the tests being run.
*
* An array, with the following keys:
* - 'test_run_id': the ID of the test being run, in the form 'simpletest_%"
* - 'in_child_site': TRUE if the current request is a cURL request from
* the parent site.
*
* @var array
*/
global $drupal_test_info;
/** /**
* Base class for Drupal tests. * Base class for Drupal tests.
* *
...@@ -15,11 +27,11 @@ abstract class DrupalTestCase { ...@@ -15,11 +27,11 @@ abstract class DrupalTestCase {
protected $testId; protected $testId;
/** /**
* The original database prefix, before it was changed for testing purposes. * The database prefix of this test run.
* *
* @var string * @var string
*/ */
protected $originalPrefix = NULL;