MediaWiki REL1_39
BotPassword.php
Go to the documentation of this file.
1<?php
29
34class BotPassword implements IDBAccessObject {
35
36 public const APPID_MAXLENGTH = 32;
37
41 public const PASSWORD_MINLENGTH = 32;
42
47 public const RESTRICTIONS_MAXLENGTH = 65535;
48
53 public const GRANTS_MAXLENGTH = 65535;
54
56 private $isSaved;
57
59 private $centralId;
60
62 private $appId;
63
65 private $token;
66
68 private $restrictions;
69
71 private $grants;
72
74 private $flags;
75
83 public function __construct( $row, $isSaved, $flags = self::READ_NORMAL ) {
84 $this->isSaved = $isSaved;
85 $this->flags = $flags;
86
87 $this->centralId = (int)$row->bp_user;
88 $this->appId = $row->bp_app_id;
89 $this->token = $row->bp_token;
90 $this->restrictions = MWRestrictions::newFromJson( $row->bp_restrictions );
91 $this->grants = FormatJson::decode( $row->bp_grants );
92 }
93
99 public static function getDB( $db ) {
100 return MediaWikiServices::getInstance()
101 ->getBotPasswordStore()
102 ->getDatabase( $db );
103 }
104
112 public static function newFromUser( UserIdentity $userIdentity, $appId, $flags = self::READ_NORMAL ) {
113 return MediaWikiServices::getInstance()
114 ->getBotPasswordStore()
115 ->getByUser( $userIdentity, (string)$appId, (int)$flags );
116 }
117
125 public static function newFromCentralId( $centralId, $appId, $flags = self::READ_NORMAL ) {
126 return MediaWikiServices::getInstance()
127 ->getBotPasswordStore()
128 ->getByCentralId( (int)$centralId, (string)$appId, (int)$flags );
129 }
130
143 public static function newUnsaved( array $data, $flags = self::READ_NORMAL ) {
144 return MediaWikiServices::getInstance()
145 ->getBotPasswordStore()
146 ->newUnsavedBotPassword( $data, (int)$flags );
147 }
148
153 public function isSaved() {
154 return $this->isSaved;
155 }
156
161 public function getUserCentralId() {
162 return $this->centralId;
163 }
164
168 public function getAppId() {
169 return $this->appId;
170 }
171
175 public function getToken() {
176 return $this->token;
177 }
178
182 public function getRestrictions() {
183 return $this->restrictions;
184 }
185
189 public function getGrants() {
190 return $this->grants;
191 }
192
197 public static function getSeparator() {
198 $userrightsInterwikiDelimiter = MediaWikiServices::getInstance()
199 ->getMainConfig()->get( MainConfigNames::UserrightsInterwikiDelimiter );
200 return $userrightsInterwikiDelimiter;
201 }
202
206 private function getPassword() {
207 list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $this->flags );
208 $db = self::getDB( $index );
209 $password = $db->selectField(
210 'bot_passwords',
211 'bp_password',
212 [ 'bp_user' => $this->centralId, 'bp_app_id' => $this->appId ],
213 __METHOD__,
214 $options
215 );
216 if ( $password === false ) {
217 return PasswordFactory::newInvalidPassword();
218 }
219
220 $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
221 try {
222 return $passwordFactory->newFromCiphertext( $password );
223 } catch ( PasswordError $ex ) {
225 }
226 }
227
233 public function isInvalid() {
234 return $this->getPassword() instanceof InvalidPassword;
235 }
236
244 public function save( $operation, Password $password = null ) {
245 // Ensure operation is valid
246 if ( $operation !== 'insert' && $operation !== 'update' ) {
247 throw new UnexpectedValueException(
248 "Expected 'insert' or 'update'; got '{$operation}'."
249 );
250 }
251
252 $store = MediaWikiServices::getInstance()->getBotPasswordStore();
253 if ( $operation === 'insert' ) {
254 $statusValue = $store->insertBotPassword( $this, $password );
255 } else {
256 // Must be update, already checked above
257 $statusValue = $store->updateBotPassword( $this, $password );
258 }
259
260 if ( $statusValue->isGood() ) {
261 $this->token = $statusValue->getValue();
262 $this->isSaved = true;
263 return Status::newGood();
264 }
265
266 // Action failed, status will have code botpasswords-insert-failed or
267 // botpasswords-update-failed depending on which action we tried
268 return Status::wrap( $statusValue );
269 }
270
275 public function delete() {
276 $ok = MediaWikiServices::getInstance()
277 ->getBotPasswordStore()
278 ->deleteBotPassword( $this );
279 if ( $ok ) {
280 $this->token = '**unsaved**';
281 $this->isSaved = false;
282 }
283 return $ok;
284 }
285
291 public static function invalidateAllPasswordsForUser( $username ) {
292 return MediaWikiServices::getInstance()
293 ->getBotPasswordStore()
294 ->invalidateUserPasswords( (string)$username );
295 }
296
305 public static function invalidateAllPasswordsForCentralId( $centralId ) {
306 wfDeprecated( __METHOD__, '1.37' );
307
308 $enableBotPasswords = MediaWikiServices::getInstance()->getMainConfig()
309 ->get( MainConfigNames::EnableBotPasswords );
310
311 if ( !$enableBotPasswords ) {
312 return false;
313 }
314
315 $dbw = self::getDB( DB_PRIMARY );
316 $dbw->update(
317 'bot_passwords',
318 [ 'bp_password' => PasswordFactory::newInvalidPassword()->toString() ],
319 [ 'bp_user' => $centralId ],
320 __METHOD__
321 );
322 return (bool)$dbw->affectedRows();
323 }
324
330 public static function removeAllPasswordsForUser( $username ) {
331 return MediaWikiServices::getInstance()
332 ->getBotPasswordStore()
333 ->removeUserPasswords( (string)$username );
334 }
335
344 public static function removeAllPasswordsForCentralId( $centralId ) {
345 wfDeprecated( __METHOD__, '1.37' );
346
347 $enableBotPasswords = MediaWikiServices::getInstance()->getMainConfig()
348 ->get( MainConfigNames::EnableBotPasswords );
349
350 if ( !$enableBotPasswords ) {
351 return false;
352 }
353
354 $dbw = self::getDB( DB_PRIMARY );
355 $dbw->delete(
356 'bot_passwords',
357 [ 'bp_user' => $centralId ],
358 __METHOD__
359 );
360 return (bool)$dbw->affectedRows();
361 }
362
368 public static function generatePassword( $config ) {
369 return PasswordFactory::generateRandomPasswordString( max(
370 self::PASSWORD_MINLENGTH, $config->get( MainConfigNames::MinimalPasswordLength ) ) );
371 }
372
382 public static function canonicalizeLoginData( $username, $password ) {
383 $sep = self::getSeparator();
384 // the strlen check helps minimize the password information obtainable from timing
385 if ( strlen( $password ) >= self::PASSWORD_MINLENGTH && strpos( $username, $sep ) !== false ) {
386 // the separator is not valid in new usernames but might appear in legacy ones
387 if ( preg_match( '/^[0-9a-w]{' . self::PASSWORD_MINLENGTH . ',}$/', $password ) ) {
388 return [ $username, $password ];
389 }
390 } elseif ( strlen( $password ) > self::PASSWORD_MINLENGTH && strpos( $password, $sep ) !== false ) {
391 $segments = explode( $sep, $password );
392 $password = array_pop( $segments );
393 $appId = implode( $sep, $segments );
394 if ( preg_match( '/^[0-9a-w]{' . self::PASSWORD_MINLENGTH . ',}$/', $password ) ) {
395 return [ $username . $sep . $appId, $password ];
396 }
397 }
398 return false;
399 }
400
408 public static function login( $username, $password, WebRequest $request ) {
409 $enableBotPasswords = MediaWikiServices::getInstance()->getMainConfig()
410 ->get( MainConfigNames::EnableBotPasswords );
411 $passwordAttemptThrottle = MediaWikiServices::getInstance()->getMainConfig()
412 ->get( MainConfigNames::PasswordAttemptThrottle );
413 if ( !$enableBotPasswords ) {
414 return Status::newFatal( 'botpasswords-disabled' );
415 }
416
417 $provider = SessionManager::singleton()->getProvider( BotPasswordSessionProvider::class );
418 if ( !$provider ) {
419 return Status::newFatal( 'botpasswords-no-provider' );
420 }
421
422 // Split name into name+appId
423 $sep = self::getSeparator();
424 if ( strpos( $username, $sep ) === false ) {
425 return self::loginHook( $username, null, Status::newFatal( 'botpasswords-invalid-name', $sep ) );
426 }
427 list( $name, $appId ) = explode( $sep, $username, 2 );
428
429 // Find the named user
430 $user = User::newFromName( $name );
431 if ( !$user || $user->isAnon() ) {
432 return self::loginHook( $user ?: $name, null, Status::newFatal( 'nosuchuser', $name ) );
433 }
434
435 if ( $user->isLocked() ) {
436 return Status::newFatal( 'botpasswords-locked' );
437 }
438
439 $throttle = null;
440 if ( !empty( $passwordAttemptThrottle ) ) {
441 $throttle = new Throttler( $passwordAttemptThrottle, [
442 'type' => 'botpassword',
443 'cache' => ObjectCache::getLocalClusterInstance(),
444 ] );
445 $result = $throttle->increase( $user->getName(), $request->getIP(), __METHOD__ );
446 if ( $result ) {
447 $msg = wfMessage( 'login-throttled' )->durationParams( $result['wait'] );
448 return self::loginHook( $user, null, Status::newFatal( $msg ) );
449 }
450 }
451
452 // Get the bot password
453 $bp = self::newFromUser( $user, $appId );
454 if ( !$bp ) {
455 return self::loginHook( $user, $bp,
456 Status::newFatal( 'botpasswords-not-exist', $name, $appId ) );
457 }
458
459 // Check restrictions
460 $status = $bp->getRestrictions()->check( $request );
461 if ( !$status->isOK() ) {
462 return self::loginHook( $user, $bp, Status::newFatal( 'botpasswords-restriction-failed' ) );
463 }
464
465 // Check the password
466 $passwordObj = $bp->getPassword();
467 if ( $passwordObj instanceof InvalidPassword ) {
468 return self::loginHook( $user, $bp,
469 Status::newFatal( 'botpasswords-needs-reset', $name, $appId ) );
470 }
471 if ( !$passwordObj->verify( $password ) ) {
472 return self::loginHook( $user, $bp, Status::newFatal( 'wrongpassword' ) );
473 }
474
475 // Ok! Create the session.
476 if ( $throttle ) {
477 $throttle->clear( $user->getName(), $request->getIP() );
478 }
479 return self::loginHook( $user, $bp,
480 // @phan-suppress-next-line PhanUndeclaredMethod
481 Status::newGood( $provider->newSessionForRequest( $user, $bp, $request ) ) );
482 }
483
495 private static function loginHook( $user, $bp, Status $status ) {
496 $extraData = [];
497 if ( $user instanceof User ) {
498 $name = $user->getName();
499 if ( $bp ) {
500 $extraData['appId'] = $name . self::getSeparator() . $bp->getAppId();
501 }
502 } else {
503 $name = $user;
504 $user = null;
505 }
506
507 if ( $status->isGood() ) {
508 $response = AuthenticationResponse::newPass( $name );
509 } else {
510 $response = AuthenticationResponse::newFail( $status->getMessage() );
511 }
512 Hooks::runner()->onAuthManagerLoginAuthenticateAudit( $response, $user, $name, $extraData );
513
514 return $status;
515 }
516}
getDB()
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Utility class for bot passwords.
isInvalid()
Whether the password is currently invalid.
__construct( $row, $isSaved, $flags=self::READ_NORMAL)
static newFromUser(UserIdentity $userIdentity, $appId, $flags=self::READ_NORMAL)
Load a BotPassword from the database.
const APPID_MAXLENGTH
static invalidateAllPasswordsForUser( $username)
Invalidate all passwords for a user, by name.
static generatePassword( $config)
Returns a (raw, unhashed) random password string.
const PASSWORD_MINLENGTH
Minimum length for a bot password.
getUserCentralId()
Get the central user ID.
static getDB( $db)
Get a database connection for the bot passwords database.
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",...
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition Hooks.php:173
Represents an invalid password hash.
A class to check request restrictions expressed as a JSON object.
This is a value object to hold authentication response data.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
This serves as the entry point to the MediaWiki session handling system.
Show an error when any operation involving passwords fails to run.
static newInvalidPassword()
Create an InvalidPassword.
Represents a password hash for use in authentication.
Definition Password.php:61
isOK()
Returns whether the operation completed.
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.
Definition Status.php:44
getMessage( $shortContext=false, $longContext=false, $lang=null)
Get a bullet list of the errors as a Message object.
Definition Status.php:244
internal since 1.36
Definition User.php:70
static newFromName( $name, $validate='valid')
Definition User.php:598
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.
Interface for objects representing user identity.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:39
const DB_PRIMARY
Definition defines.php:28