Merge "Allow putting the app ID in the password for bot passwords"
[lhc/web/wiklou.git] / includes / user / BotPassword.php
index fca7775..0bbe12e 100644 (file)
@@ -18,6 +18,8 @@
  * http://www.gnu.org/copyleft/gpl.html
  */
 
+use MediaWiki\Session\BotPasswordSessionProvider;
+
 /**
  * Utility class for bot passwords
  * @since 1.27
@@ -65,7 +67,7 @@ class BotPassword implements IDBAccessObject {
 
        /**
         * Get a database connection for the bot passwords database
-        * @param int $db Index of the connection to get, e.g. DB_MASTER or DB_SLAVE.
+        * @param int $db Index of the connection to get, e.g. DB_MASTER or DB_REPLICA.
         * @return DatabaseBase
         */
        public static function getDB( $db ) {
@@ -74,7 +76,7 @@ class BotPassword implements IDBAccessObject {
                $lb = $wgBotPasswordsCluster
                        ? wfGetLBFactory()->getExternalLB( $wgBotPasswordsCluster )
                        : wfGetLB( $wgBotPasswordsDatabase );
-               return $lb->getConnectionRef( $db, array(), $wgBotPasswordsDatabase );
+               return $lb->getConnectionRef( $db, [], $wgBotPasswordsDatabase );
        }
 
        /**
@@ -109,8 +111,8 @@ class BotPassword implements IDBAccessObject {
                $db = self::getDB( $index );
                $row = $db->selectRow(
                        'bot_passwords',
-                       array( 'bp_user', 'bp_app_id', 'bp_token', 'bp_restrictions', 'bp_grants' ),
-                       array( 'bp_user' => $centralId, 'bp_app_id' => $appId ),
+                       [ 'bp_user', 'bp_app_id', 'bp_token', 'bp_restrictions', 'bp_grants' ],
+                       [ 'bp_user' => $centralId, 'bp_app_id' => $appId ],
                        __METHOD__,
                        $options
                );
@@ -130,15 +132,15 @@ class BotPassword implements IDBAccessObject {
         * @return BotPassword|null
         */
        public static function newUnsaved( array $data, $flags = self::READ_NORMAL ) {
-               $row = (object)array(
+               $row = (object)[
                        'bp_user' => 0,
                        'bp_app_id' => isset( $data['appId'] ) ? trim( $data['appId'] ) : '',
                        'bp_token' => '**unsaved**',
                        'bp_restrictions' => isset( $data['restrictions'] )
                                ? $data['restrictions']
                                : MWRestrictions::newDefault(),
-                       'bp_grants' => isset( $data['grants'] ) ? $data['grants'] : array(),
-               );
+                       'bp_grants' => isset( $data['grants'] ) ? $data['grants'] : [],
+               ];
 
                if (
                        $row->bp_app_id === '' || strlen( $row->bp_app_id ) > self::APPID_MAXLENGTH ||
@@ -239,7 +241,7 @@ class BotPassword implements IDBAccessObject {
                $password = $db->selectField(
                        'bot_passwords',
                        'bp_password',
-                       array( 'bp_user' => $this->centralId, 'bp_app_id' => $this->appId ),
+                       [ 'bp_user' => $this->centralId, 'bp_app_id' => $this->appId ],
                        __METHOD__,
                        $options
                );
@@ -263,15 +265,15 @@ class BotPassword implements IDBAccessObject {
         * @return bool Success
         */
        public function save( $operation, Password $password = null ) {
-               $conds = array(
+               $conds = [
                        'bp_user' => $this->centralId,
                        'bp_app_id' => $this->appId,
-               );
-               $fields = array(
+               ];
+               $fields = [
                        'bp_token' => MWCryptRand::generateHex( User::TOKEN_LENGTH ),
                        'bp_restrictions' => $this->restrictions->toJson(),
                        'bp_grants' => FormatJson::encode( $this->grants ),
-               );
+               ];
 
                if ( $password !== null ) {
                        $fields['bp_password'] = $password->toString();
@@ -282,7 +284,7 @@ class BotPassword implements IDBAccessObject {
                $dbw = self::getDB( DB_MASTER );
                switch ( $operation ) {
                        case 'insert':
-                               $dbw->insert( 'bot_passwords', $fields + $conds, __METHOD__, array( 'IGNORE' ) );
+                               $dbw->insert( 'bot_passwords', $fields + $conds, __METHOD__, [ 'IGNORE' ] );
                                break;
 
                        case 'update':
@@ -305,10 +307,10 @@ class BotPassword implements IDBAccessObject {
         * @return bool Success
         */
        public function delete() {
-               $conds = array(
+               $conds = [
                        'bp_user' => $this->centralId,
                        'bp_app_id' => $this->appId,
-               );
+               ];
                $dbw = self::getDB( DB_MASTER );
                $dbw->delete( 'bot_passwords', $conds, __METHOD__ );
                $ok = (bool)$dbw->affectedRows();
@@ -346,8 +348,8 @@ class BotPassword implements IDBAccessObject {
                $dbw = self::getDB( DB_MASTER );
                $dbw->update(
                        'bot_passwords',
-                       array( 'bp_password' => PasswordFactory::newInvalidPassword()->toString() ),
-                       array( 'bp_user' => $centralId ),
+                       [ 'bp_password' => PasswordFactory::newInvalidPassword()->toString() ],
+                       [ 'bp_user' => $centralId ],
                        __METHOD__
                );
                return (bool)$dbw->affectedRows();
@@ -380,12 +382,50 @@ class BotPassword implements IDBAccessObject {
                $dbw = self::getDB( DB_MASTER );
                $dbw->delete(
                        'bot_passwords',
-                       array( 'bp_user' => $centralId ),
+                       [ 'bp_user' => $centralId ],
                        __METHOD__
                );
                return (bool)$dbw->affectedRows();
        }
 
+       /**
+        * Returns a (raw, unhashed) random password string.
+        * @param Config $config
+        * @return string
+        */
+       public static function generatePassword( $config ) {
+               return PasswordFactory::generateRandomPasswordString(
+                       max( 32, $config->get( 'MinimalPasswordLength' ) ) );
+       }
+
+       /**
+        * There are two ways to login with a bot password: "username@appId", "password" and
+        * "username", "appId@password". Transform it so it is always in the first form.
+        * Returns [bot username, bot password, could be normal password?] where the last one is a flag
+        * meaning this could either be a bot password or a normal password, it cannot be decided for
+        * certain (although in such cases it almost always will be a bot password).
+        * If this cannot be a bot password login just return false.
+        * @param string $username
+        * @param string $password
+        * @return array|false
+        */
+       public static function canonicalizeLoginData( $username, $password ) {
+               $sep = BotPassword::getSeparator();
+               if ( strpos( $username, $sep ) !== false ) {
+                       // the separator is not valid in usernames so this must be a bot login
+                       return [ $username, $password, false ];
+               } elseif ( strlen( $password ) > 32 && strpos( $password, $sep ) !== false ) {
+                       // the strlen check helps minimize the password information obtainable from timing
+                       $segments = explode( $sep, $password );
+                       $password = array_pop( $segments );
+                       $appId = implode( $sep, $segments );
+                       if ( preg_match( '/^[0-9a-w]{32,}$/', $password ) ) {
+                               return [ $username . $sep . $appId, $password, true ];
+                       }
+               }
+               return false;
+       }
+
        /**
         * Try to log the user in
         * @param string $username Combined user name and app ID
@@ -401,9 +441,7 @@ class BotPassword implements IDBAccessObject {
                }
 
                $manager = MediaWiki\Session\SessionManager::singleton();
-               $provider = $manager->getProvider(
-                       'MediaWiki\\Session\\BotPasswordSessionProvider'
-               );
+               $provider = $manager->getProvider( BotPasswordSessionProvider::class );
                if ( !$provider ) {
                        return Status::newFatal( 'botpasswords-no-provider' );
                }
@@ -429,7 +467,7 @@ class BotPassword implements IDBAccessObject {
 
                // Check restrictions
                $status = $bp->getRestrictions()->check( $request );
-               if ( !$status->isOk() ) {
+               if ( !$status->isOK() ) {
                        return Status::newFatal( 'botpasswords-restriction-failed' );
                }