Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
62 / 62
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
GlobalBlockLocalStatusManager
100.00% covered (success)
100.00%
62 / 62
100.00% covered (success)
100.00%
4 / 4
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 locallyDisableBlock
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
1 / 1
4
 locallyEnableBlock
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
4
 addLogEntry
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\GlobalBlocking\Services;
4
5use ManualLogEntry;
6use MediaWiki\Config\ServiceOptions;
7use MediaWiki\Title\Title;
8use MediaWiki\User\CentralId\CentralIdLookup;
9use MediaWiki\User\UserIdentity;
10use StatusValue;
11use Wikimedia\Rdbms\IConnectionProvider;
12
13class GlobalBlockLocalStatusManager {
14
15    public const CONSTRUCTOR_OPTIONS = [
16        'GlobalBlockingAllowGlobalAccountBlocks',
17    ];
18
19    private ServiceOptions $options;
20    private GlobalBlockLocalStatusLookup $globalBlockLocalStatusLookup;
21    private GlobalBlockLookup $globalBlockLookup;
22    private GlobalBlockingBlockPurger $globalBlockingBlockPurger;
23    private GlobalBlockingConnectionProvider $globalBlockingConnectionProvider;
24    private IConnectionProvider $localDbProvider;
25    private CentralIdLookup $centralIdLookup;
26
27    public function __construct(
28        ServiceOptions $options,
29        GlobalBlockLocalStatusLookup $globalBlockLocalStatusLookup,
30        GlobalBlockLookup $globalBlockLookup,
31        GlobalBlockingBlockPurger $globalBlockingBlockPurger,
32        GlobalBlockingConnectionProvider $globalBlockingConnectionProvider,
33        IConnectionProvider $localDbProvider,
34        CentralIdLookup $centralIdLookup
35    ) {
36        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
37        $this->options = $options;
38        $this->globalBlockLocalStatusLookup = $globalBlockLocalStatusLookup;
39        $this->globalBlockLookup = $globalBlockLookup;
40        $this->globalBlockingBlockPurger = $globalBlockingBlockPurger;
41        $this->globalBlockingConnectionProvider = $globalBlockingConnectionProvider;
42        $this->localDbProvider = $localDbProvider;
43        $this->centralIdLookup = $centralIdLookup;
44    }
45
46    /**
47     * Disable a global block applying to users on a given wiki.
48     *
49     * @param string $target The specific target of the block being disabled on this wiki. Can be an IP or IP range.
50     * @param string $reason The reason for locally disabling the block.
51     * @param UserIdentity $performer The user who is locally disabling the block. The caller of this method is
52     *     responsible for determining if the performer has the necessary rights to perform the action.
53     * @param string|false $wikiId The wiki where the block should be modified. Use false for the local wiki.
54     * @return StatusValue
55     */
56    public function locallyDisableBlock(
57        string $target, string $reason, UserIdentity $performer, $wikiId = false
58    ): StatusValue {
59        // We need to purge expired blocks so we can be sure that the block we are locally disabling isn't
60        // already expired.
61        $this->globalBlockingBlockPurger->purgeExpiredBlocks();
62
63        // Check that a block exists on the given $target.
64        $globalBlockId = $this->globalBlockLookup->getGlobalBlockId( $target );
65        if ( !$globalBlockId ) {
66            $errorMessageKey = $this->options->get( 'GlobalBlockingAllowGlobalAccountBlocks' ) ?
67                'globalblocking-notblocked-new' : 'globalblocking-notblocked';
68            return StatusValue::newFatal( $errorMessageKey, $target );
69        }
70
71        // Assert that the block is not already locally disabled.
72        $localWhitelistInfo = $this->globalBlockLocalStatusLookup
73            ->getLocalWhitelistInfo( $globalBlockId, null, $wikiId );
74        if ( $localWhitelistInfo !== false ) {
75            return StatusValue::newFatal( 'globalblocking-whitelist-nochange', $target );
76        }
77
78        // Find the expiry of the block. This is important so that we can store it in the
79        // global_block_whitelist table, which allows us to purge it when the block has expired.
80        $expiry = $this->globalBlockingConnectionProvider->getReplicaGlobalBlockingDatabase()->newSelectQueryBuilder()
81            ->select( 'gb_expiry' )
82            ->from( 'globalblocks' )
83            ->where( [ 'gb_id' => $globalBlockId ] )
84            ->caller( __METHOD__ )
85            ->fetchField();
86
87        $this->localDbProvider->getPrimaryDatabase( $wikiId )->newInsertQueryBuilder()
88            ->insertInto( 'global_block_whitelist' )
89            ->row( [
90                'gbw_by' => $performer->getId(),
91                'gbw_by_text' => $performer->getName(),
92                'gbw_reason' => trim( $reason ),
93                'gbw_address' => $target,
94                'gbw_target_central_id' => $this->centralIdLookup
95                    ->centralIdFromName( $target, CentralIdLookup::AUDIENCE_RAW ),
96                'gbw_expiry' => $expiry,
97                'gbw_id' => $globalBlockId
98            ] )
99            ->caller( __METHOD__ )
100            ->execute();
101
102        $this->addLogEntry( 'whitelist', $target, $reason, $performer );
103        return StatusValue::newGood( [ 'id' => $globalBlockId ] );
104    }
105
106    /**
107     * @param string $target The specific target of the block being enabled on this wiki. Can be an IP or IP range.
108     * @param string $reason The reason for enabling the block.
109     * @param UserIdentity $performer The user who is locally enabling the block. The caller of this method is
110     *    responsible for determining if the performer has the necessary rights to perform the action.
111     * @param string|false $wikiId The wiki where the block should be modified. Use false for the local wiki.
112     * @return StatusValue
113     */
114    public function locallyEnableBlock(
115        string $target, string $reason, UserIdentity $performer, $wikiId = false
116    ): StatusValue {
117        // Only allow locally re-enabling a global block if the global block exists.
118        $globalBlockId = $this->globalBlockLookup->getGlobalBlockId( $target );
119        if ( !$globalBlockId ) {
120            $errorMessageKey = $this->options->get( 'GlobalBlockingAllowGlobalAccountBlocks' ) ?
121                'globalblocking-notblocked-new' : 'globalblocking-notblocked';
122            return StatusValue::newFatal( $errorMessageKey, $target );
123        }
124
125        // Assert that the block is locally disabled.
126        $localWhitelistInfo = $this->globalBlockLocalStatusLookup
127            ->getLocalWhitelistInfo( $globalBlockId, null, $wikiId );
128        if ( $localWhitelistInfo === false ) {
129            return StatusValue::newFatal( 'globalblocking-whitelist-nochange', $target );
130        }
131
132        // Locally re-enable the block by removing the associated global_block_whitelist row.
133        $this->localDbProvider->getPrimaryDatabase( $wikiId )->newDeleteQueryBuilder()
134            ->deleteFrom( 'global_block_whitelist' )
135            ->where( [ 'gbw_id' => $globalBlockId ] )
136            ->caller( __METHOD__ )
137            ->execute();
138
139        $this->addLogEntry( 'dwhitelist', $target, $reason, $performer );
140        return StatusValue::newGood( [ 'id' => $globalBlockId ] );
141    }
142
143    /**
144     * Add a log entry for the change of the local status of a global block.
145     *
146     * @param string $action either 'whitelist' or 'dwhitelist'
147     * @param string $target Target IP, range, or username.
148     * @param string $reason The reason for the local status change.
149     */
150    protected function addLogEntry( string $action, string $target, string $reason, UserIdentity $performer ) {
151        $logEntry = new ManualLogEntry( 'gblblock', $action );
152        $logEntry->setTarget( Title::makeTitleSafe( NS_USER, $target ) );
153        $logEntry->setComment( $reason );
154        $logEntry->setPerformer( $performer );
155        $logId = $logEntry->insert();
156        $logEntry->publish( $logId );
157    }
158}