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