From 267ebfb7b61bd6a440e50649ebf6e3c1d36c9f84 Mon Sep 17 00:00:00 2001
From: Dries Buytaert <dries@buytaert.net>
Date: Mon, 28 Jun 2010 19:57:34 +0000
Subject: [PATCH] - Patch #195416 by Damien Tournoud, David Strauss: table
 prefixes should be per database connection.

---
 includes/bootstrap.inc                      |  62 +++++--
 includes/common.inc                         |  17 +-
 includes/database/database.inc              | 152 ++++++++++++----
 includes/database/mysql/schema.inc          |   2 +-
 includes/database/schema.inc                |   4 +-
 includes/errors.inc                         |   3 +-
 includes/install.core.inc                   |  18 +-
 modules/simpletest/drupal_web_test_case.php | 192 +++++++++++---------
 modules/simpletest/simpletest.test          |   5 +-
 sites/default/default.settings.php          |  33 ++--
 10 files changed, 311 insertions(+), 177 deletions(-)

diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc
index f1faa9a4294b..622914d65250 100644
--- a/includes/bootstrap.inc
+++ b/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')) {
@@ -2149,14 +2149,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.
@@ -2165,6 +2157,42 @@ 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.
+    $test_info = &$GLOBALS['drupal_test_info'];
+    $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
   // won't be initialized until it is actually requested.
   require_once DRUPAL_ROOT . '/includes/database/database.inc';
@@ -2222,15 +2250,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);
 }
@@ -2239,15 +2267,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 a/includes/common.inc b/includes/common.inc
index 83da25e2bb49..ebc8c1c07abd 100644
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -766,8 +766,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.
@@ -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
   // 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]);
+  $test_info = &$GLOBALS['drupal_test_info'];
+  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";
@@ -4505,13 +4504,15 @@ 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) {
-    // 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.
+
+  $test_info = &$GLOBALS['drupal_test_info'];
+  if (!empty($test_info['in_child_site'])) {
+    // Running inside the simpletest child site, log fatal errors to test
+    // specific file 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 a/includes/database/database.inc b/includes/database/database.inc
index a4f00dc467bc..6b878d99f454 100644
--- a/includes/database/database.inc
+++ b/includes/database/database.inc
@@ -259,7 +259,26 @@ 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()) {
+    // 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.
     $driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
 
@@ -342,6 +361,25 @@ public function getConnectionOptions() {
     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.
    *
@@ -357,27 +395,12 @@ public function getConnectionOptions() {
    *   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 +410,12 @@ public function prefixTables($sql) {
    * 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 +1332,20 @@ final public static function parseConnectionInfo() {
         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 +1405,58 @@ final public static function getConnectionInfo($key = 'default') {
     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.
+   * @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') {
    *   The database target to open.
    */
   final protected static function openConnection($key, $target) {
-    global $db_prefix;
-
     if (empty(self::$databaseInfo)) {
       self::parseConnectionInfo();
     }
@@ -1415,13 +1496,6 @@ final protected static function openConnection($key, $target) {
       $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 a/includes/database/mysql/schema.inc b/includes/database/mysql/schema.inc
index 6a173d4fe515..7be5d0280750 100644
--- a/includes/database/mysql/schema.inc
+++ b/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 a/includes/database/schema.inc b/includes/database/schema.inc
index 91503002f40b..07212418fc97 100644
--- a/includes/database/schema.inc
+++ b/includes/database/schema.inc
@@ -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
    *   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 a/includes/errors.inc b/includes/errors.inc
index 1047a6665f5c..96bf8fdb2ad7 100644
--- a/includes/errors.inc
+++ b/includes/errors.inc
@@ -182,7 +182,8 @@ 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)) {
+  $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
     // as it uniquely identifies each PHP error.
     static $number = 0;
diff --git a/includes/install.core.inc b/includes/install.core.inc
index 9a81a9b5c047..e2e7752bc1c3 100644
--- a/includes/install.core.inc
+++ b/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,10 @@ 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 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);
   $errors = install_database_errors($form_state['values'], $form_state['values']['settings_file']);
   foreach ($errors as $name => $message) {
@@ -959,8 +963,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 +1004,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 a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php
index 3232c9df663d..d96db0cceb3e 100644
--- a/modules/simpletest/drupal_web_test_case.php
+++ b/modules/simpletest/drupal_web_test_case.php
@@ -1,6 +1,18 @@
 <?php
 // $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.
  *
@@ -15,11 +27,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 +102,6 @@ public function __construct($test_id = NULL) {
    *   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 +115,6 @@ protected function assert($status, $message = '', $group = 'Other', array $calle
       $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 +128,11 @@ protected function assert($status, $message = '', $group = 'Other', array $calle
     );
 
     // 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 +565,9 @@ function __construct($test_id = NULL) {
   }
 
   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 +576,21 @@ protected function setUp() {
     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 +603,16 @@ protected function setUp() {
   }
 
   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 +1122,28 @@ protected function drupalLogout() {
    *   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 +1156,10 @@ protected function setUp() {
     $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;
-
     // 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';
 
@@ -1154,6 +1177,11 @@ protected function setUp() {
     $conf = array();
     drupal_static_reset();
 
+    // Set the test information for use in other parts of Drupal.
+    $test_info = &$GLOBALS['drupal_test_info'];
+    $test_info['test_run_id'] = $this->databasePrefix;
+    $test_info['in_child_site'] = FALSE;
+
     include_once DRUPAL_ROOT . '/includes/install.inc';
     drupal_install_system();
 
@@ -1230,8 +1258,9 @@ protected function setUp() {
    * 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 +1286,11 @@ protected function refreshVariables() {
    * 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 +1298,52 @@ protected function tearDown() {
       $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));
+    // Delete temporary files directory.
+    file_unmanaged_delete_recursive($this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 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);
-      }
-
-      // 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 +1355,7 @@ protected function tearDown() {
    * 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 +1367,7 @@ protected function curlInitialize() {
         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 +1380,7 @@ protected function curlInitialize() {
     }
     // 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 a/modules/simpletest/simpletest.test b/modules/simpletest/simpletest.test
index b3522f962fe8..13cd5ae4227c 100644
--- a/modules/simpletest/simpletest.test
+++ b/modules/simpletest/simpletest.test
@@ -272,10 +272,11 @@ class SimpleTestFunctionalTest extends DrupalWebTestCase {
 
   /**
    * Check if the test is being run from inside a CURL request.
-   *
-   * @return The test is being run from inside a CURL request.
    */
   function inCURL() {
+    // We cannot rely on drupal_static('drupal_test_info') here, because
+    // 'in_child_site' would be FALSE for the parent site when we are
+    // executing the tests. Default to direct detection of the HTTP headers.
     return isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^simpletest\d+/", $_SERVER['HTTP_USER_AGENT']);
   }
 }
diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php
index 549898c6c45f..1b5fbd44d210 100644
--- a/sites/default/default.settings.php
+++ b/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.
-- 
GitLab