* Int Serialized record version.
* @ingroup Constants
*/
-define( 'MW_USER_VERSION', 8 );
+define( 'MW_USER_VERSION', 9 );
/**
* String Some punctuation to prevent editing from broken text-mangling proxies.
'mEmailAuthenticated',
'mEmailToken',
'mEmailTokenExpires',
+ 'mPasswordExpires',
'mRegistration',
'mEditCount',
// user_groups table
$mEmail, $mTouched, $mToken, $mEmailAuthenticated,
$mEmailToken, $mEmailTokenExpires, $mRegistration, $mEditCount,
$mGroups, $mOptionOverrides;
+
+ protected $mPasswordExpires;
//@}
/**
* @see newFromSession()
* @see newFromRow()
*/
- function __construct() {
+ public function __construct() {
$this->clearInstanceCache( 'defaults' );
}
/**
* @return string
*/
- function __toString() {
+ public function __toString() {
return $this->getName();
}
}
}
+ /**
+ * Expire a user's password
+ * @since 1.23
+ * @param $ts Mixed: optional timestamp to convert, default 0 for the current time
+ */
+ public function expirePassword( $ts = 0 ) {
+ $this->load();
+ $timestamp = wfTimestamp( TS_MW, $ts );
+ $this->mPasswordExpires = $timestamp;
+ $this->saveSettings();
+ }
+
+ /**
+ * Clear the password expiration for a user
+ * @since 1.23
+ * @param bool $load ensure user object is loaded first
+ */
+ public function resetPasswordExpiration( $load = true ) {
+ global $wgPasswordExpirationDays;
+ if ( $load ) {
+ $this->load();
+ }
+ $newExpire = null;
+ if ( $wgPasswordExpirationDays ) {
+ $newExpire = wfTimestamp(
+ TS_MW,
+ time() + ( $wgPasswordExpirationDays * 24 * 3600 )
+ );
+ }
+ // Give extensions a chance to force an expiration
+ wfRunHooks( 'ResetPasswordExpiration', array( $this, &$newExpire ) );
+ $this->mPasswordExpires = $newExpire;
+ }
+
+ /**
+ * Check if the user's password is expired.
+ * TODO: Put this and password length into a PasswordPolicy object
+ * @since 1.23
+ * @return string|bool The expiration type, or false if not expired
+ * hard: A password change is required to login
+ * soft: Allow login, but encourage password change
+ * false: Password is not expired
+ */
+ public function getPasswordExpired() {
+ global $wgPasswordExpireGrace;
+ $expired = false;
+ $now = wfTimestamp();
+ $expiration = $this->getPasswordExpireDate();
+ $expUnix = wfTimestamp( TS_UNIX, $expiration );
+ if ( $expiration !== null && $expUnix < $now ) {
+ $expired = ( $expUnix + $wgPasswordExpireGrace < $now ) ? 'hard' : 'soft';
+ }
+ return $expired;
+ }
+
+ /**
+ * Get this user's password expiration date. Since this may be using
+ * the cached User object, we assume that whatever mechanism is setting
+ * the expiration date is also expiring the User cache.
+ * @since 1.23
+ * @return string|false the datestamp of the expiration, or null if not set
+ */
+ public function getPasswordExpireDate() {
+ $this->load();
+ return $this->mPasswordExpires;
+ }
+
/**
* Does a string look like an e-mail address?
*
$this->mEmailAuthenticated = null;
$this->mEmailToken = '';
$this->mEmailTokenExpires = null;
+ $this->mPasswordExpires = null;
+ $this->resetPasswordExpiration( false );
$this->mRegistration = wfTimestamp( TS_MW );
$this->mGroups = array();
# Get the token from DB/cache and clean it up to remove garbage padding.
# This deals with historical problems with bugs and the default column value.
$token = rtrim( $proposedUser->getToken( false ) ); // correct token
- $passwordCorrect = ( strlen( $token ) && $token === $request->getCookie( 'Token' ) );
+ // Make comparison in constant time (bug 61346)
+ $passwordCorrect = strlen( $token ) && $this->compareSecrets( $token, $request->getCookie( 'Token' ) );
$from = 'cookie';
} else {
// No session or persistent login cookie
}
}
+ /**
+ * A comparison of two strings, not vulnerable to timing attacks
+ * @param string $answer the secret string that you are comparing against.
+ * @param string $test compare this string to the $answer.
+ * @return bool True if the strings are the same, false otherwise
+ */
+ protected function compareSecrets( $answer, $test ) {
+ if ( strlen( $answer ) !== strlen( $test ) ) {
+ $passwordCorrect = false;
+ } else {
+ $result = 0;
+ for ( $i = 0; $i < strlen( $answer ); $i++ ) {
+ $result |= ord( $answer{$i} ) ^ ord( $test{$i} );
+ }
+ $passwordCorrect = ( $result == 0 );
+ }
+ return $passwordCorrect;
+ }
+
/**
* Load user and user_group data from the database.
* $this->mId must be set, this is how the user is identified.
$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;
$ipList = gethostbynamel( $host );
if ( $ipList ) {
- wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
+ wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $base!" );
$found = true;
break;
} else {
- wfDebugLog( 'dnsblacklist', "Requested $host, not found in $base.\n" );
+ wfDebugLog( 'dnsblacklist', "Requested $host, not found in $base." );
}
}
}
return false;
}
- global $wgMemc, $wgRateLimitLog;
+ global $wgMemc;
wfProfileIn( __METHOD__ );
$limits = $wgRateLimits[$action];
// Already pinged?
if ( $count ) {
if ( $count >= $max ) {
- wfDebug( __METHOD__ . ": tripped! $key at $count $summary\n" );
- if ( $wgRateLimitLog ) {
- wfSuppressWarnings();
- file_put_contents( $wgRateLimitLog, wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", FILE_APPEND );
- wfRestoreWarnings();
- }
+ wfDebugLog( 'ratelimit', $this->getName() . " tripped! $key at $count $summary");
$triggered = true;
} else {
wfDebug( __METHOD__ . ": ok. $key at $count $summary\n" );
* @param bool $bFromSlave whether to check the slave database instead of the master
* @return bool
*/
- function isBlockedFrom( $title, $bFromSlave = false ) {
+ public function isBlockedFrom( $title, $bFromSlave = false ) {
global $wgBlockAllowsUTEdit;
wfProfileIn( __METHOD__ );
* - 'registered-checkmatrix' - as above, using the 'checkmatrix' type.
* - 'userjs' - preferences with names starting with 'userjs-', intended to
* be used by user scripts.
+ * - 'special' - "preferences" that are not accessible via User::getOptions
+ * or User::setOptions.
* - 'unused' - preferences about which MediaWiki doesn't know anything.
* These are usually legacy options, removed in newer versions.
*
'registered-multiselect',
'registered-checkmatrix',
'userjs',
+ 'special',
'unused'
);
}
$prefs = Preferences::getPreferences( $this, $context );
$mapping = array();
+ // Pull out the "special" options, so they don't get converted as
+ // multiselect or checkmatrix.
+ $specialOptions = array_fill_keys( Preferences::getSaveBlacklist(), true );
+ foreach ( $specialOptions as $name => $value ) {
+ unset( $prefs[$name] );
+ }
+
// Multiselect and checkmatrix options are stored in the database with
// one key per option, each having a boolean value. Extract those keys.
$multiselectOptions = array();
$mapping[$key] = 'registered-multiselect';
} elseif ( isset( $checkmatrixOptions[$key] ) ) {
$mapping[$key] = 'registered-checkmatrix';
+ } elseif ( isset( $specialOptions[$key] ) ) {
+ $mapping[$key] = 'special';
} elseif ( substr( $key, 0, 7 ) === 'userjs-' ) {
$mapping[$key] = 'userjs';
} else {
'user_token' => strval( $this->mToken ),
'user_email_token' => $this->mEmailToken,
'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
+ 'user_password_expires' => $dbw->timestampOrNull( $this->mPasswordExpires ),
), array( /* WHERE */
'user_id' => $this->mId
), __METHOD__
* Get whether the user is allowed to create an account.
* @return bool
*/
- function isAllowedToCreateAccount() {
+ public function isAllowedToCreateAccount() {
return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
}
* @note Call saveSettings() after calling this function to commit the change.
* @return bool Returns true
*/
- function invalidateEmail() {
+ public function invalidateEmail() {
$this->load();
$this->mEmailToken = null;
$this->mEmailTokenExpires = null;
* Set the e-mail authentication timestamp.
* @param string $timestamp TS_MW timestamp
*/
- function setEmailAuthenticationTimestamp( $timestamp ) {
+ public function setEmailAuthenticationTimestamp( $timestamp ) {
$this->load();
$this->mEmailAuthenticated = $timestamp;
wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
// Don't bother storing default values
$defaultOption = self::getDefaultOption( $key );
if ( ( is_null( $defaultOption ) &&
- !( $value === false || is_null( $value ) ) ) ||
- $value != $defaultOption ) {
+ !( $value === false || is_null( $value ) ) ) ||
+ $value != $defaultOption
+ ) {
$insert_rows[] = array(
- 'up_user' => $userId,
- 'up_property' => $key,
- 'up_value' => $value,
- );
+ 'up_user' => $userId,
+ 'up_property' => $key,
+ 'up_value' => $value,
+ );
}
}
$dbw = wfGetDB( DB_MASTER );
- $hasRows = $dbw->selectField( 'user_properties', '1',
- array( 'up_user' => $userId ), __METHOD__ );
-
- if ( $hasRows ) {
- // Only do this delete if there is something there. A very large portion of
+ // Find and delete any prior preference rows...
+ $res = $dbw->select( 'user_properties',
+ array( 'up_property' ), array( 'up_user' => $userId ), __METHOD__ );
+ $priorKeys = array();
+ foreach ( $res as $row ) {
+ $priorKeys[] = $row->up_property;
+ }
+ if ( count( $priorKeys ) ) {
+ // Do the DELETE by PRIMARY KEY for prior rows. A very large portion of
// calls to this function are for setting 'rememberpassword' for new accounts.
- // Doing this delete for new accounts with no rows in the table rougly causes
- // gap locks on [max user ID,+infinity) which causes high contention since many
- // updates will pile up on each other since they are for higher (newer) user IDs.
- $dbw->delete( 'user_properties', array( 'up_user' => $userId ), __METHOD__ );
+ // Doing a blanket per-user DELETE for new accounts with no rows in the table
+ // causes gap locks on [max user ID,+infinity) which causes high contention since
+ // updates will pile up on each other as they are for higher (newer) user IDs.
+ $dbw->delete( 'user_properties',
+ array( 'up_user' => $userId, 'up_property' => $priorKeys ), __METHOD__ );
}
+ // Insert the new preference rows
$dbw->insert( 'user_properties', $insert_rows, __METHOD__, array( 'IGNORE' ) );
}
'user_email_authenticated',
'user_email_token',
'user_email_token_expires',
+ 'user_password_expires',
'user_registration',
'user_editcount',
);