X-Git-Url: http://git.cyclocoop.org/?a=blobdiff_plain;f=includes%2FUser.php;h=a887e36202b9aebad869d39dbb9ea7b759493633;hb=72462041c030caa32a71f579d1e1d8c165da01de;hp=778e713c7d2e8f427bf7d4a83e2b9bcd527590c6;hpb=6466a598a9579db0789055b73001e39a6d7840a5;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/User.php b/includes/User.php index 778e713c7d..a887e36202 100644 --- a/includes/User.php +++ b/includes/User.php @@ -30,7 +30,7 @@ define( 'USER_TOKEN_LENGTH', 32 ); * Int Serialized record version. * @ingroup Constants */ -define( 'MW_USER_VERSION', 9 ); +define( 'MW_USER_VERSION', 10 ); /** * String Some punctuation to prevent editing from broken text-mangling proxies. @@ -38,14 +38,6 @@ define( 'MW_USER_VERSION', 9 ); */ define( 'EDIT_TOKEN_SUFFIX', '+\\' ); -/** - * Thrown by User::setPassword() on error. - * @ingroup Exception - */ -class PasswordError extends MWException { - // NOP -} - /** * The User object encapsulates all of the user-specific settings (user_id, * name, rights, password, email address, options, last login time). Client @@ -70,6 +62,11 @@ class User implements IDBAccessObject { */ const MAX_WATCHED_ITEMS_CACHE = 100; + /** + * @var PasswordFactory Lazily loaded factory object for passwords + */ + private static $mPasswordFactory = null; + /** * Array of Strings List of member variables which are saved to the * shared cache (memcached). Any operation which changes the @@ -81,16 +78,12 @@ class User implements IDBAccessObject { 'mId', 'mName', 'mRealName', - 'mPassword', - 'mNewpassword', - 'mNewpassTime', 'mEmail', 'mTouched', 'mToken', 'mEmailAuthenticated', 'mEmailToken', 'mEmailTokenExpires', - 'mPasswordExpires', 'mRegistration', 'mEditCount', // user_groups table @@ -357,7 +350,7 @@ class User implements IDBAccessObject { /** * Load user table data, given mId has already been set. - * @return bool false if the ID does not exist, true otherwise + * @return bool False if the ID does not exist, true otherwise */ public function loadFromId() { global $wgMemc; @@ -369,7 +362,7 @@ class User implements IDBAccessObject { // Try cache $key = wfMemcKey( 'user', 'id', $this->mId ); $data = $wgMemc->get( $key ); - if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) { + if ( !is_array( $data ) || $data['mVersion'] != MW_USER_VERSION ) { // Object is expired, load from DB $data = false; } @@ -753,7 +746,7 @@ class User implements IDBAccessObject { * Given unvalidated password input, return error message on failure. * * @param string $password Desired password - * @return bool|string|array true on success, string or array of error message on failure + * @return bool|string|array True on success, string or array of error message on failure */ public function getPasswordValidity( $password ) { $result = $this->checkPasswordValidity( $password ); @@ -997,10 +990,13 @@ class User implements IDBAccessObject { public function loadDefaults( $name = false ) { wfProfileIn( __METHOD__ ); + $passwordFactory = self::getPasswordFactory(); + $this->mId = 0; $this->mName = $name; $this->mRealName = ''; - $this->mPassword = $this->mNewpassword = ''; + $this->mPassword = $passwordFactory->newFromCiphertext( null ); + $this->mNewpassword = $passwordFactory->newFromCiphertext( null ); $this->mNewpassTime = null; $this->mEmail = ''; $this->mOptionOverrides = null; @@ -1139,7 +1135,7 @@ class User implements IDBAccessObject { * Load user and user_group data from the database. * $this->mId must be set, this is how the user is identified. * - * @param integer $flags Supports User::READ_LOCKING + * @param int $flags Supports User::READ_LOCKING * @return bool True if the user exists, false if the user is anonymous */ public function loadFromDatabase( $flags = 0 ) { @@ -1190,6 +1186,7 @@ class User implements IDBAccessObject { */ public function loadFromRow( $row, $data = null ) { $all = true; + $passwordFactory = self::getPasswordFactory(); $this->mGroups = null; // deferred @@ -1223,9 +1220,31 @@ class User implements IDBAccessObject { } if ( isset( $row->user_password ) ) { - $this->mPassword = $row->user_password; - $this->mNewpassword = $row->user_newpassword; + // Check for *really* old password hashes that don't even have a type + // The old hash format was just an md5 hex hash, with no type information + if ( preg_match( '/^[0-9a-f]{32}$/', $row->user_password ) ) { + $row->user_password = ":A:{$this->mId}:{$row->user_password}"; + } + + try { + $this->mPassword = $passwordFactory->newFromCiphertext( $row->user_password ); + } catch ( PasswordError $e ) { + wfDebug( 'Invalid password hash found in database.' ); + $this->mPassword = $passwordFactory->newFromCiphertext( null ); + } + + try { + $this->mNewpassword = $passwordFactory->newFromCiphertext( $row->user_newpassword ); + } catch ( PasswordError $e ) { + wfDebug( 'Invalid password hash found in database.' ); + $this->mNewpassword = $passwordFactory->newFromCiphertext( null ); + } + $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time ); + $this->mPasswordExpires = wfTimestampOrNull( TS_MW, $row->user_password_expires ); + } + + if ( isset( $row->user_email ) ) { $this->mEmail = $row->user_email; $this->mTouched = wfTimestamp( TS_MW, $row->user_touched ); $this->mToken = $row->user_token; @@ -1235,7 +1254,6 @@ class User implements IDBAccessObject { $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated ); $this->mEmailToken = $row->user_email_token; $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires ); - $this->mPasswordExpires = wfTimestampOrNull( TS_MW, $row->user_password_expires ); $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration ); } else { $all = false; @@ -1286,6 +1304,26 @@ class User implements IDBAccessObject { } } + /** + * Load the user's password hashes from the database + * + * This is usually called in a scenario where the actual User object was + * loaded from the cache, and then password comparison needs to be performed. + * Password hashes are not stored in memcached. + * + * @since 1.24 + */ + private function loadPasswords() { + if ( $this->getId() !== 0 && ( $this->mPassword === null || $this->mNewpassword === null ) ) { + $this->loadFromRow( wfGetDB( DB_MASTER )->selectRow( + 'user', + array( 'user_password', 'user_newpassword', 'user_newpass_time', 'user_password_expires' ), + array( 'user_id' => $this->getId() ), + __METHOD__ + ) ); + } + } + /** * Add the user to the group if he/she meets given criteria. * @@ -2034,7 +2072,7 @@ class User implements IDBAccessObject { * @see getNewtalk() * @param string $field 'user_ip' for anonymous users, 'user_id' otherwise * @param string|int $id User's IP address for anonymous users, User ID otherwise - * @param bool $fromMaster true to fetch from the master, false for a slave + * @param bool $fromMaster True to fetch from the master, false for a slave * @return bool True if the user has new messages */ protected function checkNewtalk( $field, $id, $fromMaster = false ) { @@ -2151,7 +2189,7 @@ class User implements IDBAccessObject { * * Called implicitly from invalidateCache() and saveSettings(). */ - private function clearSharedCache() { + public function clearSharedCache() { $this->load(); if ( $this->mId ) { global $wgMemc; @@ -2224,7 +2262,7 @@ class User implements IDBAccessObject { * a new password is set, for instance via e-mail. * * @param string $str New password to set - * @throws PasswordError on failure + * @throws PasswordError On failure * * @return bool */ @@ -2267,16 +2305,16 @@ class User implements IDBAccessObject { * through the web interface. */ public function setInternalPassword( $str ) { - $this->load(); $this->setToken(); + $passwordFactory = self::getPasswordFactory(); if ( $str === null ) { - // Save an invalid hash... - $this->mPassword = ''; + $this->mPassword = $passwordFactory->newFromCiphertext( null ); } else { - $this->mPassword = self::crypt( $str ); + $this->mPassword = $passwordFactory->newFromPlaintext( $str ); } - $this->mNewpassword = ''; + + $this->mNewpassword = $passwordFactory->newFromCiphertext( null ); $this->mNewpassTime = null; } @@ -2323,7 +2361,7 @@ class User implements IDBAccessObject { $this->mNewpassword = ''; $this->mNewpassTime = null; } else { - $this->mNewpassword = self::crypt( $str ); + $this->mNewpassword = self::getPasswordFactory()->newFromPlaintext( $str ); if ( $throttle ) { $this->mNewpassTime = wfTimestampNow(); } @@ -2624,7 +2662,7 @@ class User implements IDBAccessObject { * @param IContextSource $context * @param array $options Assoc. array with options keys to check as keys. * Defaults to $this->mOptions. - * @return array the key => kind mapping data + * @return array The key => kind mapping data */ public function getOptionKinds( IContextSource $context, $options = null ) { $this->loadOptions(); @@ -2908,7 +2946,7 @@ class User implements IDBAccessObject { /** * Get the user's edit count. - * @return int|null null for anonymous users + * @return int|null Null for anonymous users */ public function getEditCount() { if ( !$this->getId() ) { @@ -3411,6 +3449,7 @@ class User implements IDBAccessObject { global $wgAuth; $this->load(); + $this->loadPasswords(); if ( wfReadOnly() ) { return; } @@ -3420,15 +3459,15 @@ class User implements IDBAccessObject { $this->mTouched = self::newTouchedTimestamp(); if ( !$wgAuth->allowSetLocalPassword() ) { - $this->mPassword = ''; + $this->mPassword = self::getPasswordFactory()->newFromCiphertext( null ); } $dbw = wfGetDB( DB_MASTER ); $dbw->update( 'user', array( /* SET */ 'user_name' => $this->mName, - 'user_password' => $this->mPassword, - 'user_newpassword' => $this->mNewpassword, + 'user_password' => $this->mPassword->toString(), + 'user_newpassword' => $this->mNewpassword->toString(), 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ), 'user_real_name' => $this->mRealName, 'user_email' => $this->mEmail, @@ -3490,6 +3529,7 @@ class User implements IDBAccessObject { public static function createNew( $name, $params = array() ) { $user = new User; $user->load(); + $user->loadPasswords(); $user->setToken(); // init token if ( isset( $params['options'] ) ) { $user->mOptions = $params['options'] + (array)$user->mOptions; @@ -3501,8 +3541,8 @@ class User implements IDBAccessObject { $fields = array( 'user_id' => $seqVal, 'user_name' => $name, - 'user_password' => $user->mPassword, - 'user_newpassword' => $user->mNewpassword, + 'user_password' => $user->mPassword->toString(), + 'user_newpassword' => $user->mNewpassword->toString(), 'user_newpass_time' => $dbw->timestampOrNull( $user->mNewpassTime ), 'user_email' => $user->mEmail, 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ), @@ -3552,6 +3592,7 @@ class User implements IDBAccessObject { */ public function addToDatabase() { $this->load(); + $this->loadPasswords(); if ( !$this->mToken ) { $this->setToken(); // init token } @@ -3565,8 +3606,8 @@ class User implements IDBAccessObject { array( 'user_id' => $seqVal, 'user_name' => $this->mName, - 'user_password' => $this->mPassword, - 'user_newpassword' => $this->mNewpassword, + 'user_password' => $this->mPassword->toString(), + 'user_newpassword' => $this->mNewpassword->toString(), 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ), 'user_email' => $this->mEmail, 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), @@ -3717,12 +3758,12 @@ class User implements IDBAccessObject { /** * Check to see if the given clear-text password is one of the accepted passwords - * @param string $password user password. - * @return bool True if the given password is correct, otherwise False. + * @param string $password User password + * @return bool True if the given password is correct, otherwise False */ public function checkPassword( $password ) { global $wgAuth, $wgLegacyEncoding; - $this->load(); + $this->loadPasswords(); // Certain authentication plugins do NOT want to save // domain passwords in a mysql database, so we should @@ -3737,19 +3778,27 @@ class User implements IDBAccessObject { // Auth plugin doesn't allow local authentication for this user name return false; } - if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) { - return true; - } elseif ( $wgLegacyEncoding ) { - // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted - // Check for this with iconv - $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password ); - if ( $cp1252Password != $password - && self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) - ) { - return true; + + $passwordFactory = self::getPasswordFactory(); + if ( !$this->mPassword->equals( $password ) ) { + if ( $wgLegacyEncoding ) { + // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted + // Check for this with iconv + $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password ); + if ( $cp1252Password === $password || !$this->mPassword->equals( $cp1252Password ) ) { + return false; + } + } else { + return false; } } - return false; + + if ( $passwordFactory->needsUpdate( $this->mPassword ) ) { + $this->mPassword = $passwordFactory->newFromPlaintext( $password ); + $this->saveSettings(); + } + + return true; } /** @@ -3764,7 +3813,7 @@ class User implements IDBAccessObject { global $wgNewPasswordExpiry; $this->load(); - if ( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) { + if ( $this->mNewpassword->equals( $plaintext ) ) { if ( is_null( $this->mNewpassTime ) ) { return true; } @@ -3779,7 +3828,7 @@ class User implements IDBAccessObject { * Alias for getEditToken. * @deprecated since 1.19, use getEditToken instead. * - * @param string|array $salt of Strings Optional function-specific data for hashing + * @param string|array $salt Array of Strings Optional function-specific data for hashing * @param WebRequest|null $request WebRequest object to use or null to use $wgRequest * @return string The new edit token */ @@ -3796,7 +3845,7 @@ class User implements IDBAccessObject { * * @since 1.19 * - * @param string|array $salt of Strings Optional function-specific data for hashing + * @param string|array $salt Array of Strings Optional function-specific data for hashing * @param WebRequest|null $request WebRequest object to use or null to use $wgRequest * @return string The new edit token */ @@ -3857,7 +3906,7 @@ class User implements IDBAccessObject { * * @param string $val Input value to compare * @param string $salt Optional function-specific data for hashing - * @param WebRequest|null $request object to use or null to use $wgRequest + * @param WebRequest|null $request Object to use or null to use $wgRequest * @return bool Whether the token matches */ public function matchEditTokenNoSuffix( $val, $salt = '', $request = null ) { @@ -4363,7 +4412,7 @@ class User implements IDBAccessObject { * Returns an array of the groups that a particular group can add/remove. * * @param string $group The group to check for whether it can add/remove - * @return array array( 'add' => array( addablegroups ), + * @return array Array( 'add' => array( addablegroups ), * 'remove' => array( removablegroups ), * 'add-self' => array( addablegroups to self), * 'remove-self' => array( removable groups from self) ) @@ -4433,7 +4482,7 @@ class User implements IDBAccessObject { /** * Returns an array of groups that this user can add and remove - * @return array array( 'add' => array( addablegroups ), + * @return array Array( 'add' => array( addablegroups ), * 'remove' => array( removablegroups ), * 'add-self' => array( addablegroups to self), * 'remove-self' => array( removable groups from self) ) @@ -4550,22 +4599,6 @@ class User implements IDBAccessObject { return $msg->isBlank() ? $right : $msg->text(); } - /** - * Make an old-style password hash - * - * @param string $password Plain-text password - * @param string $userId User ID - * @return string Password hash - */ - public static function oldCrypt( $password, $userId ) { - global $wgPasswordSalt; - if ( $wgPasswordSalt ) { - return md5( $userId . '-' . md5( $password ) ); - } else { - return md5( $password ); - } - } - /** * Make a new-style password hash * @@ -4573,23 +4606,12 @@ class User implements IDBAccessObject { * @param bool|string $salt Optional salt, may be random or the user ID. * If unspecified or false, will generate one automatically * @return string Password hash + * @deprecated since 1.23, use Password class */ public static function crypt( $password, $salt = false ) { - global $wgPasswordSalt; - - $hash = ''; - if ( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) { - return $hash; - } - - if ( $wgPasswordSalt ) { - if ( $salt === false ) { - $salt = MWCryptRand::generateHex( 8 ); - } - return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) ); - } else { - return ':A:' . md5( $password ); - } + wfDeprecated( __METHOD__, '1.23' ); + $hash = self::getPasswordFactory()->newFromPlaintext( $password ); + return $hash->toString(); } /** @@ -4601,26 +4623,24 @@ class User implements IDBAccessObject { * @param string|bool $userId User ID for old-style password salt * * @return bool + * @deprecated since 1.23, use Password class */ public static function comparePasswords( $hash, $password, $userId = false ) { - $type = substr( $hash, 0, 3 ); - - $result = false; - if ( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) { - return $result; + wfDeprecated( __METHOD__, '1.23' ); + + // Check for *really* old password hashes that don't even have a type + // The old hash format was just an md5 hex hash, with no type information + if ( preg_match( '/^[0-9a-f]{32}$/', $hash ) ) { + global $wgPasswordSalt; + if ( $wgPasswordSalt ) { + $password = ":B:{$userId}:{$hash}"; + } else { + $password = ":A:{$hash}"; + } } - if ( $type == ':A:' ) { - // Unsalted - return md5( $password ) === substr( $hash, 3 ); - } elseif ( $type == ':B:' ) { - // Salted - list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 ); - return md5( $salt . '-' . md5( $password ) ) === $realHash; - } else { - // Old-style - return self::oldCrypt( $password, $userId ) === $hash; - } + $hash = self::getPasswordFactory()->newFromCiphertext( $hash ); + return $hash->equals( $password ); } /** @@ -4824,6 +4844,20 @@ class User implements IDBAccessObject { $dbw->insert( 'user_properties', $insert_rows, __METHOD__, array( 'IGNORE' ) ); } + /** + * Lazily instantiate and return a factory object for making passwords + * + * @return PasswordFactory + */ + public static function getPasswordFactory() { + if ( self::$mPasswordFactory === null ) { + self::$mPasswordFactory = new PasswordFactory(); + self::$mPasswordFactory->init( RequestContext::getMain()->getConfig() ); + } + + return self::$mPasswordFactory; + } + /** * Provide an array of HTML5 attributes to put on an input element * intended for the user to enter a new password. This may include @@ -4892,16 +4926,12 @@ class User implements IDBAccessObject { 'user_id', 'user_name', 'user_real_name', - 'user_password', - 'user_newpassword', - 'user_newpass_time', 'user_email', 'user_touched', 'user_token', 'user_email_authenticated', 'user_email_token', 'user_email_token_expires', - 'user_password_expires', 'user_registration', 'user_editcount', );