return $this->getPasswordValidity( $password ) === true;
}
+
/**
* Given unvalidated password input, return error message on failure.
*
* @return mixed: true on success, string or array of error message on failure
*/
public function getPasswordValidity( $password ) {
+ $result = $this->checkPasswordValidity( $password );
+ if ( $result->isGood() ) {
+ return true;
+ } else {
+ $messages = array();
+ foreach ( $result->getErrorsByType( 'error' ) as $error ) {
+ $messages[] = $error['message'];
+ }
+ foreach ( $result->getErrorsByType( 'warning' ) as $warning ) {
+ $messages[] = $warning['message'];
+ }
+ if ( count( $messages ) === 1 ) {
+ return $messages[0];
+ }
+ return $messages;
+ }
+ }
+
+ /**
+ * Check if this is a valid password for this user. Status will be good if
+ * the password is valid, or have an array of error messages if not.
+ *
+ * @param string $password Desired password
+ * @return Status
+ * @since 1.23
+ */
+ public function checkPasswordValidity( $password ) {
global $wgMinimalPasswordLength, $wgContLang;
static $blockedLogins = array(
'Apitestsysop' => 'testpass', 'Apitestuser' => 'testpass' # r75605
);
+ $status = Status::newGood();
+
$result = false; //init $result to false for the internal checks
if ( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) ) {
- return $result;
+ $status->error( $result );
+ return $status;
}
if ( $result === false ) {
if ( strlen( $password ) < $wgMinimalPasswordLength ) {
- return 'passwordtooshort';
+ $status->error( 'passwordtooshort', $wgMinimalPasswordLength );
+ return $status;
} elseif ( $wgContLang->lc( $password ) == $wgContLang->lc( $this->mName ) ) {
- return 'password-name-match';
+ $status->error( 'password-name-match' );
+ return $status;
} elseif ( isset( $blockedLogins[$this->getName()] ) && $password == $blockedLogins[$this->getName()] ) {
- return 'password-login-forbidden';
+ $status->error( 'password-login-forbidden' );
+ return $status;
} else {
- //it seems weird returning true here, but this is because of the
+ //it seems weird returning a Good status here, but this is because of the
//initialization of $result to false above. If the hook is never run or it
//doesn't modify $result, then we will likely get down into this if with
//a valid password.
- return true;
+ return $status;
}
} elseif ( $result === true ) {
- return true;
+ return $status;
} else {
- return $result; //the isValidPassword hook set a string $result and returned true
+ $status->error( $result );
+ return $status; //the isValidPassword hook set a string $result and returned true
}
}
# 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.
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" );
global $wgAuth, $wgLegacyEncoding;
$this->load();
- // Even though we stop people from creating passwords that
- // are shorter than this, doesn't mean people wont be able
- // to. Certain authentication plugins do NOT want to save
+ // Certain authentication plugins do NOT want to save
// domain passwords in a mysql database, so we should
// check this (in case $wgAuth->strict() is false).
- if ( !$this->isValidPassword( $password ) ) {
- return false;
- }
if ( $wgAuth->authenticate( $this->getName(), $password ) ) {
return true;
// 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' ) );
}