SECURITY: Make $wgBlockDisablesLogin also restrict logged in permissions
[lhc/web/wiklou.git] / includes / user / User.php
index 39507af..8859609 100644 (file)
@@ -648,8 +648,6 @@ class User implements IDBAccessObject {
         * @since 1.27
         */
        public static function newSystemUser( $name, $options = [] ) {
-               global $wgDisableAuthManager;
-
                $options += [
                        'validate' => 'valid',
                        'create' => true,
@@ -662,9 +660,6 @@ class User implements IDBAccessObject {
                }
 
                $fields = self::selectFields();
-               if ( $wgDisableAuthManager ) {
-                       $fields = array_merge( $fields, [ 'user_password', 'user_newpassword' ] );
-               }
 
                $dbw = wfGetDB( DB_MASTER );
                $row = $dbw->selectRow(
@@ -681,49 +676,15 @@ class User implements IDBAccessObject {
 
                // A user is considered to exist as a non-system user if it can
                // authenticate, or has an email set, or has a non-invalid token.
-               if ( !$user->mEmail && $user->mToken === self::INVALID_TOKEN ) {
-                       if ( $wgDisableAuthManager ) {
-                               $passwordFactory = new PasswordFactory();
-                               $passwordFactory->init( RequestContext::getMain()->getConfig() );
-                               try {
-                                       $password = $passwordFactory->newFromCiphertext( $row->user_password );
-                               } catch ( PasswordError $e ) {
-                                       wfDebug( 'Invalid password hash found in database.' );
-                                       $password = PasswordFactory::newInvalidPassword();
-                               }
-                               try {
-                                       $newpassword = $passwordFactory->newFromCiphertext( $row->user_newpassword );
-                               } catch ( PasswordError $e ) {
-                                       wfDebug( 'Invalid password hash found in database.' );
-                                       $newpassword = PasswordFactory::newInvalidPassword();
-                               }
-                               $canAuthenticate = !$password instanceof InvalidPassword ||
-                                       !$newpassword instanceof InvalidPassword;
-                       } else {
-                               $canAuthenticate = AuthManager::singleton()->userCanAuthenticate( $name );
-                       }
-               }
-               if ( $user->mEmail || $user->mToken !== self::INVALID_TOKEN || $canAuthenticate ) {
+               if ( $user->mEmail || $user->mToken !== self::INVALID_TOKEN ||
+                       AuthManager::singleton()->userCanAuthenticate( $name )
+               ) {
                        // User exists. Steal it?
                        if ( !$options['steal'] ) {
                                return null;
                        }
 
-                       if ( $wgDisableAuthManager ) {
-                               $nopass = PasswordFactory::newInvalidPassword()->toString();
-                               $dbw->update(
-                                       'user',
-                                       [
-                                               'user_password' => $nopass,
-                                               'user_newpassword' => $nopass,
-                                               'user_newpass_time' => null,
-                                       ],
-                                       [ 'user_id' => $user->getId() ],
-                                       __METHOD__
-                               );
-                       } else {
-                               AuthManager::singleton()->revokeAccessForUser( $name );
-                       }
+                       AuthManager::singleton()->revokeAccessForUser( $name );
 
                        $user->invalidateEmail();
                        $user->mToken = self::INVALID_TOKEN;
@@ -1387,8 +1348,6 @@ class User implements IDBAccessObject {
         */
        protected function loadFromUserObject( $user ) {
                $user->load();
-               $user->loadGroups();
-               $user->loadOptions();
                foreach ( self::$mCacheVars as $var ) {
                        $this->$var = $user->$var;
                }
@@ -1578,9 +1537,12 @@ class User implements IDBAccessObject {
                foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
                        $defOpt[$langCode == $wgContLang->getCode() ? 'variant' : "variant-$langCode"] = $langCode;
                }
-               $namespaces = MediaWikiServices::getInstance()->getSearchEngineConfig()->searchableNamespaces();
-               foreach ( $namespaces as $nsnum => $nsname ) {
-                       $defOpt['searchNs' . $nsnum] = !empty( $wgNamespacesToBeSearchedDefault[$nsnum] );
+
+               // NOTE: don't use SearchEngineConfig::getSearchableNamespaces here,
+               // since extensions may change the set of searchable namespaces depending
+               // on user groups/permissions.
+               foreach ( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
+                       $defOpt['searchNs' . $nsnum] = (boolean)$val;
                }
                $defOpt['skin'] = Skin::normalizeKey( $wgDefaultSkin );
 
@@ -2510,31 +2472,7 @@ class User implements IDBAccessObject {
         * @return bool
         */
        public function setPassword( $str ) {
-               global $wgAuth, $wgDisableAuthManager;
-
-               if ( !$wgDisableAuthManager ) {
-                       return $this->setPasswordInternal( $str );
-               }
-
-               if ( $str !== null ) {
-                       if ( !$wgAuth->allowPasswordChange() ) {
-                               throw new PasswordError( wfMessage( 'password-change-forbidden' )->text() );
-                       }
-
-                       $status = $this->checkPasswordValidity( $str );
-                       if ( !$status->isGood() ) {
-                               throw new PasswordError( $status->getMessage()->text() );
-                       }
-               }
-
-               if ( !$wgAuth->setPassword( $this, $str ) ) {
-                       throw new PasswordError( wfMessage( 'externaldberror' )->text() );
-               }
-
-               $this->setOption( 'watchlisttoken', false );
-               $this->setPasswordInternal( $str );
-
-               return true;
+               return $this->setPasswordInternal( $str );
        }
 
        /**
@@ -2546,16 +2484,7 @@ class User implements IDBAccessObject {
         *  through the web interface.
         */
        public function setInternalPassword( $str ) {
-               global $wgAuth, $wgDisableAuthManager;
-
-               if ( !$wgDisableAuthManager ) {
-                       $this->setPasswordInternal( $str );
-               }
-
-               if ( $wgAuth->allowSetLocalPassword() ) {
-                       $this->setOption( 'watchlisttoken', false );
-                       $this->setPasswordInternal( $str );
-               }
+               $this->setPasswordInternal( $str );
        }
 
        /**
@@ -2567,55 +2496,26 @@ class User implements IDBAccessObject {
         * @return bool Success
         */
        private function setPasswordInternal( $str ) {
-               global $wgDisableAuthManager;
-
-               if ( $wgDisableAuthManager ) {
-                       $id = self::idFromName( $this->getName(), self::READ_LATEST );
-                       if ( $id == 0 ) {
-                               throw new LogicException( 'Cannot set a password for a user that is not in the database.' );
-                       }
-
-                       $passwordFactory = new PasswordFactory();
-                       $passwordFactory->init( RequestContext::getMain()->getConfig() );
-                       $dbw = wfGetDB( DB_MASTER );
-                       $dbw->update(
-                               'user',
-                               [
-                                       'user_password' => $passwordFactory->newFromPlaintext( $str )->toString(),
-                                       'user_newpassword' => PasswordFactory::newInvalidPassword()->toString(),
-                                       'user_newpass_time' => $dbw->timestampOrNull( null ),
-                               ],
-                               [
-                                       'user_id' => $id,
-                               ],
-                               __METHOD__
-                       );
-
-                       // When the main password is changed, invalidate all bot passwords too
-                       BotPassword::invalidateAllPasswordsForUser( $this->getName() );
-               } else {
-                       $manager = AuthManager::singleton();
-
-                       // If the user doesn't exist yet, fail
-                       if ( !$manager->userExists( $this->getName() ) ) {
-                               throw new LogicException( 'Cannot set a password for a user that is not in the database.' );
-                       }
+               $manager = AuthManager::singleton();
 
-                       $status = $this->changeAuthenticationData( [
-                               'username' => $this->getName(),
-                               'password' => $str,
-                               'retype' => $str,
-                       ] );
-                       if ( !$status->isGood() ) {
-                               \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
-                                       ->info( __METHOD__ . ': Password change rejected: '
-                                               . $status->getWikiText( null, null, 'en' ) );
-                               return false;
-                       }
+               // If the user doesn't exist yet, fail
+               if ( !$manager->userExists( $this->getName() ) ) {
+                       throw new LogicException( 'Cannot set a password for a user that is not in the database.' );
+               }
 
-                       $this->setOption( 'watchlisttoken', false );
+               $status = $this->changeAuthenticationData( [
+                       'username' => $this->getName(),
+                       'password' => $str,
+                       'retype' => $str,
+               ] );
+               if ( !$status->isGood() ) {
+                       \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
+                               ->info( __METHOD__ . ': Password change rejected: '
+                                       . $status->getWikiText( null, null, 'en' ) );
+                       return false;
                }
 
+               $this->setOption( 'watchlisttoken', false );
                SessionManager::singleton()->invalidateSessionsForUser( $this );
 
                return true;
@@ -2634,12 +2534,6 @@ class User implements IDBAccessObject {
         * @since 1.27
         */
        public function changeAuthenticationData( array $data ) {
-               global $wgDisableAuthManager;
-               if ( $wgDisableAuthManager ) {
-                       throw new LogicException( __METHOD__ . ' cannot be called when $wgDisableAuthManager '
-                               . 'is true' );
-               }
-
                $manager = AuthManager::singleton();
                $reqs = $manager->getAuthenticationRequests( AuthManager::ACTION_CHANGE, $this );
                $reqs = AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
@@ -2725,32 +2619,7 @@ class User implements IDBAccessObject {
         * @param bool $throttle If true, reset the throttle timestamp to the present
         */
        public function setNewpassword( $str, $throttle = true ) {
-               global $wgDisableAuthManager;
-
-               if ( $wgDisableAuthManager ) {
-                       $id = $this->getId();
-                       if ( $id == 0 ) {
-                               throw new LogicException( 'Cannot set new password for a user that is not in the database.' );
-                       }
-
-                       $dbw = wfGetDB( DB_MASTER );
-
-                       $passwordFactory = new PasswordFactory();
-                       $passwordFactory->init( RequestContext::getMain()->getConfig() );
-                       $update = [
-                               'user_newpassword' => $passwordFactory->newFromPlaintext( $str )->toString(),
-                       ];
-
-                       if ( $str === null ) {
-                               $update['user_newpass_time'] = null;
-                       } elseif ( $throttle ) {
-                               $update['user_newpass_time'] = $dbw->timestamp();
-                       }
-
-                       $dbw->update( 'user', $update, [ 'user_id' => $id ], __METHOD__ );
-               } else {
-                       throw new BadMethodCallException( __METHOD__ . ' has been removed in 1.27' );
-               }
+               throw new BadMethodCallException( __METHOD__ . ' has been removed in 1.27' );
        }
 
        /**
@@ -2760,33 +2629,7 @@ class User implements IDBAccessObject {
         * @return bool
         */
        public function isPasswordReminderThrottled() {
-               global $wgPasswordReminderResendTime, $wgDisableAuthManager;
-
-               if ( $wgDisableAuthManager ) {
-                       if ( !$wgPasswordReminderResendTime ) {
-                               return false;
-                       }
-
-                       $this->load();
-
-                       $db = ( $this->queryFlagsUsed & self::READ_LATEST )
-                               ? wfGetDB( DB_MASTER )
-                               : wfGetDB( DB_SLAVE );
-                       $newpassTime = $db->selectField(
-                               'user',
-                               'user_newpass_time',
-                               [ 'user_id' => $this->getId() ],
-                               __METHOD__
-                       );
-
-                       if ( $newpassTime === null ) {
-                               return false;
-                       }
-                       $expiry = wfTimestamp( TS_UNIX, $newpassTime ) + $wgPasswordReminderResendTime * 3600;
-                       return time() < $expiry;
-               } else {
-                       throw new BadMethodCallException( __METHOD__ . ' has been removed in 1.27' );
-               }
+               throw new BadMethodCallException( __METHOD__ . ' has been removed in 1.27' );
        }
 
        /**
@@ -3018,7 +2861,7 @@ class User implements IDBAccessObject {
         * @return string|bool User's current value for the option, or false if this option is disabled.
         * @see resetTokenFromOption()
         * @see getOption()
-        * @deprecated 1.26 Applications should use the OAuth extension
+        * @deprecated since 1.26 Applications should use the OAuth extension
         */
        public function getTokenFromOption( $oname ) {
                global $wgHiddenPrefs;
@@ -3304,6 +3147,22 @@ class User implements IDBAccessObject {
                        Hooks::run( 'UserGetRights', [ $this, &$this->mRights ] );
                        // Force reindexation of rights when a hook has unset one of them
                        $this->mRights = array_values( array_unique( $this->mRights ) );
+
+                       // If block disables login, we should also remove any
+                       // extra rights blocked users might have, in case the
+                       // blocked user has a pre-existing session (T129738).
+                       // This is checked here for cases where people only call
+                       // $user->isAllowed(). It is also checked in Title::checkUserBlock()
+                       // to give a better error message in the common case.
+                       $config = RequestContext::getMain()->getConfig();
+                       if (
+                               $this->isLoggedIn() &&
+                               $config->get( 'BlockDisablesLogin' ) &&
+                               $this->isBlocked()
+                       ) {
+                               $anon = new User;
+                               $this->mRights = array_intersect( $this->mRights, $anon->getRights() );
+                       }
                }
                return $this->mRights;
        }
@@ -3928,7 +3787,7 @@ class User implements IDBAccessObject {
                        ScopedCallback::consume( $delay );
                        $error = false;
                }
-               \MediaWiki\Logger\LoggerFactory::getInstance( 'authmanager' )->info( 'Logout', [
+               \MediaWiki\Logger\LoggerFactory::getInstance( 'authevents' )->info( 'Logout', [
                        'event' => 'logout',
                        'successful' => $error === false,
                        'status' => $error ?: 'success',
@@ -4282,87 +4141,27 @@ class User implements IDBAccessObject {
         * @return bool True if the given password is correct, otherwise False
         */
        public function checkPassword( $password ) {
-               global $wgAuth, $wgLegacyEncoding, $wgDisableAuthManager;
-
-               if ( $wgDisableAuthManager ) {
-                       $this->load();
-
-                       // Some passwords will give a fatal Status, which means there is
-                       // some sort of technical or security reason for this password to
-                       // be completely invalid and should never be checked (e.g., T64685)
-                       if ( !$this->checkPasswordValidity( $password )->isOK() ) {
-                               return false;
-                       }
-
-                       // 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 ( $wgAuth->authenticate( $this->getName(), $password ) ) {
+               $manager = AuthManager::singleton();
+               $reqs = AuthenticationRequest::loadRequestsFromSubmission(
+                       $manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN ),
+                       [
+                               'username' => $this->getName(),
+                               'password' => $password,
+                       ]
+               );
+               $res = AuthManager::singleton()->beginAuthentication( $reqs, 'null:' );
+               switch ( $res->status ) {
+                       case AuthenticationResponse::PASS:
                                return true;
-                       } elseif ( $wgAuth->strict() ) {
-                               // Auth plugin doesn't allow local authentication
-                               return false;
-                       } elseif ( $wgAuth->strictUserAuth( $this->getName() ) ) {
-                               // Auth plugin doesn't allow local authentication for this user name
+                       case AuthenticationResponse::FAIL:
+                               // Hope it's not a PreAuthenticationProvider that failed...
+                               \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
+                                       ->info( __METHOD__ . ': Authentication failed: ' . $res->message->plain() );
                                return false;
-                       }
-
-                       $passwordFactory = new PasswordFactory();
-                       $passwordFactory->init( RequestContext::getMain()->getConfig() );
-                       $db = ( $this->queryFlagsUsed & self::READ_LATEST )
-                               ? wfGetDB( DB_MASTER )
-                               : wfGetDB( DB_SLAVE );
-
-                       try {
-                               $mPassword = $passwordFactory->newFromCiphertext( $db->selectField(
-                                       'user', 'user_password', [ 'user_id' => $this->getId() ], __METHOD__
-                               ) );
-                       } catch ( PasswordError $e ) {
-                               wfDebug( 'Invalid password hash found in database.' );
-                               $mPassword = PasswordFactory::newInvalidPassword();
-                       }
-
-                       if ( !$mPassword->equals( $password ) ) {
-                               if ( $wgLegacyEncoding ) {
-                                       // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
-                                       // Check for this with iconv
-                                       $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
-                                       if ( $cp1252Password === $password || !$mPassword->equals( $cp1252Password ) ) {
-                                               return false;
-                                       }
-                               } else {
-                                       return false;
-                               }
-                       }
-
-                       if ( $passwordFactory->needsUpdate( $mPassword ) && !wfReadOnly() ) {
-                               $this->setPasswordInternal( $password );
-                       }
-
-                       return true;
-               } else {
-                       $manager = AuthManager::singleton();
-                       $reqs = AuthenticationRequest::loadRequestsFromSubmission(
-                               $manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN ),
-                               [
-                                       'username' => $this->getName(),
-                                       'password' => $password,
-                               ]
-                       );
-                       $res = AuthManager::singleton()->beginAuthentication( $reqs, 'null:' );
-                       switch ( $res->status ) {
-                               case AuthenticationResponse::PASS:
-                                       return true;
-                               case AuthenticationResponse::FAIL:
-                                       // Hope it's not a PreAuthenticationProvider that failed...
-                                       \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
-                                               ->info( __METHOD__ . ': Authentication failed: ' . $res->message->plain() );
-                                       return false;
-                               default:
-                                       throw new BadMethodCallException(
-                                               'AuthManager returned a response unsupported by ' . __METHOD__
-                                       );
-                       }
+                       default:
+                               throw new BadMethodCallException(
+                                       'AuthManager returned a response unsupported by ' . __METHOD__
+                               );
                }
        }
 
@@ -4375,43 +4174,8 @@ class User implements IDBAccessObject {
         * @return bool True if matches, false otherwise
         */
        public function checkTemporaryPassword( $plaintext ) {
-               global $wgNewPasswordExpiry, $wgDisableAuthManager;
-
-               if ( $wgDisableAuthManager ) {
-                       $this->load();
-
-                       $passwordFactory = new PasswordFactory();
-                       $passwordFactory->init( RequestContext::getMain()->getConfig() );
-                       $db = ( $this->queryFlagsUsed & self::READ_LATEST )
-                               ? wfGetDB( DB_MASTER )
-                               : wfGetDB( DB_SLAVE );
-
-                       $row = $db->selectRow(
-                               'user',
-                               [ 'user_newpassword', 'user_newpass_time' ],
-                               [ 'user_id' => $this->getId() ],
-                               __METHOD__
-                       );
-                       try {
-                               $newPassword = $passwordFactory->newFromCiphertext( $row->user_newpassword );
-                       } catch ( PasswordError $e ) {
-                               wfDebug( 'Invalid password hash found in database.' );
-                               $newPassword = PasswordFactory::newInvalidPassword();
-                       }
-
-                       if ( $newPassword->equals( $plaintext ) ) {
-                               if ( is_null( $row->user_newpass_time ) ) {
-                                       return true;
-                               }
-                               $expiry = wfTimestamp( TS_UNIX, $row->user_newpass_time ) + $wgNewPasswordExpiry;
-                               return ( time() < $expiry );
-                       } else {
-                               return false;
-                       }
-               } else {
-                       // Can't check the temporary password individually.
-                       return $this->checkPassword( $plaintext );
-               }
+               // Can't check the temporary password individually.
+               return $this->checkPassword( $plaintext );
        }
 
        /**
@@ -4442,6 +4206,8 @@ class User implements IDBAccessObject {
         * login credentials aren't being hijacked with a foreign form
         * submission.
         *
+        * The $salt for 'edit' and 'csrf' tokens is the default (empty string).
+        *
         * @since 1.19
         * @param string|array $salt Array of Strings Optional function-specific data for hashing
         * @param WebRequest|null $request WebRequest object to use or null to use $wgRequest
@@ -5161,12 +4927,20 @@ class User implements IDBAccessObject {
                                // If we actually have a slave server, the count is
                                // at least one behind because the current transaction
                                // has not been committed and replicated.
-                               $this->initEditCount( 1 );
+                               $this->mEditCount = $this->initEditCount( 1 );
                        } else {
                                // But if DB_SLAVE is selecting the master, then the
                                // count we just read includes the revision that was
                                // just added in the working transaction.
-                               $this->initEditCount();
+                               $this->mEditCount = $this->initEditCount();
+                       }
+               } else {
+                       if ( $this->mEditCount === null ) {
+                               $this->getEditCount();
+                               $dbr = wfGetDB( DB_SLAVE );
+                               $this->mEditCount += ( $dbr !== $dbw ) ? 1 : 0;
+                       } else {
+                               $this->mEditCount++;
                        }
                }
                // Edit count in user cache too
@@ -5280,45 +5054,10 @@ class User implements IDBAccessObject {
         *     - false will be converted to 'create' if this object is the same as
         *       $wgUser and to 'create2' otherwise
         * @param string $reason User supplied reason
-        * @return int|bool True if not $wgNewUserLog or not $wgDisableAuthManager;
-        *   otherwise ID of log item or 0 on failure
+        * @return bool true
         */
        public function addNewUserLogEntry( $action = false, $reason = '' ) {
-               global $wgUser, $wgNewUserLog, $wgDisableAuthManager;
-               if ( !$wgDisableAuthManager || empty( $wgNewUserLog ) ) {
-                       return true; // disabled
-               }
-
-               if ( $action === true ) {
-                       $action = 'byemail';
-               } elseif ( $action === false ) {
-                       if ( $this->equals( $wgUser ) ) {
-                               $action = 'create';
-                       } else {
-                               $action = 'create2';
-                       }
-               }
-
-               if ( $action === 'create' || $action === 'autocreate' ) {
-                       $performer = $this;
-               } else {
-                       $performer = $wgUser;
-               }
-
-               $logEntry = new ManualLogEntry( 'newusers', $action );
-               $logEntry->setPerformer( $performer );
-               $logEntry->setTarget( $this->getUserPage() );
-               $logEntry->setComment( $reason );
-               $logEntry->setParameters( [
-                       '4::userid' => $this->getId(),
-               ] );
-               $logid = $logEntry->insert();
-
-               if ( $action !== 'autocreate' ) {
-                       $logEntry->publish( $logid );
-               }
-
-               return (int)$logid;
+               return true; // disabled
        }
 
        /**