? cron-timing-131536-34.patch ? fieldset_html_in_title.patch ? node_delete_link.patch ? node_welcome_page-126221-17.patch ? node_welcome_page-126221-18.patch ? pluggable-pass-smtp-259103-27.patch ? pluggable-pass-smtp-259103-27a.patch ? sites/default/files ? sites/default/settings.php Index: includes/mail.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/mail.inc,v retrieving revision 1.16 diff -u -p -r1.16 mail.inc --- includes/mail.inc 6 Oct 2008 11:00:01 -0000 1.16 +++ includes/mail.inc 30 Oct 2008 01:32:31 -0000 @@ -9,7 +9,7 @@ * appropriate places in the template. Processed e-mail templates are * requested from hook_mail() from the module sending the e-mail. Any module * can modify the composed e-mail message array using hook_mail_alter(). - * Finally drupal_mail_send() sends the e-mail, which can be reused + * Finally drupal_smtp()->send() sends the e-mail, which can be reused * if the exact same composed e-mail is to be sent to multiple recipients. * * Finding out what language to send the e-mail with needs some consideration. @@ -72,7 +72,7 @@ * @param $from * Sets From, Reply-To, Return-Path and Error-To to this value, if given. * @param $send - * Send the message directly, without calling drupal_mail_send() manually. + * Send the message directly, without a manual call to drupal_smtp()->send(). * @return * The $message array structure containing all details of the * message. If already sent ($send = TRUE), then the 'result' element @@ -127,7 +127,7 @@ function drupal_mail($module, $key, $to, // Optionally send e-mail. if ($send) { - $message['result'] = drupal_mail_send($message); + $message['result'] = drupal_smtp($module, $key)->send($message); // Log errors if (!$message['result']) { @@ -139,11 +139,104 @@ function drupal_mail($module, $key, $to, return $message; } +class DrupalMailSend implements DrupalSmtpInterface { + /** + * Send an e-mail message, using Drupal variables and default settings. + * More information in the + * PHP function reference for mail(). See drupal_mail() for information on + * how $message is composed. + * + * @param $message + * Message array with at least the following elements: + * - id + * A unique identifier of the e-mail type. Examples: 'contact_user_copy', + * 'user_password_reset'. + * - to + * The mail address or addresses where the message will be sent to. The + * formatting of this string must comply with RFC 2822. Some examples are: + * user@example.com + * user@example.com, anotheruser@example.com + * User + * User , Another User + * - subject + * Subject of the e-mail to be sent. This must not contain any newline + * characters, or the mail may not be sent properly. + * - body + * Message to be sent. Accepts both CRLF and LF line-endings. + * E-mail bodies must be wrapped. You can use drupal_wrap_mail() for + * smart plain text wrapping. + * - headers + * Associative array containing all mail headers. + * @return + * Returns TRUE if the mail was successfully accepted for delivery, + * FALSE otherwise. + */ + public function send($message) { + $mimeheaders = array(); + foreach ($message['headers'] as $name => $value) { + $mimeheaders[] = $name . ': ' . mime_header_encode($value); + } + return mail( + $message['to'], + mime_header_encode($message['subject']), + // Note: e-mail uses CRLF for line-endings, but PHP's API requires LF. + // They will appear correctly in the actual e-mail that is sent. + str_replace("\r", '', $message['body']), + // For headers, PHP's API suggests that we use CRLF normally, + // but some MTAs incorrecly replace LF with CRLF. See #234403. + join("\n", $mimeheaders) + ); + } +} + /** - * Send an e-mail message, using Drupal variables and default settings. - * More information in the - * PHP function reference for mail(). See drupal_mail() for information on - * how $message is composed. + * Returns an object that implements DrupalSmtpInterface.. + * + * Allows for one or more custom mail backends to send mail messages + * composed using drupal_mail(). + * + * @param $module + * The module name which was used by drupal_mail() to invoke hook_mail(). + * @param $key + * A key to identify the e-mail sent. The final e-mail id for the e-mail + * alter hook in drupal_mail() would have been {$module}_{$key}. + */ +function drupal_smtp($module, $key) { + static $instance = array(); + + $id = $module . '_' . $key; + $config = variable_get('smtp_system', array('default' => 'DrupalMailSend')); + + // Look for overrides for the default class, starting from the most specific + // id, and falling back to the module name. + if (isset($config[$id])) { + $class = $config[$id]; + } + elseif (isset($config[$module])) { + $class = $config[$module]; + } + else { + $class = $config['default']; + } + + if (empty($instance[$class])) { + $interfaces = class_implements($class); + if (isset($interfaces['DrupalSmtpInterface'])) { + $instance[$class] = new $class; + } + else { + throw new Exception(t('Class %class does not implement interface %interface', array('%class' => $class, '%interface' => 'DrupalSmtpInterface'))); + } + } + return $instance[$class]; +} + +/** + * An interface for pluggable mail back-ends. + */ +interface DrupalSmtpInterface { +/** + * Send an e-mail message composed by drupal_mail(). * * @param $message * Message array with at least the following elements: @@ -170,28 +263,7 @@ function drupal_mail($module, $key, $to, * Returns TRUE if the mail was successfully accepted for delivery, * FALSE otherwise. */ -function drupal_mail_send($message) { - // Allow for a custom mail backend. - if (variable_get('smtp_library', '') && file_exists(variable_get('smtp_library', ''))) { - include_once DRUPAL_ROOT . '/' . variable_get('smtp_library', ''); - return drupal_mail_wrapper($message); - } - else { - $mimeheaders = array(); - foreach ($message['headers'] as $name => $value) { - $mimeheaders[] = $name . ': ' . mime_header_encode($value); - } - return mail( - $message['to'], - mime_header_encode($message['subject']), - // Note: e-mail uses CRLF for line-endings, but PHP's API requires LF. - // They will appear correctly in the actual e-mail that is sent. - str_replace("\r", '', $message['body']), - // For headers, PHP's API suggests that we use CRLF normally, - // but some MTAs incorrecly replace LF with CRLF. See #234403. - join("\n", $mimeheaders) - ); - } + public function send($message); } /** Index: includes/password.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/password.inc,v retrieving revision 1.3 diff -u -p -r1.3 password.inc --- includes/password.inc 26 May 2008 17:12:54 -0000 1.3 +++ includes/password.inc 30 Oct 2008 01:32:32 -0000 @@ -9,235 +9,228 @@ * @see http://www.openwall.com/phpass/ * * An alternative or custom version of this password hashing API may be - * used by setting the variable password_inc to the name of the PHP file - * containing replacement user_hash_password(), user_check_password(), and - * user_needs_new_hash() functions. - */ - -/** - * The standard log2 number of iterations for password stretching. This should - * increase by 1 at least every other Drupal version in order to counteract - * increases in the speed and power of computers available to crack the hashes. - */ -define('DRUPAL_HASH_COUNT', 14); - -/** - * The minimum allowed log2 number of iterations for password stretching. - */ -define('DRUPAL_MIN_HASH_COUNT', 7); - -/** - * The maximum allowed log2 number of iterations for password stretching. - */ -define('DRUPAL_MAX_HASH_COUNT', 30); - -/** - * Returns a string for mapping an int to the corresponding base 64 character. - */ -function _password_itoa64() { - return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; -} + * used by setting the variable password_system to the name of a class + * that implements interface PasswordInterface, with hash(), check(), + * needs_new_hash(), and set_hash_strength() methods. + */ +class UserPassword implements PasswordInterface { + /** + * The standard log2 number of iterations for password stretching. This should + * increase by 1 at least every other Drupal version in order to counteract + * increases in the speed and power of computers available to crack the hashes. + */ + const DEFAULT_HASH_COUNT = 14; + + /** + * The minimum allowed log2 number of iterations for password stretching. + */ + const MIN_HASH_COUNT = 7; + + /** + * The maximum allowed log2 number of iterations for password stretching. + */ + const MAX_HASH_COUNT = 30; + + /** + * Returns a string for mapping an int to the corresponding base 64 character. + */ + protected $itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + + /** + * The log2 number of iterations to use when generating a hash. + */ + protected $countLog2Setting = self::DEFAULT_HASH_COUNT; + + /** + * Encode bytes into printable base 64 using the *nix standard from crypt(). + * + * @param $input + * The string containing bytes to encode. + * @param $count + * The number of characters (bytes) to encode. + * + * @return + * Encoded string + */ + private function base64Encode($input, $count) { + $output = ''; + $i = 0; + do { + $value = ord($input[$i++]); + $output .= $this->itoa64[$value & 0x3f]; + if ($i < $count) { + $value |= ord($input[$i]) << 8; + } + $output .= $this->itoa64[($value >> 6) & 0x3f]; + if ($i++ >= $count) { + break; + } + if ($i < $count) { + $value |= ord($input[$i]) << 16; + } + $output .= $this->itoa64[($value >> 12) & 0x3f]; + if ($i++ >= $count) { + break; + } + $output .= $this->itoa64[($value >> 18) & 0x3f]; + } while ($i < $count); + + return $output; + } + + /** + * Generates a random base 64-encoded salt prefixed with settings for the hash. + * + * Proper use of salts may defeat a number of attacks, including: + * - The ability to try candidate passwords against multiple hashes at once. + * - The ability to use pre-hashed lists of candidate passwords. + * - The ability to determine whether two users have the same (or different) + * password without actually having to guess one of the passwords. + * + * @return + * A 12 character string containing the iteration count and a random salt. + */ + private function generateSalt() { + $output = '$P$'; + // Minimum log2 iterations is DRUPAL_MIN_HASH_COUNT. + $count_log2 = max($this->countLog2Setting, self::MIN_HASH_COUNT); + // Maximum log2 iterations is DRUPAL_MAX_HASH_COUNT. + // We encode the final log2 iteration count in base 64. + $output .= $this->itoa64[min($count_log2, self::MAX_HASH_COUNT)]; + // 6 bytes is the standard salt for a portable phpass hash. + $output .= $this->base64Encode(drupal_random_bytes(6), 6); + return $output; + } + + /** + * Hash a password using a secure stretched hash. + * + * By using a salt and repeated hashing the password is "stretched". Its + * security is increased because it becomes much more computationally costly + * for an attacker to try to break the hash by brute-force computation of the + * hashes of a large number of plain-text words or strings to find a match. + * + * @param $password + * The plain-text password to hash. + * @param $setting + * An existing hash or the output of generate_salt(). + * + * @return + * A string containing the hashed password (and salt) or FALSE on failure. + */ + private function passwordCrypt($password, $setting) { + // The first 12 characters of an existing hash are its setting string. + $setting = substr($setting, 0, 12); -/** - * Encode bytes into printable base 64 using the *nix standard from crypt(). - * - * @param $input - * The string containing bytes to encode. - * @param $count - * The number of characters (bytes) to encode. - * - * @return - * Encoded string - */ -function _password_base64_encode($input, $count) { - $output = ''; - $i = 0; - $itoa64 = _password_itoa64(); - do { - $value = ord($input[$i++]); - $output .= $itoa64[$value & 0x3f]; - if ($i < $count) { - $value |= ord($input[$i]) << 8; - } - $output .= $itoa64[($value >> 6) & 0x3f]; - if ($i++ >= $count) { - break; - } - if ($i < $count) { - $value |= ord($input[$i]) << 16; - } - $output .= $itoa64[($value >> 12) & 0x3f]; - if ($i++ >= $count) { - break; + if (substr($setting, 0, 3) != '$P$') { + return FALSE; + } + $count_log2 = $this->getCountLog2($setting); + // Hashes may be imported from elsewhere, so we allow != DEFAULT_HASH_COUNT + if ($count_log2 < self::MIN_HASH_COUNT || $count_log2 > self::MAX_HASH_COUNT) { + return FALSE; + } + $salt = substr($setting, 4, 8); + // Hashes must have an 8 character salt. + if (strlen($salt) != 8) { + return FALSE; } - $output .= $itoa64[($value >> 18) & 0x3f]; - } while ($i < $count); - - return $output; -} - -/** - * Generates a random base 64-encoded salt prefixed with settings for the hash. - * - * Proper use of salts may defeat a number of attacks, including: - * - The ability to try candidate passwords against multiple hashes at once. - * - The ability to use pre-hashed lists of candidate passwords. - * - The ability to determine whether two users have the same (or different) - * password without actually having to guess one of the passwords. - * - * @param $count_log2 - * Integer that determines the number of iterations used in the hashing - * process. A larger value is more secure, but takes more time to complete. - * - * @return - * A 12 character string containing the iteration count and a random salt. - */ -function _password_generate_salt($count_log2) { - $output = '$P$'; - // Minimum log2 iterations is DRUPAL_MIN_HASH_COUNT. - $count_log2 = max($count_log2, DRUPAL_MIN_HASH_COUNT); - // Maximum log2 iterations is DRUPAL_MAX_HASH_COUNT. - // We encode the final log2 iteration count in base 64. - $itoa64 = _password_itoa64(); - $output .= $itoa64[min($count_log2, DRUPAL_MAX_HASH_COUNT)]; - // 6 bytes is the standard salt for a portable phpass hash. - $output .= _password_base64_encode(drupal_random_bytes(6), 6); - return $output; -} - -/** - * Hash a password using a secure stretched hash. - * - * By using a salt and repeated hashing the password is "stretched". Its - * security is increased because it becomes much more computationally costly - * for an attacker to try to break the hash by brute-force computation of the - * hashes of a large number of plain-text words or strings to find a match. - * - * @param $password - * The plain-text password to hash. - * @param $setting - * An existing hash or the output of _password_generate_salt(). - * - * @return - * A string containing the hashed password (and salt) or FALSE on failure. - */ -function _password_crypt($password, $setting) { - // The first 12 characters of an existing hash are its setting string. - $setting = substr($setting, 0, 12); - - if (substr($setting, 0, 3) != '$P$') { - return FALSE; - } - $count_log2 = _password_get_count_log2($setting); - // Hashes may be imported from elsewhere, so we allow != DRUPAL_HASH_COUNT - if ($count_log2 < DRUPAL_MIN_HASH_COUNT || $count_log2 > DRUPAL_MAX_HASH_COUNT) { - return FALSE; - } - $salt = substr($setting, 4, 8); - // Hashes must have an 8 character salt. - if (strlen($salt) != 8) { - return FALSE; - } - - // We must use md5() or sha1() here since they are the only cryptographic - // primitives always available in PHP 5. To implement our own low-level - // cryptographic function in PHP would result in much worse performance and - // consequently in lower iteration counts and hashes that are quicker to crack - // (by non-PHP code). - - $count = 1 << $count_log2; - - $hash = md5($salt . $password, TRUE); - do { - $hash = md5($hash . $password, TRUE); - } while (--$count); - - $output = $setting . _password_base64_encode($hash, 16); - // _password_base64_encode() of a 16 byte MD5 will always be 22 characters. - return (strlen($output) == 34) ? $output : FALSE; -} - -/** - * Parse the log2 iteration count from a stored hash or setting string. - */ -function _password_get_count_log2($setting) { - $itoa64 = _password_itoa64(); - return strpos($itoa64, $setting[3]); -} - -/** - * Hash a password using a secure hash. - * - * @param $password - * A plain-text password. - * @param $count_log2 - * Optional integer to specify the iteration count. Generally used only during - * mass operations where a value less than the default is needed for speed. - * - * @return - * A string containing the hashed password (and a salt), or FALSE on failure. - */ -function user_hash_password($password, $count_log2 = 0) { - if (empty($count_log2)) { - // Use the standard iteration count. - $count_log2 = variable_get('password_count_log2', DRUPAL_HASH_COUNT); - } - return _password_crypt($password, _password_generate_salt($count_log2)); -} -/** - * Check whether a plain text password matches a stored hashed password. - * - * Alternative implementations of this function may use other data in the - * $account object, for example the uid to look up the hash in a custom table - * or remote database. - * - * @param $password - * A plain-text password - * @param $account - * A user object with at least the fields from the {users} table. - * - * @return - * TRUE or FALSE. - */ -function user_check_password($password, $account) { - if (substr($account->pass, 0, 3) == 'U$P') { - // This may be an updated password from user_update_7000(). Such hashes - // have 'U' added as the first character and need an extra md5(). - $stored_hash = substr($account->pass, 1); - $password = md5($password); - } - else { - $stored_hash = $account->pass; + // We must use md5() or sha1() here since they are the only cryptographic + // primitives always available in PHP 5. To implement our own low-level + // cryptographic function in PHP would result in much worse performance and + // consequently in lower iteration counts and hashes that are quicker to crack + // (by non-PHP code). + + $count = 1 << $count_log2; + + $hash = md5($salt . $password, TRUE); + do { + $hash = md5($hash . $password, TRUE); + } while (--$count); + + $output = $setting . $this->base64Encode($hash, 16); + // password_base64_encode() of a 16 byte MD5 will always be 22 characters. + return (strlen($output) == 34) ? $output : FALSE; + } + + /** + * Parse the log2 iteration count from a stored hash or setting string. + */ + private function getCountLog2($setting) { + return strpos($this->itoa64, $setting[3]); + } + + /** + * Set the relative hash strength as compared to the default. + * + * @param $strength + * A positive number; 1.0 for the default. Typical range is 0.01 - 1000.0. + */ + public function setHashStrength($strength) { + // We accept a linear scale strength and convert it to a base 2 logarithm. + $this->countLog2Setting = (int)(log($strength, 2) + self::DEFAULT_HASH_COUNT); + } + + /** + * Hash a password using a secure hash. + * + * @param $password + * A plain-text password. + * + * @return + * A string containing the hashed password (and a salt), or FALSE on failure. + */ + public function hash($password) { + return $this->passwordCrypt($password, $this->generateSalt()); + } + + /** + * Check whether a plain text password matches a stored hashed password. + * + * @param $password + * A plain-text password + * @param $account + * A user object with at least the fields from the {users} table. + * + * @return + * TRUE or FALSE. + */ + public function check($password, $account) { + if (substr($account->pass, 0, 3) == 'U$P') { + // This may be an updated password from user_update_7000(). Such hashes + // have 'U' added as the first character and need an extra md5(). + $stored_hash = substr($account->pass, 1); + $password = md5($password); + } + else { + $stored_hash = $account->pass; + } + $hash = $this->passwordCrypt($password, $stored_hash); + return ($hash && $stored_hash == $hash); } - $hash = _password_crypt($password, $stored_hash); - return ($hash && $stored_hash == $hash); -} -/** - * Check whether a user's hashed password needs to be replaced with a new hash. - * - * This is typically called during the login process when the plain text - * password is available. A new hash is needed when the desired iteration count - * has changed through a change in the variable password_count_log2 or - * DRUPAL_HASH_COUNT or if the user's password hash was generated in an update - * like user_update_7000(). - * - * Alternative implementations of this function might use other criteria based - * on the fields in $account. - * - * @param $account - * A user object with at least the fields from the {users} table. - * - * @return - * TRUE or FALSE. - */ -function user_needs_new_hash($account) { - // Check whether this was an updated password. - if ((substr($account->pass, 0, 3) != '$P$') || (strlen($account->pass) != 34)) { - return TRUE; + /** + * Check whether a user's hashed password needs to be replaced with a new hash. + * + * This is typically called during the login process when the plain text + * password is available. A new hash is needed when the desired iteration count + * has changed through a change in the variable password_hash_strength or + * DEFAULT_HASH_COUNT or if the user's password hash was generated in an update + * like user_update_7000(). + * + * @param $account + * A user object with at least the fields from the {users} table. + * + * @return + * TRUE or FALSE. + */ + public function needsNewHash($account) { + // Check whether this was an updated password. + if ((substr($account->pass, 0, 3) != '$P$') || (strlen($account->pass) != 34)) { + return TRUE; + } + // Check whether the iteration count used differs from the standard number. + return ($this->getCountLog2($account->pass) != $this->countLog2Setting); } - // Check whether the iteration count used differs from the standard number. - return (_password_get_count_log2($account->pass) != variable_get('password_count_log2', DRUPAL_HASH_COUNT)); } - Index: modules/dblog/dblog.test =================================================================== RCS file: /cvs/drupal/drupal/modules/dblog/dblog.test,v retrieving revision 1.9 diff -u -p -r1.9 dblog.test --- modules/dblog/dblog.test 17 Sep 2008 07:11:56 -0000 1.9 +++ modules/dblog/dblog.test 30 Oct 2008 01:32:32 -0000 @@ -180,7 +180,7 @@ class DBLogTestCase extends DrupalWebTes private function doUser() { // Set user variables. $name = $this->randomName(); - $pass = user_password(); + $pass = user_generate_password(); // Add user using form to generate add user event (which is not triggered by drupalCreateUser). $edit = array(); $edit['name'] = $name; Index: modules/openid/openid.module =================================================================== RCS file: /cvs/drupal/drupal/modules/openid/openid.module,v retrieving revision 1.32 diff -u -p -r1.32 openid.module --- modules/openid/openid.module 26 Oct 2008 18:06:38 -0000 1.32 +++ modules/openid/openid.module 30 Oct 2008 01:32:32 -0000 @@ -120,7 +120,7 @@ function openid_form_alter(&$form, $form // with random password to avoid confusion. if (!variable_get('user_email_verification', TRUE)) { $form['pass']['#type'] = 'hidden'; - $form['pass']['#value'] = user_password(); + $form['pass']['#value'] = user_generate_password(); } $form['auth_openid'] = array('#type' => 'hidden', '#value' => $_SESSION['openid']['values']['auth_openid']); } @@ -399,7 +399,7 @@ function openid_authentication($response $form_state['redirect'] = NULL; $form_state['values']['name'] = (empty($response['openid.sreg.nickname'])) ? $identity : $response['openid.sreg.nickname']; $form_state['values']['mail'] = (empty($response['openid.sreg.email'])) ? '' : $response['openid.sreg.email']; - $form_state['values']['pass'] = user_password(); + $form_state['values']['pass'] = user_generate_password(); $form_state['values']['status'] = variable_get('user_register', 1) == 1; $form_state['values']['response'] = $response; $form = drupal_retrieve_form('user_register', $form_state); Index: modules/simpletest/drupal_web_test_case.php =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/drupal_web_test_case.php,v retrieving revision 1.52 diff -u -p -r1.52 drupal_web_test_case.php --- modules/simpletest/drupal_web_test_case.php 20 Oct 2008 13:06:15 -0000 1.52 +++ modules/simpletest/drupal_web_test_case.php 30 Oct 2008 01:32:32 -0000 @@ -542,7 +542,7 @@ class DrupalWebTestCase { $edit['name'] = $this->randomName(); $edit['mail'] = $edit['name'] . '@example.com'; $edit['roles'] = array($rid => $rid); - $edit['pass'] = user_password(); + $edit['pass'] = user_generate_password(); $edit['status'] = 1; $account = user_save('', $edit); Index: modules/user/user.install =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.install,v retrieving revision 1.13 diff -u -p -r1.13 user.install --- modules/user/user.install 20 Sep 2008 20:22:25 -0000 1.13 +++ modules/user/user.install 30 Oct 2008 01:32:32 -0000 @@ -242,8 +242,7 @@ function user_schema() { */ function user_update_7000(&$sandbox) { $ret = array('#finished' => 0); - // Lower than DRUPAL_HASH_COUNT to make the update run at a reasonable speed. - $hash_count_log2 = 11; + // Multi-part update. if (!isset($sandbox['user_from'])) { db_change_field($ret, 'users', 'pass', 'pass', array('type' => 'varchar', 'length' => 128, 'not null' => TRUE, 'default' => '')); @@ -251,15 +250,16 @@ function user_update_7000(&$sandbox) { $sandbox['user_count'] = db_result(db_query("SELECT COUNT(uid) FROM {users}")); } else { - require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc'); // Hash again all current hashed passwords. + // Lower than default strength to make the update run at a better speed. + user_password()->set_hash_strength(0.1); $has_rows = FALSE; // Update this many per page load. $count = 1000; $result = db_query_range("SELECT uid, pass FROM {users} WHERE uid > 0 ORDER BY uid", $sandbox['user_from'], $count); while ($account = db_fetch_array($result)) { $has_rows = TRUE; - $new_hash = user_hash_password($account['pass'], $hash_count_log2); + $new_hash = user_password()->hash($account['pass']); if ($new_hash) { // Indicate an updated password. $new_hash = 'U' . $new_hash; @@ -270,7 +270,7 @@ function user_update_7000(&$sandbox) { $sandbox['user_from'] += $count; if (!$has_rows) { $ret['#finished'] = 1; - $ret[] = array('success' => TRUE, 'query' => "UPDATE {users} SET pass = 'U' . user_hash_password(pass) WHERE uid > 0"); + $ret[] = array('success' => TRUE, 'query' => "UPDATE {users} SET pass = 'U' . user_password()->hash(pass) WHERE uid > 0"); } } return $ret; Index: modules/user/user.module =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.module,v retrieving revision 1.930 diff -u -p -r1.930 user.module --- modules/user/user.module 26 Oct 2008 18:06:39 -0000 1.930 +++ modules/user/user.module 30 Oct 2008 01:32:33 -0000 @@ -221,9 +221,7 @@ function user_save($account, $edit = arr $user_fields = $table['fields']; if (!empty($edit['pass'])) { - // Allow alternate password hashing schemes. - require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc'); - $edit['pass'] = user_hash_password(trim($edit['pass'])); + $edit['pass'] = user_password()->hash(trim($edit['pass'])); // Abort if the hashing failed and returned FALSE. if (!$edit['pass']) { return FALSE; @@ -427,7 +425,7 @@ function user_validate_picture(&$form, & /** * Generate a random alphanumeric password. */ -function user_password($length = 10) { +function user_generate_password($length = 10) { // This variable contains the list of allowable characters for the // password. Note that the number 0 and the letter 'O' have been // removed to avoid confusion between the two. The same is true @@ -1341,11 +1339,9 @@ function user_authenticate($form_values 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'])); if ($account) { - // Allow alternate password hashing schemes. - require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc'); - if (user_check_password($password, $account)) { - if (user_needs_new_hash($account)) { - $new_hash = user_hash_password($password); + if (user_password()->check($password, $account)) { + if (user_password()->needsNewHash($account)) { + $new_hash = user_password()->hash($password); if ($new_hash) { db_query("UPDATE {users} SET pass = '%s' WHERE uid = %d", $new_hash, $account->uid); } @@ -1409,7 +1405,7 @@ function user_external_login_register($n // Register this new user. $userinfo = array( 'name' => $name, - 'pass' => user_password(), + 'pass' => user_generate_password(), 'init' => $name, 'status' => 1, 'access' => REQUEST_TIME @@ -2129,7 +2125,7 @@ function user_preferred_language($accoun * @param $language * Optional language to use for the notification, overriding account language. * @return - * The return value from drupal_mail_send(), if ends up being called. + * The return value from drupal_smtp()->send(), if it is called. */ function _user_mail_notify($op, $account, $language = NULL) { // By default, we always notify except for deleted and blocked. @@ -2261,7 +2257,7 @@ function user_register_submit($form, &$f $pass = $form_state['values']['pass']; } else { - $pass = user_password(); + $pass = user_generate_password(); }; $notify = isset($form_state['values']['notify']) ? $form_state['values']['notify'] : NULL; $from = variable_get('site_mail', ini_get('sendmail_from')); @@ -2447,3 +2443,85 @@ function _user_forms(&$edit, $account, $ return empty($groups) ? FALSE : $groups; } + +/** + * User module interfaces and their corresponding factory functions. + */ + +/** + * Returns a password hashing object that implements PasswordInterface. + */ +function user_password() { + static $instance; + + if (empty($instance)) { + $class = variable_get('password_system', 'UserPassword'); + $interfaces = class_implements($class); + if (isset($interfaces['PasswordInterface'])) { + $instance = new $class; + // Set a default hash strength. + $instance->setHashStrength(variable_get('password_hash_strength', 1.0)); + } + else { + throw new Exception(t('Class %class does not implement interface %interface', array('%class' => $class, '%interface' => 'PasswordInterface'))); + } + } + return $instance; +} + +interface PasswordInterface { + /** + * Set the relative hash strength as compared to the default. + * + * @param $strength + * A positive number; 1.0 for the default. Typical range is 0.01 - 1000.0. + * Generally used only during mass operations where a value less than + * the default is needed for speed. + */ + public function setHashStrength($strength); + + /** + * Hash a password using a secure hash. + * + * @param $password + * A plain-text password. + * + * @return + * A string containing the hashed password, or FALSE on failure. + */ + public function hash($password); + + /** + * Check whether a plain text password matches a stored hashed password. + * + * Implementations of this function may use a variety of data in the + * $account object, for example the uid to look up the hash in a custom table + * or remote database. + * + * @param $password + * A plain-text password + * @param $account + * A user object with at least the fields from the {users} table. + * + * @return + * TRUE or FALSE. + */ + public function check($password, $account); + + /** + * Check whether a user's hashed password needs to be replaced with a new hash. + * + * This is typically called during the login process when the plain text + * password is available. A new hash is needed, for example, when the desired + * iteration count has changed. Implementations of this function might use a + * variety of criteria based on the fields in $account. + * + * @param $account + * A user object with at least the fields from the {users} table. + * + * @return + * TRUE or FALSE. + */ + public function needsNewHash($account); +} + Index: modules/user/user.test =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.test,v retrieving revision 1.17 diff -u -p -r1.17 user.test --- modules/user/user.test 10 Oct 2008 07:49:49 -0000 1.17 +++ modules/user/user.test 30 Oct 2008 01:32:33 -0000 @@ -67,7 +67,7 @@ class UserRegistrationTestCase extends D $this->assertText(t('You have just used your one-time login link. It is no longer necessary to use this link to login. Please change your password.'), t('This link is no longer valid.')); // Change user password. - $new_pass = user_password(); + $new_pass = user_generate_password(); $edit = array(); $edit['pass[pass1]'] = $new_pass; $edit['pass[pass2]'] = $new_pass; @@ -75,10 +75,8 @@ class UserRegistrationTestCase extends D $this->assertText(t('The changes have been saved.'), t('Password changed to @password', array('@password' => $new_pass))); // Make sure password changes are present in database. - require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc'); - $user = user_load(array('uid' => $user->uid)); - $this->assertTrue(user_check_password($new_pass, $user), t('Correct password in database.')); + $this->assertTrue(user_password()->check($new_pass, $user), t('Correct password in database.')); // Logout of user account. $this->clickLink(t('Log out')); Index: scripts/password-hash.sh =================================================================== RCS file: /cvs/drupal/drupal/scripts/password-hash.sh,v retrieving revision 1.2 diff -u -p -r1.2 password-hash.sh --- scripts/password-hash.sh 20 Sep 2008 20:22:25 -0000 1.2 +++ scripts/password-hash.sh 30 Oct 2008 01:32:33 -0000 @@ -85,11 +85,12 @@ while ($param = array_shift($_SERVER['ar define('DRUPAL_ROOT', getcwd()); -include_once DRUPAL_ROOT . '/includes/password.inc'; +include_once DRUPAL_ROOT . '/modules/user/user.module'; include_once DRUPAL_ROOT . '/includes/common.inc'; +include_once DRUPAL_ROOT . '/includes/password.inc'; foreach ($passwords as $password) { - print("\npassword: $password \t\thash: ". user_hash_password($password) ."\n"); + print("\npassword: $password \t\thash: ". user_password()->hash($password) ."\n"); } print("\n");