=== modified file 'includes/actions.inc' --- includes/actions.inc 2009-07-20 18:51:31 +0000 +++ includes/actions.inc 2009-07-21 14:07:51 +0000 @@ -324,7 +324,7 @@ function actions_save($function, $type, // aid is the callback for singleton actions so we need to keep a separate // table for numeric aids. if (!$aid) { - $aid = db_insert('actions_aid')->useDefaults(array('aid'))->execute(); + $aid = db_next_id('actions'); } db_merge('actions') === modified file 'includes/bootstrap.inc' --- includes/bootstrap.inc 2009-07-19 05:26:11 +0000 +++ includes/bootstrap.inc 2009-07-21 05:34:02 +0000 @@ -1343,6 +1343,13 @@ function _drupal_bootstrap($phase) { break; case DRUPAL_BOOTSTRAP_DATABASE: + // The user agent header is used to pass a database prefix in the request when + // running tests. However, for security reasons, it is imperative that we + // validate we ourselves made the request. + 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; + } // 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'; @@ -1429,6 +1436,45 @@ function _drupal_bootstrap($phase) { } /** + * Validate the HMAC and timestamp of a user agent header from simpletest. + */ +function drupal_valid_test_ua($user_agent) { + global $databases; + + list($prefix, $time, $salt, $hmac) = explode(';', $user_agent); + $check_string = $prefix . ';' . $time . ';' . $salt; + // We use the database credentials from settings.php to make the HMAC key, since + // the database is not yet initialized and we can't access any Drupal variables. + // The file properties add more entropy not easily accessible to others. + $filepath = DRUPAL_ROOT . '/includes/bootstrap.inc'; + $key = sha1(serialize($databases) . filectime($filepath) . fileinode($filepath), TRUE); + $time_diff = REQUEST_TIME - $time; + // Since we are making a local request, a 2 second time window is allowed, + // and the HMAC must match. + return (($time_diff >= 0) && ($time_diff < 3) && ($hmac == base64_encode(hash_hmac('sha1', $check_string, $key, TRUE)))); +} + +/** + * Generate a user agent string with a HMAC and timestamp for simpletest. + */ +function drupal_generate_test_ua($prefix) { + global $databases; + static $key; + + if (!isset($key)) { + // We use the database credentials to make the HMAC key, since we + // check the HMAC before the database is initialized. filectime() + // and fileinode() are not easily determined from remote. + $filepath = DRUPAL_ROOT . '/includes/bootstrap.inc'; + $key = sha1(serialize($databases) . filectime($filepath) . fileinode($filepath), TRUE); + } + // Generate a moderately secure HMAC based on the database credentials. + $salt = uniqid('', TRUE); + $check_string = $prefix . ';' . time() . ';' . $salt; + return $check_string . ';' . base64_encode(hash_hmac('sha1', $check_string, $key, TRUE)); +} + +/** * Enables use of the theme system without requiring database access. * * Loads and initializes the theme system for site installs, updates and when === modified file 'includes/common.inc' --- includes/common.inc 2009-07-20 18:51:31 +0000 +++ includes/common.inc 2009-07-21 05:34:02 +0000 @@ -552,8 +552,8 @@ function drupal_http_request($url, 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'] = $matches[0]; + if (is_string($db_prefix) && preg_match("/simpletest\d+/", $db_prefix, $matches)) { + $options['headers']['User-Agent'] = drupal_generate_test_ua($matches[0]); } $request = $options['method'] . ' ' . $path . " HTTP/1.0\r\n"; @@ -809,7 +809,7 @@ function _drupal_log_error($error, $fata // 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 (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^simpletest\d+;/", $_SERVER['HTTP_USER_AGENT']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) { // $number does not use drupal_static as it should not be reset // as it uniquely identifies each PHP error. static $number = 0; === modified file 'includes/database/database.inc' --- includes/database/database.inc 2009-07-21 01:56:35 +0000 +++ includes/database/database.inc 2009-07-21 14:21:16 +0000 @@ -1046,6 +1046,46 @@ abstract class DatabaseConnection extend public function commit() { throw new ExplicitTransactionsNotSupportedException(); } + + /** + * Retrieves an unique id from a given sequence. + * + * Use this function if for some reason you can't use a serial field. + * + * @param $name + * The name of the sequence. + * @param $existing_id + * After a database import, it might be that the sequences table is behind, + * so by passing in the maximum existing id, it can be assured that we + * never issue the same id. + * @return + * An integer number larger than any number returned by earlier calls with + * the same $name and also larger than the $existing_id if one was passed + * in. + */ + public function nextId($name, $existing_id = 0) { + $transaction = $this->startTransaction(); + // We can safely use literal queries here instead of the slower query + // builder because if a given database breaks here then it can simply + // override nextId. However, this is unlikely as we deal with short + // strings and integers and no known databases require special handling + // for those simple cases. + // If another transaction wants to write the same row, it will wait until + // this transaction commits. + $stmt = $this->query('UPDATE {sequences} SET value = GREATEST(value, :existing_id) + 1 WHERE name = :name', array( + ':existing_id' => $existing_id, + ':name' => $name, + )); + if (!$stmt->rowCount()) { + $this->query('INSERT INTO {sequences} (name, value) VALUES (:name, :existing_id + 1)', array( + ':existing_id' => $existing_id, + ':name' => $name, + )); + } + // The transaction gets committed when the transaction object gets + // destructed because it gets out of scope. + return $new_value; + } } /** @@ -1347,9 +1387,10 @@ abstract class Database { } // We need to pass around the simpletest database prefix in the request - // and we put that in the user_agent header. - if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^simpletest\d+$/", $_SERVER['HTTP_USER_AGENT'])) { - $db_prefix .= $_SERVER['HTTP_USER_AGENT']; + // 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 .= $matches[1]; } return $new_connection; } @@ -2071,6 +2112,26 @@ function db_driver() { } /** + * Retrieves an unique id from a given sequence. + * + * Use this function if for some reason you can't use a serial field, + * normally a serial field with db_last_insert_id is preferred. + * + * @param $name + * The name of the sequence. + * @param $existing_id + * After a database import, it might be that the sequences table is behind, + * so by passing in a minimum id, it can be assured that we never issue the + * same id. + * @return + * An integer number larger than any number returned before for this + * sequence. + */ +function db_next_id($name, $existing_id = 0) { + return Database::getConnection()->nextId($name, $existing_id); +} + +/** * @} End of "defgroup database". */ === modified file 'includes/database/mysql/database.inc' --- includes/database/mysql/database.inc 2009-03-25 18:43:01 +0000 +++ includes/database/mysql/database.inc 2009-07-21 14:07:51 +0000 @@ -77,6 +77,28 @@ class DatabaseConnection_mysql extends D // (?transactionSupport) { + return parent::nextId($name, $existing_id); + } + try { + // Try an INSERT first. + $this->query('INSERT INTO {sequences} (name, value) VALUES (:name, :value)', array(':name' => $name, ':value' => $existing_id)); + } + catch (PDOException $e) { + // The most likely case for an exception is that the sequence already + // exists and that's exactly what we wanted to assure. + } + // LAST_INSERT_ID(x) stores x to be retrieved with LAST_INSERT_ID(). + // This way there is no window for race condition between the UPDATE and + // the SELECT. + $this->query('UPDATE {sequences} SET value = LAST_INSERT_ID(GREATEST(:existing_id, value) + 1) WHERE name = :name', array( + ':existing_id' => $existing_id, + ':name' => $name, + )); + return $this->query('SELECT LAST_INSERT_ID()')->fetchField(); + } } === modified file 'install.php' --- install.php 2009-07-19 04:48:09 +0000 +++ install.php 2009-07-21 05:34:02 +0000 @@ -28,7 +28,7 @@ function install_main() { // The user agent header is used to pass a database prefix in the request when // running tests. However, for security reasons, it is imperative that no // installation be permitted using such a prefix. - if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^simpletest\d+$/", $_SERVER['HTTP_USER_AGENT'])) { + if (isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], "simpletest") !== FALSE) { header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden'); exit; } === modified file 'modules/simpletest/drupal_web_test_case.php' --- modules/simpletest/drupal_web_test_case.php 2009-07-15 02:08:40 +0000 +++ modules/simpletest/drupal_web_test_case.php 2009-07-21 05:34:02 +0000 @@ -1194,6 +1194,7 @@ class DrupalWebTestCase extends DrupalTe */ protected function curlInitialize() { global $base_url, $db_prefix; + if (!isset($this->curlHandle)) { $this->curlHandle = curl_init(); $curl_options = $this->additionalCurlOptions + array( @@ -1206,9 +1207,6 @@ class DrupalWebTestCase extends DrupalTe CURLOPT_SSL_VERIFYHOST => FALSE, // Required to make the tests run on https. CURLOPT_HEADERFUNCTION => array(&$this, 'curlHeaderCallback'), ); - if (preg_match('/simpletest\d+/', $db_prefix, $matches)) { - $curl_options[CURLOPT_USERAGENT] = $matches[0]; - } if (isset($this->httpauth_credentials)) { $curl_options[CURLOPT_USERPWD] = $this->httpauth_credentials; } @@ -1217,6 +1215,11 @@ class DrupalWebTestCase extends DrupalTe // By default, the child session name should be the same as the parent. $this->session_name = session_name(); } + // 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)) { + curl_setopt($this->curlHandle, CURLOPT_USERAGENT, drupal_generate_test_ua($matches[0])); + } } /** === modified file 'modules/simpletest/simpletest.install' --- modules/simpletest/simpletest.install 2009-07-09 10:21:14 +0000 +++ modules/simpletest/simpletest.install 2009-07-21 05:37:00 +0000 @@ -130,6 +130,7 @@ function simpletest_requirements($phase) $t = get_t(); $has_curl = function_exists('curl_init'); + $has_hash = function_exists('hash_hmac'); $has_domdocument = class_exists('DOMDocument'); $requirements['curl'] = array( @@ -140,6 +141,14 @@ function simpletest_requirements($phase) $requirements['curl']['severity'] = REQUIREMENT_ERROR; $requirements['curl']['description'] = $t('Simpletest could not be installed because the PHP cURL library is not available.', array('@curl_url' => 'http://php.net/manual/en/curl.setup.php')); } + $requirements['hash'] = array( + 'title' => $t('hash'), + 'value' => $has_hash ? $t('Enabled') : $t('Not found'), + ); + if (!$has_hash) { + $requirements['hash']['severity'] = REQUIREMENT_ERROR; + $requirements['hash']['description'] = $t('Simpletest could not be installed because the PHP hash extension is disabled.', array('@hash_url' => 'http://php.net/manual/en/book.hash.php')); + } $requirements['php_domdocument'] = array( 'title' => $t('PHP DOMDocument class'), === modified file 'modules/simpletest/tests/database_test.test' --- modules/simpletest/tests/database_test.test 2009-07-21 01:56:35 +0000 +++ modules/simpletest/tests/database_test.test 2009-07-21 14:07:51 +0000 @@ -2762,3 +2762,30 @@ class DatabaseTransactionTestCase extend } } } + +/** + * Check the sequences API. + */ +class DatabaseNextIdCase extends DrupalWebTestCase { + function getInfo() { + return array( + 'name' => t('Sequences API'), + 'description' => t('Test the secondary sequences API.'), + 'group' => t('Database'), + ); + } + + /** + * Test that the sequences API work. + */ + function testDbNextId() { + $first = db_next_id('test'); + $second = db_next_id('test'); + // We can test for exact increase in here because we know there is no + // other process operating on these tables -- normally we could only + // expect $second > $first. + $this->assertEqual($first + 1, $second, t('The second call from a sequence provides a number increased by one.')); + $result = db_next_id('test', 1000); + $this->assertEqual($result, 1001, t('Sequence provides a larger number than the existing ID.')); + } +} === modified file 'modules/system/system.install' --- modules/system/system.install 2009-07-20 18:51:31 +0000 +++ modules/system/system.install 2009-07-21 14:07:51 +0000 @@ -355,21 +355,10 @@ function system_install() { // Load system theme data appropriately. system_get_theme_data(); - // Inserting uid 0 here confuses MySQL -- the next user might be created as - // uid 2 which is not what we want. So we insert the first user here, the - // anonymous user. uid is 1 here for now, but very soon it will be changed - // to 0. - db_query("INSERT INTO {users} (name, mail) VALUES('%s', '%s')", '', ''); + db_query("INSERT INTO {users} (uid, name, mail) VALUES (0, '%s', '%s')", '', ''); // We need some placeholders here as name and mail are uniques and data is - // presumed to be a serialized array. Install will change uid 1 immediately - // anyways. So we insert the superuser here, the uid is 2 here for now, but - // very soon it will be changed to 1. - db_query("INSERT INTO {users} (name, mail, created, status, data) VALUES('%s', '%s', %d, %d, '%s')", 'placeholder-for-uid-1', 'placeholder-for-uid-1', REQUEST_TIME, 1, serialize(array())); - // This sets the above two users uid 0 (anonymous). We avoid an explicit 0 - // otherwise MySQL might insert the next auto_increment value. - db_query("UPDATE {users} SET uid = uid - uid WHERE name = '%s'", ''); - // This sets uid 1 (superuser). We skip uid 2 but that's not a big problem. - db_query("UPDATE {users} SET uid = 1 WHERE name = '%s'", 'placeholder-for-uid-1'); + // presumed to be a serialized array. + db_query("INSERT INTO {users} (uid, name, mail, created, status, data) VALUES (1, '%s', '%s', %d, %d, '%s')", 'placeholder-for-uid-1', 'placeholder-for-uid-1', REQUEST_TIME, 1, serialize(array())); // Built-in roles. $rid_anonymous = db_insert('role') @@ -585,19 +574,6 @@ function system_schema() { 'primary key' => array('aid'), ); - $schema['actions_aid'] = array( - 'description' => 'Stores action IDs for non-default actions.', - 'fields' => array( - 'aid' => array( - 'description' => 'Primary Key: Unique actions ID.', - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - ), - ), - 'primary key' => array('aid'), - ); - $schema['batch'] = array( 'description' => 'Stores details about batches (processes that run in multiple HTTP requests).', 'fields' => array( @@ -1207,18 +1183,6 @@ function system_schema() { ), ); - $schema['queue_consumer_id'] = array( - 'description' => 'Stores queue consumer IDs, used to auto-increment the consumer ID so that a unique consumer ID is used.', - 'fields' => array( - 'consumer_id' => array( - 'type' => 'serial', - 'not null' => TRUE, - 'description' => 'Primary Key: Unique consumer ID used to make sure only one consumer gets one item.', - ), - ), - 'primary key' => array('consumer_id'), - ); - $schema['registry'] = array( 'description' => "Each record is a function, class, or interface name and the file it is in.", 'fields' => array( @@ -1288,6 +1252,26 @@ function system_schema() { 'primary key' => array('filename'), ); + $schema['sequences'] = array( + 'description' => 'Stores IDs.', + 'fields' => array( + 'value' => array( + 'description' => 'The .', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'name' => array( + 'description' => "Primary key: A session ID. The value is generated by PHP's Session API.", + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + 'default' => '', + ), + ), + 'primary key' => array('name'), + ); + $schema['sessions'] = array( 'description' => "Drupal's session handlers read and write into the sessions table. Each record represents a user session, either anonymous or authenticated.", 'fields' => array( @@ -2241,6 +2225,23 @@ function system_update_7029() { } /** + * Reuse the actions_aid table as sequences. + */ +function system_update_7030() { + $ret = array(); + db_drop_primary_key($ret, 'actions_aid'); + db_change_field($ret, 'actions_aid', 'aid', 'sid', array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE), array('primary key' => array('sid'))); + db_rename_table($ret, 'actions_aid', 'sequences'); + $max_sid = db_query('SELECT MAX(sid) FROM {sequences}')->fetchField(); + db_delete('sequences')->condition('sid', $max_sid, '<'); + $max_uid = db_query('SELECT MAX(uid) FROM {users}')->fetchField(); + if ($max_uid > $max_sid) { + db_update('sequences')->fields(array('sid' => $max_uid))->execute(); + } + return $ret; +} + +/** * @} End of "defgroup updates-6.x-to-7.x" * The next series of updates should start at 8000. */ === modified file 'modules/system/system.queue.inc' --- modules/system/system.queue.inc 2009-05-24 17:39:30 +0000 +++ modules/system/system.queue.inc 2009-07-21 14:07:51 +0000 @@ -195,9 +195,7 @@ class SystemQueue implements DrupalQueue public function claimItem($lease_time = 30) { if (!isset($this->consumerId)) { - $this->consumerId = db_insert('queue_consumer_id') - ->useDefaults(array('consumer_id')) - ->execute(); + $this->consumerId = db_next_id('queue_consumer_id'); } // Claim an item by updating its consumer_id and expire fields. If claim // is not successful another thread may have claimed the item in the === modified file 'modules/user/user.install' --- modules/user/user.install 2009-06-01 22:07:08 +0000 +++ modules/user/user.install 2009-07-21 14:07:51 +0000 @@ -102,10 +102,11 @@ function user_schema() { 'description' => 'Stores user data.', 'fields' => array( 'uid' => array( - 'type' => 'serial', + 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'description' => 'Primary Key: Unique user ID.', + 'default' => 0, ), 'name' => array( 'type' => 'varchar', === modified file 'modules/user/user.module' --- modules/user/user.module 2009-07-15 17:40:17 +0000 +++ modules/user/user.module 2009-07-21 14:07:51 +0000 @@ -376,6 +376,7 @@ function user_save($account, $edit = arr if (!isset($account->is_new)) { $account->is_new = empty($account->uid); } + if (is_object($account) && !$account->is_new) { user_module_invoke('update', $edit, $account, $category); $data = unserialize(db_query('SELECT data FROM {users} WHERE uid = :uid', array(':uid' => $account->uid))->fetchField()); @@ -486,6 +487,11 @@ function user_save($account, $edit = arr user_module_invoke('after_update', $edit, $user, $category); } else { + // Allow 'uid' to be set by the caller. There is no danger of writing an + // existing user as drupal_write_record will do an INSERT. + if (empty($edit['uid'])) { + $edit['uid'] = db_next_id('users', db_query('SELECT MAX(uid) FROM {users}')->fetchField()); + } // Allow 'created' to be set by the caller. if (!isset($edit['created'])) { $edit['created'] = REQUEST_TIME;