Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
69.49% covered (warning)
69.49%
123 / 177
28.57% covered (danger)
28.57%
2 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialBlockList
69.89% covered (warning)
69.89%
123 / 176
28.57% covered (danger)
28.57%
2 / 7
74.38
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 execute
95.77% covered (success)
95.77%
68 / 71
0.00% covered (danger)
0.00%
0 / 1
5
 getBlockListPager
79.63% covered (warning)
79.63%
43 / 54
0.00% covered (danger)
0.00%
0 / 1
20.74
 getTargetConds
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 showList
32.26% covered (danger)
32.26%
10 / 31
0.00% covered (danger)
0.00%
0 / 1
17.19
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDB
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Specials;
8
9use InvalidArgumentException;
10use MediaWiki\Block\AutoBlockTarget;
11use MediaWiki\Block\BlockActionInfo;
12use MediaWiki\Block\BlockRestrictionStore;
13use MediaWiki\Block\BlockTarget;
14use MediaWiki\Block\BlockTargetFactory;
15use MediaWiki\Block\BlockTargetWithIp;
16use MediaWiki\Block\DatabaseBlockStore;
17use MediaWiki\Block\HideUserUtils;
18use MediaWiki\Block\UserBlockTarget;
19use MediaWiki\CommentFormatter\RowCommentFormatter;
20use MediaWiki\CommentStore\CommentStore;
21use MediaWiki\Html\Html;
22use MediaWiki\HTMLForm\HTMLForm;
23use MediaWiki\Page\LinkBatchFactory;
24use MediaWiki\Pager\BlockListPager;
25use MediaWiki\Parser\ParserOptions;
26use MediaWiki\SpecialPage\SpecialPage;
27use MediaWiki\User\TempUser\TempUserConfig;
28use Wikimedia\Rdbms\IConnectionProvider;
29use Wikimedia\Rdbms\IExpression;
30use Wikimedia\Rdbms\IReadableDatabase;
31
32/**
33 * List of existing blocks
34 *
35 * @see SpecialBlock
36 * @see SpecialAutoblockList
37 * @ingroup SpecialPage
38 */
39class SpecialBlockList extends SpecialPage {
40    /** @var string */
41    protected $target;
42
43    /** @var array */
44    protected $options;
45
46    /** @var string|null */
47    protected $blockType;
48
49    public function __construct(
50        private readonly LinkBatchFactory $linkBatchFactory,
51        private readonly DatabaseBlockStore $blockStore,
52        private readonly BlockRestrictionStore $blockRestrictionStore,
53        private readonly IConnectionProvider $dbProvider,
54        private readonly CommentStore $commentStore,
55        private readonly BlockTargetFactory $blockTargetFactory,
56        private readonly HideUserUtils $hideUserUtils,
57        private readonly BlockActionInfo $blockActionInfo,
58        private readonly RowCommentFormatter $rowCommentFormatter,
59        private readonly TempUserConfig $tempUserConfig
60    ) {
61        parent::__construct( 'BlockList' );
62    }
63
64    /**
65     * @param string|null $par Title fragment
66     */
67    public function execute( $par ) {
68        $this->setHeaders();
69        $this->outputHeader();
70        $this->addHelpLink( 'Help:Blocking_users' );
71        $out = $this->getOutput();
72        $out->setPageTitleMsg( $this->msg( 'ipblocklist' ) );
73        $out->addModuleStyles( [ 'mediawiki.special' ] );
74
75        $request = $this->getRequest();
76        $par = $request->getVal( 'ip', $par ?? '' );
77        $this->target = trim( $request->getVal( 'wpTarget', $par ) );
78
79        $this->options = $request->getArray( 'wpOptions', [] );
80        $this->blockType = $request->getVal( 'blockType' );
81
82        $action = $request->getText( 'action' );
83
84        if ( $action == 'unblock' || ( $action == 'submit' && $request->wasPosted() ) ) {
85            // B/C @since 1.18: Unblock interface is now at Special:Unblock
86            $title = $this->getSpecialPageFactory()->getTitleForAlias( 'Unblock/' . $this->target );
87            $out->redirect( $title->getFullURL() );
88
89            return;
90        }
91
92        // Setup BlockListPager here to get the actual default Limit
93        $pager = $this->getBlockListPager();
94
95        $blockFilterOptions = [
96            'blocklist-tempblocks' => 'tempblocks',
97            'blocklist-indefblocks' => 'indefblocks',
98            'blocklist-autoblocks' => 'autoblocks',
99            'blocklist-addressblocks' => 'addressblocks',
100            'blocklist-rangeblocks' => 'rangeblocks',
101        ];
102
103        if ( $this->tempUserConfig->isKnown() ) {
104            // Clarify that "userblocks" excludes named users only if temporary accounts are known (T380266)
105            $blockFilterOptions['blocklist-nameduserblocks'] = 'userblocks';
106            $blockFilterOptions['blocklist-tempuserblocks'] = 'tempuserblocks';
107        } else {
108            $blockFilterOptions['blocklist-userblocks'] = 'userblocks';
109        }
110
111        // Just show the block list
112        $fields = [
113            'Target' => [
114                'type' => 'user',
115                'label-message' => 'ipaddressorusername',
116                'tabindex' => '1',
117                'size' => '45',
118                'default' => $this->target,
119            ],
120            'Options' => [
121                'type' => 'multiselect',
122                'options-messages' => $blockFilterOptions,
123                'flatlist' => true,
124            ],
125        ];
126
127        $fields['BlockType'] = [
128            'type' => 'select',
129            'label-message' => 'blocklist-type',
130            'options' => [
131                $this->msg( 'blocklist-type-opt-all' )->escaped() => '',
132                $this->msg( 'blocklist-type-opt-sitewide' )->escaped() => 'sitewide',
133                $this->msg( 'blocklist-type-opt-partial' )->escaped() => 'partial',
134            ],
135            'name' => 'blockType',
136            'cssclass' => 'mw-field-block-type',
137        ];
138
139        $fields['Limit'] = [
140            'type' => 'limitselect',
141            'label-message' => 'table_pager_limit_label',
142            'options' => $pager->getLimitSelectList(),
143            'name' => 'limit',
144            'default' => $pager->getLimit(),
145            'cssclass' => 'mw-field-limit mw-has-field-block-type',
146        ];
147
148        $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
149        $form
150            ->setMethod( 'get' )
151            ->setTitle( $this->getPageTitle() ) // Remove subpage
152            ->setFormIdentifier( 'blocklist' )
153            ->setWrapperLegendMsg( 'ipblocklist-legend' )
154            ->setSubmitTextMsg( 'ipblocklist-submit' )
155            ->prepareForm()
156            ->displayForm( false );
157
158        $this->showList( $pager );
159    }
160
161    /**
162     * Setup a new BlockListPager instance.
163     * @return BlockListPager
164     */
165    protected function getBlockListPager() {
166        $conds = [];
167        $db = $this->getDB();
168
169        // Add target conditions
170        if ( $this->target !== '' ) {
171            $target = $this->blockTargetFactory->newFromString( $this->target );
172            if ( $target ) {
173                $conds = $this->getTargetConds( $target );
174            }
175        }
176
177        // Apply filters
178        if ( in_array( 'userblocks', $this->options ) ) {
179            $namedUserConds = $db->expr( 'bt_user', '=', null );
180
181            // If temporary accounts are a known concept on this wiki,
182            // have the "Hide account blocks" filter exclude only named users (T380266).
183            if ( $this->tempUserConfig->isKnown() ) {
184                $namedUserConds = $namedUserConds->orExpr(
185                    $this->tempUserConfig->getMatchCondition( $db, 'bt_user_text', IExpression::LIKE )
186                );
187            }
188
189            $conds[] = $namedUserConds;
190        }
191        if ( in_array( 'autoblocks', $this->options ) ) {
192            $conds['bl_parent_block_id'] = null;
193        }
194        if ( in_array( 'addressblocks', $this->options )
195            && in_array( 'rangeblocks', $this->options )
196        ) {
197            // Simpler conditions for only user blocks (T360864)
198            $conds[] = $db->expr( 'bt_user', '!=', null );
199        } elseif ( in_array( 'addressblocks', $this->options ) ) {
200            $conds[] = $db->expr( 'bt_user', '!=', null )->or( 'bt_range_start', '!=', null );
201        } elseif ( in_array( 'rangeblocks', $this->options ) ) {
202            $conds['bt_range_start'] = null;
203        }
204
205        if (
206            in_array( 'tempuserblocks', $this->options ) &&
207            $this->tempUserConfig->isKnown()
208        ) {
209            $conds[] = $db->expr( 'bt_user', '=', null )
210                ->orExpr(
211                    $this->tempUserConfig->getMatchCondition( $db, 'bt_user_text', IExpression::NOT_LIKE )
212                );
213        }
214
215        $hideTemp = in_array( 'tempblocks', $this->options );
216        $hideIndef = in_array( 'indefblocks', $this->options );
217        if ( $hideTemp && $hideIndef ) {
218            // If both types are hidden, ensure query doesn't produce any results
219            $conds[] = '1=0';
220        } elseif ( $hideTemp ) {
221            $conds['bl_expiry'] = $db->getInfinity();
222        } elseif ( $hideIndef ) {
223            $conds[] = $db->expr( 'bl_expiry', '!=', $db->getInfinity() );
224        }
225
226        if ( $this->blockType === 'sitewide' ) {
227            $conds['bl_sitewide'] = 1;
228        } elseif ( $this->blockType === 'partial' ) {
229            $conds['bl_sitewide'] = 0;
230        }
231
232        return new BlockListPager(
233            $this->getContext(),
234            $this->blockActionInfo,
235            $this->blockRestrictionStore,
236            $this->blockTargetFactory,
237            $this->hideUserUtils,
238            $this->commentStore,
239            $this->linkBatchFactory,
240            $this->getLinkRenderer(),
241            $this->dbProvider,
242            $this->rowCommentFormatter,
243            $this->getSpecialPageFactory(),
244            $conds
245        );
246    }
247
248    /**
249     * Get conditions matching a parsed block target.
250     *
251     * The details are different from other similarly named functions elsewhere:
252     *   - If an IP address or range is requested, autoblocks are not shown.
253     *   - Requests for single IP addresses include range blocks covering the
254     *     address. This is like a "vague target" query in DatabaseBlockStore,
255     *     except that autoblocks are excluded.
256     *   - If a named user doesn't exist, it is assumed that there are no blocks.
257     *
258     * @param BlockTarget $target
259     * @return array
260     */
261    private function getTargetConds( BlockTarget $target ) {
262        if ( $target instanceof AutoBlockTarget ) {
263            return [ 'bl_id' => $target->getId() ];
264        }
265        if ( $target instanceof BlockTargetWithIp ) {
266            $range = $target->toHexRange();
267            return [
268                $this->blockStore->getRangeCond( $range[0], $range[1] ),
269                'bt_auto' => 0
270            ];
271        }
272        if ( $target instanceof UserBlockTarget ) {
273            $user = $target->getUserIdentity();
274            if ( $user->getId() ) {
275                return [
276                    'bt_user' => $user->getId(),
277                    'bt_auto' => 0
278                ];
279            } else {
280                // No such user
281                return [ '1=0' ];
282            }
283        }
284        throw new InvalidArgumentException( 'Invalid block target type' );
285    }
286
287    /**
288     * Show the list of blocked accounts matching the actual filter.
289     * @param BlockListPager $pager The BlockListPager instance for this page
290     */
291    protected function showList( BlockListPager $pager ) {
292        $out = $this->getOutput();
293
294        // Check for other blocks, i.e. global/tor blocks
295        $otherBlockLink = [];
296        $this->getHookRunner()->onOtherBlockLogLink( $otherBlockLink, $this->target );
297
298        // Show additional header for the local block only when other blocks exists.
299        // Not necessary in a standard installation without such extensions enabled
300        if ( count( $otherBlockLink ) ) {
301            $out->addHTML(
302                Html::element( 'h2', [], $this->msg( 'ipblocklist-localblock' )->text() ) . "\n"
303            );
304        }
305
306        if ( $pager->getNumRows() ) {
307            $out->addParserOutputContent(
308                $pager->getFullOutput(),
309                ParserOptions::newFromContext( $this->getContext() )
310            );
311        } elseif ( $this->target ) {
312            $out->addWikiMsg( 'ipblocklist-no-results' );
313        } else {
314            $out->addWikiMsg( 'ipblocklist-empty' );
315        }
316
317        if ( count( $otherBlockLink ) ) {
318            $out->addHTML(
319                Html::rawElement(
320                    'h2',
321                    [],
322                    $this->msg( 'ipblocklist-otherblocks', count( $otherBlockLink ) )->parse()
323                ) . "\n"
324            );
325            $list = '';
326            foreach ( $otherBlockLink as $link ) {
327                $list .= Html::rawElement( 'li', [], $link ) . "\n";
328            }
329            $out->addHTML( Html::rawElement(
330                'ul',
331                [ 'class' => 'mw-ipblocklist-otherblocks' ],
332                $list
333            ) . "\n" );
334        }
335    }
336
337    /** @inheritDoc */
338    protected function getGroupName() {
339        return 'users';
340    }
341
342    /**
343     * Return a IDatabase object for reading
344     *
345     * @return IReadableDatabase
346     */
347    protected function getDB() {
348        return $this->dbProvider->getReplicaDatabase();
349    }
350}
351
352/** @deprecated class alias since 1.41 */
353class_alias( SpecialBlockList::class, 'SpecialBlockList' );