=== 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;