77 protected function __construct( $row, $isSaved, $flags = self::READ_NORMAL ) {
81 $this->centralId = (int)$row->bp_user;
82 $this->appId = $row->bp_app_id;
83 $this->token = $row->bp_token;
85 $this->grants = FormatJson::decode( $row->bp_grants );
93 public static function getDB( $db ) {
96 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
110 public static function newFromUser(
User $user, $appId, $flags = self::READ_NORMAL ) {
111 $centralId = CentralIdLookup::factory()->centralIdFromLocalUser(
112 $user, CentralIdLookup::AUDIENCE_RAW,
$flags
124 public static function newFromCentralId( $centralId, $appId, $flags = self::READ_NORMAL ) {
131 list( $index, $options ) = DBAccessObjectUtils::getDBOptions(
$flags );
133 $row = $db->selectRow(
135 [
'bp_user',
'bp_app_id',
'bp_token',
'bp_restrictions',
'bp_grants' ],
140 return $row ?
new self( $row,
true,
$flags ) :
null;
155 public static function newUnsaved( array $data, $flags = self::READ_NORMAL ) {
158 'bp_app_id' => isset( $data[
'appId'] ) ? trim( $data[
'appId'] ) :
'',
159 'bp_token' =>
'**unsaved**',
161 'bp_grants' => $data[
'grants'] ?? [],
165 $row->bp_app_id ===
'' || strlen( $row->bp_app_id ) > self::APPID_MAXLENGTH ||
167 !is_array( $row->bp_grants )
172 $row->bp_restrictions = $row->bp_restrictions->toJson();
173 $row->bp_grants = FormatJson::encode( $row->bp_grants );
175 if ( isset( $data[
'user'] ) ) {
176 if ( !$data[
'user'] instanceof
User ) {
179 $row->bp_user = CentralIdLookup::factory()->centralIdFromLocalUser(
180 $data[
'user'], CentralIdLookup::AUDIENCE_RAW,
$flags
182 } elseif ( isset( $data[
'username'] ) ) {
183 $row->bp_user = CentralIdLookup::factory()->centralIdFromName(
184 $data[
'username'], CentralIdLookup::AUDIENCE_RAW,
$flags
186 } elseif ( isset( $data[
'centralId'] ) ) {
187 $row->bp_user = $data[
'centralId'];
189 if ( !$row->bp_user ) {
193 return new self( $row,
false,
$flags );
201 return $this->isSaved;
209 return $this->centralId;
233 return $this->restrictions;
241 return $this->grants;
258 list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $this->flags );
260 $password = $db->selectField(
263 [
'bp_user' => $this->centralId,
'bp_app_id' => $this->appId ],
267 if ( $password ===
false ) {
268 return PasswordFactory::newInvalidPassword();
271 $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
273 return $passwordFactory->newFromCiphertext( $password );
275 return PasswordFactory::newInvalidPassword();
297 if ( $operation !==
'insert' && $operation !==
'update' ) {
298 throw new UnexpectedValueException(
299 "Expected 'insert' or 'update'; got '{$operation}'."
304 'bp_user' => $this->centralId,
305 'bp_app_id' => $this->appId,
308 $res = Status::newGood();
312 if ( strlen(
$restrictions ) > self::RESTRICTIONS_MAXLENGTH ) {
313 $res->fatal(
'botpasswords-toolong-restrictions' );
316 $grants = FormatJson::encode( $this->grants );
318 if ( strlen(
$grants ) > self::GRANTS_MAXLENGTH ) {
319 $res->fatal(
'botpasswords-toolong-grants' );
322 if ( !
$res->isGood() ) {
332 if ( $password !==
null ) {
333 $fields[
'bp_password'] = $password->toString();
334 } elseif ( $operation ===
'insert' ) {
335 $fields[
'bp_password'] = PasswordFactory::newInvalidPassword()->toString();
340 if ( $operation ===
'insert' ) {
341 $dbw->insert(
'bot_passwords', $fields + $conds, __METHOD__, [
'IGNORE' ] );
344 $dbw->update(
'bot_passwords', $fields, $conds, __METHOD__ );
346 $ok = (bool)$dbw->affectedRows();
348 $this->token = $dbw->selectField(
'bot_passwords',
'bp_token', $conds, __METHOD__ );
355 return Status::newFatal(
"botpasswords-{$operation}-failed", $this->appId );
362 public function delete() {
364 'bp_user' => $this->centralId,
365 'bp_app_id' => $this->appId,
368 $dbw->delete(
'bot_passwords', $conds, __METHOD__ );
369 $ok = (bool)$dbw->affectedRows();
371 $this->token =
'**unsaved**';
383 $centralId = CentralIdLookup::factory()->centralIdFromName(
384 $username, CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST
404 [
'bp_password' => PasswordFactory::newInvalidPassword()->toString() ],
408 return (
bool)$dbw->affectedRows();
417 $centralId = CentralIdLookup::factory()->centralIdFromName(
418 $username, CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST
441 return (
bool)$dbw->affectedRows();
450 return PasswordFactory::generateRandomPasswordString(
451 max( self::PASSWORD_MINLENGTH, $config->get(
'MinimalPasswordLength' ) ) );
464 $sep = self::getSeparator();
466 if ( strlen( $password ) >= self::PASSWORD_MINLENGTH && strpos( $username, $sep ) !==
false ) {
468 if ( preg_match(
'/^[0-9a-w]{' . self::PASSWORD_MINLENGTH .
',}$/', $password ) ) {
469 return [ $username, $password ];
471 } elseif ( strlen( $password ) > self::PASSWORD_MINLENGTH && strpos( $password, $sep ) !==
false ) {
472 $segments = explode( $sep, $password );
473 $password = array_pop( $segments );
474 $appId = implode( $sep, $segments );
475 if ( preg_match(
'/^[0-9a-w]{' . self::PASSWORD_MINLENGTH .
',}$/', $password ) ) {
476 return [ $username . $sep .
$appId, $password ];
493 return Status::newFatal(
'botpasswords-disabled' );
496 $manager = MediaWiki\Session\SessionManager::singleton();
497 $provider = $manager->getProvider( BotPasswordSessionProvider::class );
499 return Status::newFatal(
'botpasswords-no-provider' );
503 $sep = self::getSeparator();
504 if ( strpos( $username, $sep ) ===
false ) {
505 return self::loginHook( $username,
null, Status::newFatal(
'botpasswords-invalid-name', $sep ) );
507 list( $name,
$appId ) = explode( $sep, $username, 2 );
510 $user = User::newFromName( $name );
511 if ( !$user || $user->isAnon() ) {
512 return self::loginHook( $user ?: $name,
null, Status::newFatal(
'nosuchuser', $name ) );
515 if ( $user->isLocked() ) {
516 return Status::newFatal(
'botpasswords-locked' );
522 'type' =>
'botpassword',
523 'cache' => ObjectCache::getLocalClusterInstance(),
525 $result = $throttle->increase( $user->getName(), $request->
getIP(), __METHOD__ );
527 $msg =
wfMessage(
'login-throttled' )->durationParams( $result[
'wait'] );
528 return self::loginHook( $user,
null, Status::newFatal( $msg ) );
533 $bp = self::newFromUser( $user,
$appId );
535 return self::loginHook( $user, $bp,
536 Status::newFatal(
'botpasswords-not-exist', $name,
$appId ) );
540 $status = $bp->getRestrictions()->check( $request );
541 if ( !$status->isOK() ) {
542 return self::loginHook( $user, $bp, Status::newFatal(
'botpasswords-restriction-failed' ) );
546 $passwordObj = $bp->getPassword();
548 return self::loginHook( $user, $bp,
549 Status::newFatal(
'botpasswords-needs-reset', $name,
$appId ) );
551 if ( !$passwordObj->verify( $password ) ) {
552 return self::loginHook( $user, $bp, Status::newFatal(
'wrongpassword' ) );
557 $throttle->clear( $user->getName(), $request->
getIP() );
559 return self::loginHook( $user, $bp,
561 Status::newGood( $provider->newSessionForRequest( $user, $bp, $request ) ) );
577 if ( $user instanceof
User ) {
578 $name = $user->getName();
580 $extraData[
'appId'] = $name . self::getSeparator() . $bp->getAppId();
587 if ( $status->
isGood() ) {
588 $response = AuthenticationResponse::newPass( $name );
590 $response = AuthenticationResponse::newFail( $status->
getMessage() );
592 Hooks::runner()->onAuthManagerLoginAuthenticateAudit( $response, $user, $name, $extraData );
$wgUserrightsInterwikiDelimiter
Character used as a delimiter when testing for interwiki userrights (In Special:UserRights,...
bool $wgEnableBotPasswords
Whether to enable bot passwords.
$wgPasswordAttemptThrottle
Limit password attempts to X attempts per Y seconds per IP per account.
string bool $wgBotPasswordsDatabase
Database name for the bot_passwords table.
string bool $wgBotPasswordsCluster
Cluster for the bot_passwords table If false, the normal cluster will be used.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Utility class for bot passwords.
static newFromUser(User $user, $appId, $flags=self::READ_NORMAL)
Load a BotPassword from the database.
isInvalid()
Whether the password is currently invalid.
__construct( $row, $isSaved, $flags=self::READ_NORMAL)
static invalidateAllPasswordsForUser( $username)
Invalidate all passwords for a user, by name.
static loginHook( $user, $bp, Status $status)
Call AuthManagerLoginAuthenticateAudit.
getRestrictions()
Get the restrictions.
MWRestrictions $restrictions
static generatePassword( $config)
Returns a (raw, unhashed) random password string.
getPassword()
Get the password.
const PASSWORD_MINLENGTH
Minimum length for a bot password.
getUserCentralId()
Get the central user ID.
getGrants()
Get the grants.
static getDB( $db)
Get a database connection for the bot passwords database.
getAppId()
Get the app ID.
const RESTRICTIONS_MAXLENGTH
Maximum length of the json representation of restrictions.
static newFromCentralId( $centralId, $appId, $flags=self::READ_NORMAL)
Load a BotPassword from the database.
const GRANTS_MAXLENGTH
Maximum length of the json representation of grants.
static login( $username, $password, WebRequest $request)
Try to log the user in.
static invalidateAllPasswordsForCentralId( $centralId)
Invalidate all passwords for a user, by central ID.
isSaved()
Indicate whether this is known to be saved.
static removeAllPasswordsForUser( $username)
Remove all passwords for a user, by name.
static removeAllPasswordsForCentralId( $centralId)
Remove all passwords for a user, by central ID.
static newUnsaved(array $data, $flags=self::READ_NORMAL)
Create an unsaved BotPassword.
static getSeparator()
Get the separator for combined user name + app ID.
save( $operation, Password $password=null)
Save the BotPassword to the database.
static canonicalizeLoginData( $username, $password)
There are two ways to login with a bot password: "username@appId", "password" and "username",...
Represents an invalid password hash.
static generateHex( $chars)
Generate a run of cryptographically random data and return it in hexadecimal string format.
A class to check request restrictions expressed as a JSON object.
static newFromJson( $json)
toJson( $pretty=false)
Return the restrictions as a JSON string.
Show an error when any operation involving passwords fails to run.
Represents a password hash for use in authentication.
isGood()
Returns whether the operation completed and didn't have any error or warnings.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
getMessage( $shortContext=false, $longContext=false, $lang=null)
Get a bullet list of the errors as a Message object.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
getIP()
Work out the IP address based on various globals For trusted proxies, use the XFF client IP (first of...
Interface for database access objects.