Index: modules/comment/comment.module =================================================================== RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v retrieving revision 1.691 diff -u -p -r1.691 comment.module --- modules/comment/comment.module 6 Feb 2009 16:25:08 -0000 1.691 +++ modules/comment/comment.module 7 Feb 2009 07:47:46 -0000 @@ -1297,7 +1297,7 @@ function comment_validate($edit) { if ($edit['name']) { $query = db_select('users', 'u'); $query->addField('u', 'uid', 'uid'); - $taken = $query->where('LOWER(name) = :name', array(':name' => $edit['name'])) + $taken = $query->where('username = :name', array(':name' => $edit['name'])) ->countQuery() ->execute() ->fetchField(); Index: modules/node/node.module =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.module,v retrieving revision 1.1023 diff -u -p -r1.1023 node.module --- modules/node/node.module 6 Feb 2009 16:25:08 -0000 1.1023 +++ modules/node/node.module 7 Feb 2009 07:47:46 -0000 @@ -3023,7 +3023,7 @@ function node_assign_owner_action_form($ // Use dropdown for fewer than 200 users; textbox for more than that. if (intval($count) < 200) { $options = array(); - $result = db_query("SELECT uid, name FROM {users} WHERE uid > 0 ORDER BY name"); + $result = db_query("SELECT uid, name FROM {users} WHERE uid > 0 ORDER BY username"); while ($data = db_fetch_object($result)) { $options[$data->name] = $data->name; } @@ -3050,7 +3050,7 @@ function node_assign_owner_action_form($ } function node_assign_owner_action_validate($form, $form_state) { - $count = db_result(db_query("SELECT COUNT(*) FROM {users} WHERE name = '%s'", $form_state['values']['owner_name'])); + $count = db_result(db_query("SELECT COUNT(*) FROM {users} WHERE username = LOWER(:owner_name)", array(':owner_name' => $form_state['values']['owner_name']))); if (intval($count) != 1) { form_set_error('owner_name', t('Please enter a valid username.')); } @@ -3058,7 +3058,7 @@ function node_assign_owner_action_valida function node_assign_owner_action_submit($form, $form_state) { // Username can change, so we need to store the ID, not the username. - $uid = db_result(db_query("SELECT uid from {users} WHERE name = '%s'", $form_state['values']['owner_name'])); + $uid = db_result(db_query("SELECT uid from {users} WHERE username = LOWER(:owner_name)", array(':owner_name:' => $form_state['values']['owner_name']))); return array('owner_uid' => $uid); } Index: modules/system/system.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.admin.inc,v retrieving revision 1.124 diff -u -p -r1.124 system.admin.inc --- modules/system/system.admin.inc 3 Feb 2009 18:55:31 -0000 1.124 +++ modules/system/system.admin.inc 7 Feb 2009 07:47:46 -0000 @@ -1784,7 +1784,7 @@ function system_status($check = FALSE) { } // MySQL import might have set the uid of the anonymous user to autoincrement // value. Let's try fixing it. See http://drupal.org/node/204411 - db_query("UPDATE {users} SET uid = uid - uid WHERE name = '' AND pass = '' AND status = 0"); + db_query("UPDATE {users} SET uid = uid - uid WHERE username = '' AND pass = '' AND status = 0"); return theme('status_report', $requirements); } Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.306 diff -u -p -r1.306 system.install --- modules/system/system.install 3 Feb 2009 12:30:14 -0000 1.306 +++ modules/system/system.install 7 Feb 2009 07:47:46 -0000 @@ -342,17 +342,17 @@ function system_install() { // 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} (name, username, mail) VALUES(:empty, :empty, :empty)", array(':empty' => '')); // 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())); + db_query("INSERT INTO {users} (name, username, mail, created, data) VALUES(:placeholder, :placeholder, :placeholder, :time, :serial)", array(':placeholder' => 'placeholder-for-uid-1', ':time' => REQUEST_TIME, ':serial' => 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'", ''); + db_query("UPDATE {users} SET uid = uid - uid WHERE username = :empty", array(':empty' =>'')); // 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'); + db_query("UPDATE {users} SET uid = 1 WHERE username = :placeholder", array(':placeholder' => 'placeholder-for-uid-1')); // Built-in roles. db_query("INSERT INTO {role} (name) VALUES ('%s')", 'anonymous user'); Index: modules/user/user.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.admin.inc,v retrieving revision 1.37 diff -u -p -r1.37 user.admin.inc --- modules/user/user.admin.inc 3 Feb 2009 18:55:32 -0000 1.37 +++ modules/user/user.admin.inc 7 Feb 2009 07:47:46 -0000 @@ -136,7 +136,7 @@ function user_admin_account() { $header = array( array(), - array('data' => t('Username'), 'field' => 'u.name'), + array('data' => t('Username'), 'field' => 'u.username'), array('data' => t('Status'), 'field' => 'u.status'), t('Roles'), array('data' => t('Member for'), 'field' => 'u.created', 'sort' => 'desc'), Index: modules/user/user.install =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.install,v retrieving revision 1.17 diff -u -p -r1.17 user.install --- modules/user/user.install 20 Jan 2009 03:10:00 -0000 1.17 +++ modules/user/user.install 7 Feb 2009 07:47:46 -0000 @@ -101,7 +101,14 @@ function user_schema() { 'length' => 60, 'not null' => TRUE, 'default' => '', - 'description' => 'Unique user name.', + 'description' => 'User\'s name with case preserved. Uniqueness is enforced on the derivative {users}.username field.', + ), + 'username' => array( + 'type' => 'varchar', + 'length' => 60, + 'not null' => TRUE, + 'default' => '', + 'description' => 'Unique, lowercase version of {users}.name for query performance. This column should be used for WHERE, ORDER BY, etc. in queries instead of {users}.name.', ), 'pass' => array( 'type' => 'varchar', @@ -196,7 +203,7 @@ function user_schema() { 'mail' => array('mail'), ), 'unique keys' => array( - 'name' => array('name'), + 'username' => array('username'), ), 'primary key' => array('uid'), ); @@ -464,6 +471,18 @@ function user_update_7004(&$sandbox) { } /** + * Add username column and populate it with lowercased versions of existing names. + */ +function user_update_7005() { + $ret = array(); + db_add_field($ret, 'users', 'username', array('type' => 'varchar', 'length' => 60, 'not null' => TRUE, 'default' => '')); + $ret[] = update_sql("UPDATE {users} SET username = LOWER(name)"); + db_add_unique_key($ret, 'users', 'username', array('username')); + db_drop_unique_key($ret, 'users', 'name'); + return $ret; +} + +/** * @} End of "defgroup user-updates-6.x-to-7.x" * The next series of updates should start at 8000. */ Index: modules/user/user.module =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.module,v retrieving revision 1.962 diff -u -p -r1.962 user.module --- modules/user/user.module 3 Feb 2009 18:55:32 -0000 1.962 +++ modules/user/user.module 7 Feb 2009 07:47:46 -0000 @@ -191,6 +191,10 @@ function user_load($array = array()) { $query[] = "pass = '%s'"; $params[] = $value; } + elseif ($key == 'username') { + $query[] = "username = LOWER('%s')"; + $params[] = $value; + } else { $query[]= "LOWER($key) = LOWER('%s')"; $params[] = $value; @@ -283,6 +287,11 @@ function user_save($account, $edit = arr $edit = (array) $edit; + // If username isn't set, populate it with name. + if (!empty($edit['name']) && empty($edit['username'])) { + $edit['username'] = $edit['name']; + } + if (is_object($account) && $account->uid) { user_module_invoke('update', $edit, $account, $category); $data = unserialize(db_result(db_query('SELECT data FROM {users} WHERE uid = %d', $account->uid))); @@ -337,6 +346,15 @@ function user_save($account, $edit = arr // return FALSE; } + // Ensure $user->username is lowercase if it's the same as $user->name. + // We use the database to do the lowercasing to ensure correct searches + // in case of any discrepencies in character handling between the + // database and PHP. + // LOWER() is also slightly faster than mb_strtolower() for this task. + if (!empty($edit['name']) && $edit['username'] == $edit['name']) { + db_query("UPDATE {users} SET username = LOWER(username) WHERE uid = :uid", array(':uid' => $account->uid)); + } + // If the picture changed or was unset, remove the old one. This step needs // to occur after updating the {users} record so that user_file_references() // doesn't report it in use and block the deletion. @@ -403,6 +421,14 @@ function user_save($account, $edit = arr return FALSE; } + // Ensure $user->username is lowercase if it's the same as $user->name. + // We use the database to do the lowercasing to ensure that there are + // no discrepencies in collation handling between the database and PHP. + // LOWER() is also slightly faster than mb_strtolower() for this task. + if ($edit['username'] == $edit['name']) { + db_query("UPDATE {users} SET username = LOWER(username) WHERE uid = :uid", array(':uid' => $edit['uid'])); + } + // Build the initial user object. $user = user_load(array('uid' => $edit['uid'])); @@ -643,8 +669,8 @@ function user_access($string, $account = * * @return boolean TRUE for blocked users, FALSE for active. */ -function user_is_blocked($name) { - $deny = db_fetch_object(db_query("SELECT name FROM {users} WHERE status = 0 AND name = LOWER('%s')", $name)); +function user_is_blocked($username) { + $deny = db_fetch_object(db_query("SELECT name FROM {users} WHERE status = 0 AND username = LOWER(:username)", array(':username' => $username))); return $deny; } @@ -732,13 +758,13 @@ function user_search($op = 'search', $ke $keys = preg_replace('!\*+!', '%', $keys); if (user_access('administer users')) { // Administrators can also search in the otherwise private email field. - $result = pager_query("SELECT name, uid, mail FROM {users} WHERE LOWER(name) LIKE LOWER('%%%s%%') OR LOWER(mail) LIKE LOWER('%%%s%%')", 15, 0, NULL, $keys, $keys); + $result = pager_query("SELECT name, uid, mail FROM {users} WHERE username LIKE LOWER('%%%s%%') OR LOWER(mail) LIKE LOWER('%%%s%%')", 15, 0, NULL, $keys, $keys); while ($account = db_fetch_object($result)) { $find[] = array('title' => $account->name . ' (' . $account->mail . ')', 'link' => url('user/' . $account->uid, array('absolute' => TRUE))); } } else { - $result = pager_query("SELECT name, uid FROM {users} WHERE LOWER(name) LIKE LOWER('%%%s%%')", 15, 0, NULL, $keys); + $result = pager_query("SELECT name, uid FROM {users} WHERE username LIKE LOWER('%%%s%%')", 15, 0, NULL, $keys); while ($account = db_fetch_object($result)) { $find[] = array('title' => $account->name, 'link' => url('user/' . $account->uid, array('absolute' => TRUE))); } @@ -807,7 +833,7 @@ function user_user_validate(&$edit, &$ac if ($error = user_validate_name($edit['name'])) { form_set_error('name', $error); } - elseif (db_result(db_query("SELECT COUNT(*) FROM {users} WHERE uid != %d AND LOWER(name) = LOWER('%s')", $uid, $edit['name'])) > 0) { + elseif (db_result(db_query("SELECT COUNT(*) FROM {users} WHERE uid != :uid AND username = LOWER(:name)", array(':uid' => $uid, ':name' => $edit['name']))) > 0) { form_set_error('name', t('The name %name is already taken.', array('%name' => $edit['name']))); } } @@ -1543,7 +1569,7 @@ function user_authenticate($form_values $password = trim($form_values['pass']); // Name and pass keys are required. if (!empty($form_values['name']) && !empty($password)) { - $account = db_fetch_object(db_query("SELECT * FROM {users} WHERE name = '%s' AND status = 1", $form_values['name'])); + $account = db_fetch_object(db_query("SELECT * FROM {users} WHERE username = LOWER(:name) AND status = 1", array(':name' => $form_values['name']))); if ($account) { // Allow alternate password hashing schemes. require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc'); Index: modules/user/user.pages.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.pages.inc,v retrieving revision 1.28 diff -u -p -r1.28 user.pages.inc --- modules/user/user.pages.inc 3 Feb 2009 17:30:13 -0000 1.28 +++ modules/user/user.pages.inc 7 Feb 2009 07:47:46 -0000 @@ -12,7 +12,7 @@ function user_autocomplete($string = '') { $matches = array(); if ($string) { - $result = db_query_range("SELECT name FROM {users} WHERE LOWER(name) LIKE LOWER(:name)", array(':name' => $string .'%'), 0, 10); + $result = db_query_range("SELECT name FROM {users} WHERE username LIKE LOWER(:name)", array(':name' => $string .'%'), 0, 10); while ($user = db_fetch_object($result)) { $matches[$user->name] = check_plain($user->name); } Index: modules/user/user.test =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.test,v retrieving revision 1.27 diff -u -p -r1.27 user.test --- modules/user/user.test 31 Jan 2009 16:50:57 -0000 1.27 +++ modules/user/user.test 7 Feb 2009 07:47:46 -0000 @@ -27,6 +27,8 @@ class UserRegistrationTestCase extends D $edit = array(); $edit['name'] = $name = $this->randomName(); $edit['mail'] = $mail = $edit['name'] . '@example.com'; + $this->drupalGet('user/register'); + $this->assertResponse('200'); $this->drupalPost('user/register', $edit, t('Create new account')); $this->assertText(t('Your password and further instructions have been sent to your e-mail address.'), t('User registered successfully.')); @@ -36,7 +38,10 @@ class UserRegistrationTestCase extends D $this->assertTrue($user->uid > 0, t('User has valid user id.')); // Check user fields. - $this->assertEqual($user->name, $name, t('Username matches.')); + $this->assertEqual($user->name, $name, t('User name matches.')); + $name = db_result(db_query("SELECT LOWER(name) FROM {users} WHERE uid = %d", $user->uid)); + $username = db_result(db_query("SELECT username FROM {users} WHERE uid = :uid", array(':uid' => $user->uid))); + $this->assertEqual($name, $username, t('Username matches.')); $this->assertEqual($user->mail, $mail, t('E-mail address matches.')); $this->assertEqual($user->theme, '', t('Correct theme field.')); $this->assertEqual($user->signature, '', t('Correct signature field.')); @@ -47,6 +52,10 @@ class UserRegistrationTestCase extends D $this->assertEqual($user->picture, '', t('Correct picture field.')); $this->assertEqual($user->init, $mail, t('Correct init field.')); + // Attempt to register with a duplicate username. + $this->drupalPost('user/register', $edit, t('Create new account')); + $this->assertRaw(t('The name %name is already taken.', array('%name' => $edit['name'])), t('Duplicate username registration prevented.')); + // Attempt to login with incorrect password. $edit = array(); $edit['name'] = $name; @@ -749,6 +758,68 @@ class UserPermissionsTestCase extends Dr } +class UserSearchTestCase extends DrupalWebTestCase { + /** + * Implementation of getInfo(). + */ + function getInfo() { + return array( + 'name' => t('User search'), + 'description' => t('Tests user integration with the search module.'), + 'group' => t('User') + ); + } + + /** + * Implementation of setUp(). + */ + function setUp() { + parent::setUp('search'); + // Create users. + $this->normal_user = $this->drupalCreateUser(array('access user profiles', 'search content')); + $this->admin_user = $this->drupalCreateUser(array('administer users', 'search content')); + } + + function testUserSearch() { + // Register a new user for searching. + $edit = array(); + $edit['name'] = $name = $this->randomName(); + $edit['mail'] = $mail = $edit['name'] . '@example.com'; + $this->drupalPost('user/register', $edit, t('Create new account')); + $this->assertText(t('Your password and further instructions have been sent to your e-mail address.'), t('User registered successfully.')); + $this->drupalLogin($this->admin_user); + $this->drupalGet('search/user'); + $this->assertResponse(200, t('User search page exists.')); + + // Search for a username with 'administer users' permission. + $edit = array(); + $edit['keys'] = $name; + $this->drupalPost(NULL, $edit, t('Search')); + $this->assertRaw($name, t('Username search successful.')); + // Email address search. + $this->drupalGet('search/user'); + $edit = array(); + $edit['keys'] = $mail; + $this->drupalPost(NULL, $edit, t('Search')); + $this->assertRaw($name, t('Email search successful.')); + $this->drupalLogout(); + + // Search for a username with 'access user profiles' permission. + $this->drupalLogin($this->normal_user); + $this->drupalGet('search/user'); + $this->assertResponse(200, t('User search page exists.')); + $edit = array(); + $edit['keys'] = $name; + $this->drupalPost(NULL, $edit, t('Search')); + $this->assertRaw($name, t('Username search successful')); + // E-mail address search. + $edit = array(); + $edit['keys'] = $mail; + $this->drupalPost(NULL, $edit, t('Search')); + $this->assertRaw($name, t('Email search does not return results without appropriate permission')); + } +} + class UserAdminTestCase extends DrupalWebTestCase { function getInfo() { return array(