Merge "Introduce a BlockManager service"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 1 May 2019 18:26:59 +0000 (18:26 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 1 May 2019 18:26:59 +0000 (18:26 +0000)
1  2 
RELEASE-NOTES-1.34
includes/user/User.php
tests/phpunit/includes/user/UserTest.php

diff --combined RELEASE-NOTES-1.34
@@@ -107,10 -107,6 +107,10 @@@ because of Phabricator reports
  * wfArrayFilter() and wfArrayFilterByKey(), deprecated in 1.32, have been
    removed.
  * wfMakeUrlIndexes() function, deprecated in 1.33, have been removed.
 +* User::getGroupPage() and ::makeGroupLinkHTML(), deprecated in 1.29, have been
 +  removed. Use UserGroupMembership::getGroupPage and ::getLink instead.
 +* User::makeGroupLinkWiki(), deprecated in 1.29, has been removed. Use
 +  UserGroupMembership::getLink() instead.
  * …
  
  === Deprecations in 1.34 ===
  * User::isBlocked() is deprecated since it does not tell you if the user is
    blocked from editing a particular page. Use User::getBlock() or
    PermissionManager::isBlockedFrom() or PermissionManager::userCan() instead.
+ * User::isLocallyBlockedProxy and User::inDnsBlacklist are deprecated and moved
+   to the BlockManager as private helper methods.
+ * User::isDnsBlacklisted is deprecated. Use BlockManager::isDnsBlacklisted
+   instead.
  * …
  
  === Other changes in 1.34 ===
diff --combined includes/user/User.php
@@@ -1813,13 -1813,14 +1813,14 @@@ class User implements IDBAccessObject, 
  
        /**
         * Get blocking information
+        *
+        * TODO: Move this into the BlockManager, along with block-related properties.
+        *
         * @param bool $fromReplica Whether to check the replica DB first.
         *   To improve performance, non-critical checks are done against replica DBs.
         *   Check when actually saving should be done against master.
         */
        private function getBlockedStatus( $fromReplica = true ) {
-               global $wgProxyWhitelist, $wgApplyIpBlocksToXff, $wgSoftBlockRanges;
                if ( $this->mBlockedby != -1 ) {
                        return;
                }
                // overwriting mBlockedby, surely?
                $this->load();
  
-               # We only need to worry about passing the IP address to the Block generator if the
-               # user is not immune to autoblocks/hardblocks, and they are the current user so we
-               # know which IP address they're actually coming from
-               $ip = null;
-               $sessionUser = RequestContext::getMain()->getUser();
-               // the session user is set up towards the end of Setup.php. Until then,
-               // assume it's a logged-out user.
-               $globalUserName = $sessionUser->isSafeToLoad()
-                       ? $sessionUser->getName()
-                       : IP::sanitizeIP( $sessionUser->getRequest()->getIP() );
-               if ( $this->getName() === $globalUserName && !$this->isAllowed( 'ipblock-exempt' ) ) {
-                       $ip = $this->getRequest()->getIP();
-               }
-               // User/IP blocking
-               $block = Block::newFromTarget( $this, $ip, !$fromReplica );
-               // Cookie blocking
-               if ( !$block instanceof Block ) {
-                       $block = $this->getBlockFromCookieValue( $this->getRequest()->getCookie( 'BlockID' ) );
-               }
-               // Proxy blocking
-               if ( !$block instanceof Block && $ip !== null && !in_array( $ip, $wgProxyWhitelist ) ) {
-                       // Local list
-                       if ( self::isLocallyBlockedProxy( $ip ) ) {
-                               $block = new Block( [
-                                       'byText' => wfMessage( 'proxyblocker' )->text(),
-                                       'reason' => wfMessage( 'proxyblockreason' )->plain(),
-                                       'address' => $ip,
-                                       'systemBlock' => 'proxy',
-                               ] );
-                       } elseif ( $this->isAnon() && $this->isDnsBlacklisted( $ip ) ) {
-                               $block = new Block( [
-                                       'byText' => wfMessage( 'sorbs' )->text(),
-                                       'reason' => wfMessage( 'sorbsreason' )->plain(),
-                                       'address' => $ip,
-                                       'systemBlock' => 'dnsbl',
-                               ] );
-                       }
-               }
-               // (T25343) Apply IP blocks to the contents of XFF headers, if enabled
-               if ( !$block instanceof Block
-                       && $wgApplyIpBlocksToXff
-                       && $ip !== null
-                       && !in_array( $ip, $wgProxyWhitelist )
-               ) {
-                       $xff = $this->getRequest()->getHeader( 'X-Forwarded-For' );
-                       $xff = array_map( 'trim', explode( ',', $xff ) );
-                       $xff = array_diff( $xff, [ $ip ] );
-                       $xffblocks = Block::getBlocksForIPList( $xff, $this->isAnon(), !$fromReplica );
-                       $block = Block::chooseBlock( $xffblocks, $xff );
-                       if ( $block instanceof Block ) {
-                               # Mangle the reason to alert the user that the block
-                               # originated from matching the X-Forwarded-For header.
-                               $block->setReason( wfMessage( 'xffblockreason', $block->getReason() )->plain() );
-                       }
-               }
-               if ( !$block instanceof Block
-                       && $ip !== null
-                       && $this->isAnon()
-                       && IP::isInRanges( $ip, $wgSoftBlockRanges )
-               ) {
-                       $block = new Block( [
-                               'address' => $ip,
-                               'byText' => 'MediaWiki default',
-                               'reason' => wfMessage( 'softblockrangesreason', $ip )->plain(),
-                               'anonOnly' => true,
-                               'systemBlock' => 'wgSoftBlockRanges',
-                       ] );
-               }
+               $block = MediaWikiServices::getInstance()->getBlockManager()->getUserBlock(
+                       $this,
+                       $fromReplica
+               );
  
                if ( $block instanceof Block ) {
                        wfDebug( __METHOD__ . ": Found block.\n" );
                Hooks::run( 'GetBlockedStatus', [ &$thisUser ] );
        }
  
-       /**
-        * Try to load a Block from an ID given in a cookie value.
-        * @param string|null $blockCookieVal The cookie value to check.
-        * @return Block|bool The Block object, or false if none could be loaded.
-        */
-       protected function getBlockFromCookieValue( $blockCookieVal ) {
-               // Make sure there's something to check. The cookie value must start with a number.
-               if ( strlen( $blockCookieVal ) < 1 || !is_numeric( substr( $blockCookieVal, 0, 1 ) ) ) {
-                       return false;
-               }
-               // Load the Block from the ID in the cookie.
-               $blockCookieId = Block::getIdFromCookieValue( $blockCookieVal );
-               if ( $blockCookieId !== null ) {
-                       // An ID was found in the cookie.
-                       $tmpBlock = Block::newFromID( $blockCookieId );
-                       if ( $tmpBlock instanceof Block ) {
-                               $config = RequestContext::getMain()->getConfig();
-                               switch ( $tmpBlock->getType() ) {
-                                       case Block::TYPE_USER:
-                                               $blockIsValid = !$tmpBlock->isExpired() && $tmpBlock->isAutoblocking();
-                                               $useBlockCookie = ( $config->get( 'CookieSetOnAutoblock' ) === true );
-                                               break;
-                                       case Block::TYPE_IP:
-                                       case Block::TYPE_RANGE:
-                                               // If block is type IP or IP range, load only if user is not logged in (T152462)
-                                               $blockIsValid = !$tmpBlock->isExpired() && !$this->isLoggedIn();
-                                               $useBlockCookie = ( $config->get( 'CookieSetOnIpBlock' ) === true );
-                                               break;
-                                       default:
-                                               $blockIsValid = false;
-                                               $useBlockCookie = false;
-                               }
-                               if ( $blockIsValid && $useBlockCookie ) {
-                                       // Use the block.
-                                       return $tmpBlock;
-                               }
-                               // If the block is not valid, remove the cookie.
-                               Block::clearCookie( $this->getRequest()->response() );
-                       } else {
-                               // If the block doesn't exist, remove the cookie.
-                               Block::clearCookie( $this->getRequest()->response() );
-                       }
-               }
-               return false;
-       }
        /**
         * Whether the given IP is in a DNS blacklist.
         *
+        * @deprecated since 1.34 Use BlockManager::isDnsBlacklisted.
         * @param string $ip IP to check
         * @param bool $checkWhitelist Whether to check the whitelist first
         * @return bool True if blacklisted.
         */
        public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
-               global $wgEnableDnsBlacklist, $wgDnsBlacklistUrls, $wgProxyWhitelist;
-               if ( !$wgEnableDnsBlacklist ||
-                       ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) )
-               ) {
-                       return false;
-               }
-               return $this->inDnsBlacklist( $ip, $wgDnsBlacklistUrls );
+               return MediaWikiServices::getInstance()->getBlockManager()
+                       ->isDnsBlacklisted( $ip, $checkWhitelist );
        }
  
        /**
         * Whether the given IP is in a given DNS blacklist.
         *
+        * @deprecated since 1.34 Check via BlockManager::isDnsBlacklisted instead.
         * @param string $ip IP to check
         * @param string|array $bases Array of Strings: URL of the DNS blacklist
         * @return bool True if blacklisted.
         */
        public function inDnsBlacklist( $ip, $bases ) {
+               wfDeprecated( __METHOD__, '1.34' );
                $found = false;
                // @todo FIXME: IPv6 ???  (https://bugs.php.net/bug.php?id=33170)
                if ( IP::isIPv4( $ip ) ) {
        /**
         * Check if an IP address is in the local proxy list
         *
+        * @deprecated since 1.34 Use BlockManager::getUserBlock instead.
         * @param string $ip
-        *
         * @return bool
         */
        public static function isLocallyBlockedProxy( $ip ) {
+               wfDeprecated( __METHOD__, '1.34' );
                global $wgProxyList;
  
                if ( !$wgProxyList ) {
                $newTouched = $this->newTouchedTimestamp();
  
                $dbw = wfGetDB( DB_MASTER );
 -              $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) use ( $newTouched ) {
 +              $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) use ( $newTouched ) {
                        global $wgActorTableSchemaMigrationStage;
  
                        $dbw->update( 'user',
                        $fields["user_$name"] = $value;
                }
  
 -              return $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) use ( $fields ) {
 +              return $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) use ( $fields ) {
                        $dbw->insert( 'user', $fields, $fname, [ 'IGNORE' ] );
                        if ( $dbw->affectedRows() ) {
                                $newUser = self::newFromId( $dbw->insertId() );
                $this->mTouched = $this->newTouchedTimestamp();
  
                $dbw = wfGetDB( DB_MASTER );
 -              $status = $dbw->doAtomicSection( __METHOD__, function ( $dbw, $fname ) {
 +              $status = $dbw->doAtomicSection( __METHOD__, function ( IDatabase $dbw, $fname ) {
                        $noPass = PasswordFactory::newInvalidPassword()->toString();
                        $dbw->insert( 'user',
                                [
                return $wgImplicitGroups;
        }
  
 -      /**
 -       * Get the title of a page describing a particular group
 -       * @deprecated since 1.29 Use UserGroupMembership::getGroupPage instead
 -       *
 -       * @param string $group Internal group name
 -       * @return Title|bool Title of the page if it exists, false otherwise
 -       */
 -      public static function getGroupPage( $group ) {
 -              wfDeprecated( __METHOD__, '1.29' );
 -              return UserGroupMembership::getGroupPage( $group );
 -      }
 -
 -      /**
 -       * Create a link to the group in HTML, if available;
 -       * else return the group name.
 -       * @deprecated since 1.29 Use UserGroupMembership::getLink instead, or
 -       * make the link yourself if you need custom text
 -       *
 -       * @param string $group Internal name of the group
 -       * @param string $text The text of the link
 -       * @return string HTML link to the group
 -       */
 -      public static function makeGroupLinkHTML( $group, $text = '' ) {
 -              wfDeprecated( __METHOD__, '1.29' );
 -
 -              if ( $text == '' ) {
 -                      $text = UserGroupMembership::getGroupName( $group );
 -              }
 -              $title = UserGroupMembership::getGroupPage( $group );
 -              if ( $title ) {
 -                      return MediaWikiServices::getInstance()
 -                              ->getLinkRenderer()->makeLink( $title, $text );
 -              }
 -
 -              return htmlspecialchars( $text );
 -      }
 -
 -      /**
 -       * Create a link to the group in Wikitext, if available;
 -       * else return the group name.
 -       * @deprecated since 1.29 Use UserGroupMembership::getLink instead, or
 -       * make the link yourself if you need custom text
 -       *
 -       * @param string $group Internal name of the group
 -       * @param string $text The text of the link
 -       * @return string Wikilink to the group
 -       */
 -      public static function makeGroupLinkWiki( $group, $text = '' ) {
 -              wfDeprecated( __METHOD__, '1.29' );
 -
 -              if ( $text == '' ) {
 -                      $text = UserGroupMembership::getGroupName( $group );
 -              }
 -              $title = UserGroupMembership::getGroupPage( $group );
 -              if ( $title ) {
 -                      $page = $title->getFullText();
 -                      return "[[$page|$text]]";
 -              }
 -
 -              return $text;
 -      }
 -
        /**
         * Returns an array of the groups that a particular group can add/remove.
         *
@@@ -28,7 -28,7 +28,7 @@@ class UserTest extends MediaWikiTestCas
                $this->setMwGlobals( [
                        'wgGroupPermissions' => [],
                        'wgRevokePermissions' => [],
 -                      'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
 +                      'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
                ] );
                $this->overrideMwServices();
  
                        RequestContext::getMain()->setRequest( $request );
                        TestingAccessWrapper::newFromObject( $user )->mRequest = $request;
                        $request->getSession()->setUser( $user );
+                       $this->overrideMwServices();
                };
                $this->setMwGlobals( 'wgSoftBlockRanges', [ '10.0.0.0/8' ] );
  
                $this->assertFalse( $user->getExperienceLevel() );
        }
  
-       public static function provideIsLocallBlockedProxy() {
+       public static function provideIsLocallyBlockedProxy() {
                return [
                        [ '1.2.3.4', '1.2.3.4' ],
                        [ '1.2.3.4', '1.2.3.0/16' ],
        }
  
        /**
-        * @dataProvider provideIsLocallBlockedProxy
+        * @dataProvider provideIsLocallyBlockedProxy
         * @covers User::isLocallyBlockedProxy
         */
        public function testIsLocallyBlockedProxy( $ip, $blockListEntry ) {
+               $this->hideDeprecated( 'User::isLocallyBlockedProxy' );
                $this->setMwGlobals(
                        'wgProxyList', []
                );
                $user = User::newFromId( $id );
                $this->assertTrue( $user->getActorId() > 0, 'Actor ID can be retrieved for user loaded by ID' );
  
 +              $user2 = User::newFromActorId( $user->getActorId() );
 +              $this->assertEquals( $user->getId(), $user2->getId(),
 +                      'User::newFromActorId works for an existing user' );
 +
 +              $row = $this->db->selectRow( 'user', User::selectFields(), [ 'user_id' => $id ], __METHOD__ );
 +              $user = User::newFromRow( $row );
 +              $this->assertTrue( $user->getActorId() > 0,
 +                      'Actor ID can be retrieved for user loaded with User::selectFields()' );
 +
 +              $user = User::newFromId( $id );
 +              $user->setName( 'UserTestActorId4-renamed' );
 +              $user->saveSettings();
 +              $this->assertEquals(
 +                      $user->getName(),
 +                      $this->db->selectField(
 +                              'actor', 'actor_name', [ 'actor_id' => $user->getActorId() ], __METHOD__
 +                      ),
 +                      'User::saveSettings updates actor table for name change'
 +              );
 +
 +              // For sanity
 +              $ip = '192.168.12.34';
 +              $this->db->delete( 'actor', [ 'actor_name' => $ip ], __METHOD__ );
 +
 +              $user = User::newFromName( $ip, false );
 +              $this->assertFalse( $user->getActorId() > 0, 'Anonymous user has no actor ID by default' );
 +              $this->assertTrue( $user->getActorId( $this->db ) > 0,
 +                      'Actor ID can be created for an anonymous user' );
 +
 +              $user = User::newFromName( $ip, false );
 +              $this->assertTrue( $user->getActorId() > 0, 'Actor ID can be loaded for an anonymous user' );
 +              $user2 = User::newFromActorId( $user->getActorId() );
 +              $this->assertEquals( $user->getName(), $user2->getName(),
 +                      'User::newFromActorId works for an anonymous user' );
 +      }
 +
 +      /**
 +       * Actor tests with SCHEMA_COMPAT_READ_OLD
 +       *
 +       * The only thing different from testActorId() is the behavior if the actor
 +       * row doesn't exist in the DB, since with SCHEMA_COMPAT_READ_NEW that
 +       * situation can't happen. But we copy all the other tests too just for good measure.
 +       *
 +       * @covers User::newFromActorId
 +       */
 +      public function testActorId_old() {
 +              $this->setMwGlobals( [
 +                      'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
 +              ] );
 +              $this->overrideMwServices();
 +
 +              $domain = MediaWikiServices::getInstance()->getDBLoadBalancer()->getLocalDomainID();
 +              $this->hideDeprecated( 'User::selectFields' );
 +
 +              // Newly-created user has an actor ID
 +              $user = User::createNew( 'UserTestActorIdOld1' );
 +              $id = $user->getId();
 +              $this->assertTrue( $user->getActorId() > 0, 'User::createNew sets an actor ID' );
 +
 +              $user = User::newFromName( 'UserTestActorIdOld2' );
 +              $user->addToDatabase();
 +              $this->assertTrue( $user->getActorId() > 0, 'User::addToDatabase sets an actor ID' );
 +
 +              $user = User::newFromName( 'UserTestActorIdOld1' );
 +              $this->assertTrue( $user->getActorId() > 0, 'Actor ID can be retrieved for user loaded by name' );
 +
 +              $user = User::newFromId( $id );
 +              $this->assertTrue( $user->getActorId() > 0, 'Actor ID can be retrieved for user loaded by ID' );
 +
                $user2 = User::newFromActorId( $user->getActorId() );
                $this->assertEquals( $user->getId(), $user2->getId(),
                        'User::newFromActorId works for an existing user' );
                $this->assertFalse( $user->getActorId() > 0, 'No Actor ID by default if none in database' );
                $this->assertTrue( $user->getActorId( $this->db ) > 0, 'Actor ID can be created if none in db' );
  
 -              $user->setName( 'UserTestActorId4-renamed' );
 +              $user->setName( 'UserTestActorIdOld4-renamed' );
                $user->saveSettings();
                $this->assertEquals(
                        $user->getName(),