* @file
*/
-/**
- * Int Number of characters in user_token field.
- * @ingroup Constants
- */
-define( 'USER_TOKEN_LENGTH', 32 );
-
-/**
- * Int Serialized record version.
- * @ingroup Constants
- */
-define( 'MW_USER_VERSION', 9 );
-
/**
* String Some punctuation to prevent editing from broken text-mangling proxies.
* @ingroup Constants
*/
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
*/
class User implements IDBAccessObject {
/**
- * Global constants made accessible as class constants so that autoloader
+ * @const int Number of characters in user_token field.
+ */
+ const TOKEN_LENGTH = 32;
+
+ /**
+ * Global constant made accessible as class constants so that autoloader
* magic can be used.
*/
- const USER_TOKEN_LENGTH = USER_TOKEN_LENGTH;
- const MW_USER_VERSION = MW_USER_VERSION;
const EDIT_TOKEN_SUFFIX = EDIT_TOKEN_SUFFIX;
+ /**
+ * @const int Serialized record version.
+ */
+ const VERSION = 10;
+
/**
* Maximum items in $mWatchedItems
*/
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
'mId',
'mName',
'mRealName',
- 'mPassword',
- 'mNewpassword',
- 'mNewpassTime',
'mEmail',
'mTouched',
'mToken',
'mEmailAuthenticated',
'mEmailToken',
'mEmailTokenExpires',
- 'mPasswordExpires',
'mRegistration',
'mEditCount',
// user_groups table
// 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'] != self::VERSION ) {
// Object is expired, load from DB
$data = false;
}
foreach ( self::$mCacheVars as $name ) {
$data[$name] = $this->$name;
}
- $data['mVersion'] = MW_USER_VERSION;
+ $data['mVersion'] = self::VERSION;
$key = wfMemcKey( 'user', 'id', $this->mId );
global $wgMemc;
$wgMemc->set( $key, $data );
return false;
}
- // Clean up name according to title rules
- $t = ( $validate === 'valid' ) ?
+ // Clean up name according to title rules,
+ // but only when validation is requested (bug 12654)
+ $t = ( $validate !== false ) ?
Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
// Check for invalid titles
if ( is_null( $t ) ) {
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;
*/
public function loadFromRow( $row, $data = null ) {
$all = true;
+ $passwordFactory = self::getPasswordFactory();
$this->mGroups = null; // deferred
}
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;
$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;
}
}
+ /**
+ * 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.
*
*
* Called implicitly from invalidateCache() and saveSettings().
*/
- private function clearSharedCache() {
+ public function clearSharedCache() {
$this->load();
if ( $this->mId ) {
global $wgMemc;
* 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;
}
public function setToken( $token = false ) {
$this->load();
if ( !$token ) {
- $this->mToken = MWCryptRand::generateHex( USER_TOKEN_LENGTH );
+ $this->mToken = MWCryptRand::generateHex( self::TOKEN_LENGTH );
} else {
$this->mToken = $token;
}
$this->mNewpassword = '';
$this->mNewpassTime = null;
} else {
- $this->mNewpassword = self::crypt( $str );
+ $this->mNewpassword = self::getPasswordFactory()->newFromPlaintext( $str );
if ( $throttle ) {
$this->mNewpassTime = wfTimestampNow();
}
if ( $str == $this->mEmail ) {
return;
}
- $this->mEmail = $str;
$this->invalidateEmail();
+ $this->mEmail = $str;
wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
}
global $wgAuth;
$this->load();
+ $this->loadPasswords();
if ( wfReadOnly() ) {
return;
}
$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,
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;
$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 ),
*/
public function addToDatabase() {
$this->load();
+ $this->loadPasswords();
if ( !$this->mToken ) {
$this->setToken(); // init token
}
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 ),
*/
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
// 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;
}
/**
global $wgNewPasswordExpiry;
$this->load();
- if ( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) {
+ $this->loadPasswords();
+ if ( $this->mNewpassword->equals( $plaintext ) ) {
if ( is_null( $this->mNewpassTime ) ) {
return true;
}
}
if ( $this->isAnon() ) {
- return EDIT_TOKEN_SUFFIX;
+ return self::EDIT_TOKEN_SUFFIX;
} else {
$token = $request->getSessionData( 'wsEditToken' );
if ( $token === null ) {
if ( is_array( $salt ) ) {
$salt = implode( '|', $salt );
}
- return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
+ return md5( $token . $salt ) . self::EDIT_TOKEN_SUFFIX;
}
}
$this->mEmailToken = null;
$this->mEmailTokenExpires = null;
$this->setEmailAuthenticationTimestamp( null );
+ $this->mEmail = '';
wfRunHooks( 'InvalidateEmailComplete', array( $this ) );
return true;
}
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
*
* @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.24, 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.24' );
+ $hash = self::getPasswordFactory()->newFromPlaintext( $password );
+ return $hash->toString();
}
/**
* @param string|bool $userId User ID for old-style password salt
*
* @return bool
+ * @deprecated since 1.24, 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.24' );
+
+ // 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 );
}
/**
$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
'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',
);