Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
34.07% covered (danger)
34.07%
154 / 452
38.46% covered (danger)
38.46%
5 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialCheckUser
34.07% covered (danger)
34.07%
154 / 452
38.46% covered (danger)
38.46%
5 / 13
2793.35
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
1
 doesWrites
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 execute
0.00% covered (danger)
0.00%
0 / 161
0.00% covered (danger)
0.00%
0 / 1
1560
 showIntroductoryText
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 showForm
0.00% covered (danger)
0.00%
0 / 70
0.00% covered (danger)
0.00%
0 / 1
56
 addJsCIDRForm
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 checkReason
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 doMassUserBlock
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
210
 doMassUserBlockInternal
96.49% covered (success)
96.49%
55 / 57
0.00% covered (danger)
0.00%
0 / 1
20
 tagPage
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 getPager
100.00% covered (success)
100.00%
62 / 62
100.00% covered (success)
100.00%
1 / 1
5
 prefixSearchSubpages
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\CheckUser\CheckUser;
4
5use HTMLForm;
6use MediaWiki\Block\BlockPermissionCheckerFactory;
7use MediaWiki\Block\BlockUserFactory;
8use MediaWiki\Cache\LinkBatchFactory;
9use MediaWiki\CheckUser\CheckUser\Pagers\AbstractCheckUserPager;
10use MediaWiki\CheckUser\CheckUser\Pagers\CheckUserGetActionsPager;
11use MediaWiki\CheckUser\CheckUser\Pagers\CheckUserGetIPsPager;
12use MediaWiki\CheckUser\CheckUser\Pagers\CheckUserGetUsersPager;
13use MediaWiki\CheckUser\CheckUser\Widgets\CIDRCalculator;
14use MediaWiki\CheckUser\Hook\HookRunner;
15use MediaWiki\CheckUser\Services\CheckUserLogService;
16use MediaWiki\CheckUser\Services\CheckUserLookupUtils;
17use MediaWiki\CheckUser\Services\CheckUserUtilityService;
18use MediaWiki\CheckUser\Services\TokenQueryManager;
19use MediaWiki\CheckUser\Services\UserAgentClientHintsFormatter;
20use MediaWiki\CheckUser\Services\UserAgentClientHintsLookup;
21use MediaWiki\CommentFormatter\CommentFormatter;
22use MediaWiki\CommentStore\CommentStore;
23use MediaWiki\Html\FormOptions;
24use MediaWiki\Html\Html;
25use MediaWiki\Page\WikiPageFactory;
26use MediaWiki\Permissions\PermissionManager;
27use MediaWiki\SpecialPage\SpecialPage;
28use MediaWiki\Status\Status;
29use MediaWiki\Title\Title;
30use MediaWiki\User\CentralId\CentralIdLookup;
31use MediaWiki\User\CentralId\CentralIdLookupFactory;
32use MediaWiki\User\UserEditTracker;
33use MediaWiki\User\UserFactory;
34use MediaWiki\User\UserGroupManager;
35use MediaWiki\User\UserIdentity;
36use MediaWiki\User\UserIdentityLookup;
37use MediaWiki\User\UserIdentityValue;
38use MediaWiki\User\UserNamePrefixSearch;
39use MediaWiki\User\UserNameUtils;
40use MediaWiki\User\UserRigorOptions;
41use Message;
42use OOUI\IconWidget;
43use UserBlockedError;
44use Wikimedia\IPUtils;
45use Wikimedia\Rdbms\IConnectionProvider;
46use WikitextContent;
47
48class SpecialCheckUser extends SpecialPage {
49    /**
50     * The possible subtypes represented as constants.
51     * The constants represent the old string values
52     * for backwards compatibility.
53     */
54    public const SUBTYPE_GET_IPS = 'subuserips';
55
56    public const SUBTYPE_GET_ACTIONS = 'subactions';
57
58    public const SUBTYPE_GET_USERS = 'subipusers';
59
60    /**
61     * @var FormOptions the form parameters.
62     */
63    protected $opts;
64
65    private LinkBatchFactory $linkBatchFactory;
66    private BlockPermissionCheckerFactory $blockPermissionCheckerFactory;
67    private BlockUserFactory $blockUserFactory;
68    private UserGroupManager $userGroupManager;
69    private CentralIdLookup $centralIdLookup;
70    private WikiPageFactory $wikiPageFactory;
71    private PermissionManager $permissionManager;
72    private UserIdentityLookup $userIdentityLookup;
73    private TokenQueryManager $tokenQueryManager;
74    private IConnectionProvider $dbProvider;
75    private UserFactory $userFactory;
76    private CheckUserLogService $checkUserLogService;
77    private CommentFormatter $commentFormatter;
78    private UserEditTracker $userEditTracker;
79    private UserNamePrefixSearch $userNamePrefixSearch;
80    private UserNameUtils $userNameUtils;
81    private HookRunner $hookRunner;
82    private CheckUserUtilityService $checkUserUtilityService;
83    private CommentStore $commentStore;
84    private UserAgentClientHintsLookup $clientHintsLookup;
85    private UserAgentClientHintsFormatter $clientHintsFormatter;
86    private CheckUserLookupUtils $checkUserLookupUtils;
87
88    /**
89     * @param LinkBatchFactory $linkBatchFactory
90     * @param BlockPermissionCheckerFactory $blockPermissionCheckerFactory
91     * @param BlockUserFactory $blockUserFactory
92     * @param UserGroupManager $userGroupManager
93     * @param CentralIdLookupFactory $centralIdLookupFactory
94     * @param WikiPageFactory $wikiPageFactory
95     * @param PermissionManager $permissionManager
96     * @param UserIdentityLookup $userIdentityLookup
97     * @param TokenQueryManager $tokenQueryManager
98     * @param IConnectionProvider $dbProvider
99     * @param UserFactory $userFactory
100     * @param CheckUserLogService $checkUserLogService
101     * @param CommentFormatter $commentFormatter
102     * @param UserEditTracker $userEditTracker
103     * @param UserNamePrefixSearch $userNamePrefixSearch
104     * @param UserNameUtils $userNameUtils
105     * @param HookRunner $hookRunner
106     * @param CheckUserUtilityService $checkUserUtilityService
107     * @param CommentStore $commentStore
108     * @param UserAgentClientHintsLookup $clientHintsLookup
109     * @param UserAgentClientHintsFormatter $clientHintsFormatter
110     * @param CheckUserLookupUtils $checkUserLookupUtils
111     */
112    public function __construct(
113        LinkBatchFactory $linkBatchFactory,
114        BlockPermissionCheckerFactory $blockPermissionCheckerFactory,
115        BlockUserFactory $blockUserFactory,
116        UserGroupManager $userGroupManager,
117        CentralIdLookupFactory $centralIdLookupFactory,
118        WikiPageFactory $wikiPageFactory,
119        PermissionManager $permissionManager,
120        UserIdentityLookup $userIdentityLookup,
121        TokenQueryManager $tokenQueryManager,
122        IConnectionProvider $dbProvider,
123        UserFactory $userFactory,
124        CheckUserLogService $checkUserLogService,
125        CommentFormatter $commentFormatter,
126        UserEditTracker $userEditTracker,
127        UserNamePrefixSearch $userNamePrefixSearch,
128        UserNameUtils $userNameUtils,
129        HookRunner $hookRunner,
130        CheckUserUtilityService $checkUserUtilityService,
131        CommentStore $commentStore,
132        UserAgentClientHintsLookup $clientHintsLookup,
133        UserAgentClientHintsFormatter $clientHintsFormatter,
134        CheckUserLookupUtils $checkUserLookupUtils
135    ) {
136        parent::__construct( 'CheckUser', 'checkuser' );
137
138        $this->linkBatchFactory = $linkBatchFactory;
139        $this->blockPermissionCheckerFactory = $blockPermissionCheckerFactory;
140        $this->blockUserFactory = $blockUserFactory;
141        $this->userGroupManager = $userGroupManager;
142        $this->centralIdLookup = $centralIdLookupFactory->getLookup();
143        $this->wikiPageFactory = $wikiPageFactory;
144        $this->permissionManager = $permissionManager;
145        $this->userIdentityLookup = $userIdentityLookup;
146        $this->tokenQueryManager = $tokenQueryManager;
147        $this->dbProvider = $dbProvider;
148        $this->userFactory = $userFactory;
149        $this->checkUserLogService = $checkUserLogService;
150        $this->commentFormatter = $commentFormatter;
151        $this->userEditTracker = $userEditTracker;
152        $this->userNamePrefixSearch = $userNamePrefixSearch;
153        $this->userNameUtils = $userNameUtils;
154        $this->hookRunner = $hookRunner;
155        $this->checkUserUtilityService = $checkUserUtilityService;
156        $this->commentStore = $commentStore;
157        $this->clientHintsLookup = $clientHintsLookup;
158        $this->clientHintsFormatter = $clientHintsFormatter;
159        $this->checkUserLookupUtils = $checkUserLookupUtils;
160    }
161
162    public function doesWrites() {
163        // logging
164        return true;
165    }
166
167    /** @inheritDoc */
168    public function execute( $subPage ) {
169        $this->setHeaders();
170        $this->addHelpLink( 'Extension:CheckUser' );
171        $this->checkPermissions();
172        // Logging and blocking requires writing so stop from here if read-only mode
173        $this->checkReadOnly();
174
175        // Blocked users are not allowed to run checkuser queries (bug T157883)
176        $block = $this->getUser()->getBlock();
177        if ( $block && $block->isSitewide() ) {
178            throw new UserBlockedError( $block );
179        }
180
181        $request = $this->getRequest();
182
183        $opts = new FormOptions();
184        $opts->add( 'reason', '' );
185        $opts->add( 'checktype', '' );
186        $opts->add( 'period', 0 );
187        $opts->add( 'offset', '' );
188        $opts->add( 'limit', 0 );
189        $opts->add( 'dir', '' );
190        $opts->add( 'token', '' );
191        $opts->add( 'action', '' );
192        $opts->add( 'users', [] );
193        $opts->add( 'blockreason', 'other' );
194        $opts->add( 'blockreason-other', '' );
195        $opts->add( 'blocktalk', false );
196        $opts->add( 'blockemail', false );
197        $opts->add( 'reblock', false );
198        $opts->add( 'usetag', false );
199        $opts->add( 'usettag', false );
200        $opts->add( 'blocktag', '' );
201        $opts->add( 'talktag', '' );
202        $opts->fetchValuesFromRequest( $request );
203
204        // If the client has provided a token, they are trying to paginate.
205        //  If the token is valid, then use the values from this and later
206        //  don't log this as a new check.
207        $tokenData = $this->tokenQueryManager->getDataFromRequest( $this->getRequest() );
208        $validatedRequest = $this->getRequest();
209        $user = '';
210        if ( $tokenData ) {
211            foreach (
212                array_diff( AbstractCheckUserPager::TOKEN_MANAGED_FIELDS, array_keys( $tokenData ) ) as $key
213            ) {
214                $opts->reset( $key );
215                $validatedRequest->unsetVal( $key );
216            }
217            foreach ( $tokenData as $key => $value ) {
218                // Update the FormOptions
219                if ( $key === 'user' ) {
220                    $user = $value;
221                } else {
222                    $opts->setValue( $key, $value, true );
223                }
224                // Update the actual request so that IndexPager.php reads the validated values.
225                //  (used for dir, offset and limit)
226                $validatedRequest->setVal( $key, $value );
227            }
228        } else {
229            $user = trim(
230                $request->getText( 'user', $request->getText( 'ip', $subPage ?? '' ) )
231            );
232        }
233        $this->getContext()->setRequest( $validatedRequest );
234        $this->opts = $opts;
235
236        // Normalise 'user' parameter and ignore if not valid (T217713)
237        // It must be valid when making a link to Special:CheckUserLog/<user>.
238        $userTitle = Title::makeTitleSafe( NS_USER, $user );
239        $user = $userTitle ? $userTitle->getText() : '';
240
241        $out = $this->getOutput();
242        $links = [];
243        $out->enableOOUI();
244        $out->addModuleStyles( 'oojs-ui.styles.icons-interactions' );
245        $icon = new IconWidget( [ 'icon' => 'lightbulb' ] );
246        $investigateLink = $this->getLinkRenderer()->makeKnownLink(
247            SpecialPage::getTitleFor( 'Investigate' ),
248            $this->msg( 'checkuser-link-investigate-label' )->text()
249        );
250        $out->setIndicators( [ 'investigate-link' => $icon . $investigateLink ] );
251        $query = [];
252        if ( $user !== '' ) {
253            $query['targets'] = $user;
254        }
255        $links[] = Html::rawElement(
256            'span',
257            [],
258            $this->getLinkRenderer()->makeKnownLink(
259                SpecialPage::getTitleFor( 'Investigate' ),
260                $this->msg( $user ? 'checkuser-investigate-this-user' : 'checkuser-show-investigate' )->text(),
261                [],
262                $query
263            )
264        );
265        if ( $this->permissionManager->userHasRight( $this->getUser(), 'checkuser-log' ) ) {
266            $links[] = Html::rawElement(
267                'span',
268                [],
269                $this->getLinkRenderer()->makeKnownLink(
270                    SpecialPage::getTitleFor( 'CheckUserLog' ),
271                    $this->msg( 'checkuser-showlog' )->text()
272                )
273            );
274            if ( $user !== '' ) {
275                $links[] = Html::rawElement(
276                    'span',
277                    [],
278                    $this->getLinkRenderer()->makeKnownLink(
279                        SpecialPage::getTitleFor( 'CheckUserLog', $user ),
280                        $this->msg( 'checkuser-recent-checks' )->text()
281                    )
282                );
283            }
284        }
285
286        if ( count( $links ) ) {
287            $out->addSubtitle( Html::rawElement(
288                'span',
289                [ 'class' => 'mw-checkuser-links-no-parentheses' ],
290                Html::openElement( 'span' ) .
291                implode(
292                    Html::closeElement( 'span' ) . Html::openElement( 'span' ),
293                    $links
294                ) .
295                Html::closeElement( 'span' )
296            ) );
297        }
298
299        $userIdentity = null;
300        $isIP = false;
301        $xfor = false;
302        $m = [];
303        if ( IPUtils::isIPAddress( $user ) ) {
304            // A single IP address or an IP range
305            $userIdentity = UserIdentityValue::newAnonymous( IPUtils::sanitizeIP( $user ) );
306            $isIP = true;
307        } elseif ( preg_match( '/^(.+)\/xff$/', $user, $m ) && IPUtils::isIPAddress( $m[1] ) ) {
308            // A single IP address or range with XFF string included
309            $userIdentity = UserIdentityValue::newAnonymous( IPUtils::sanitizeIP( $m[1] ) );
310            $xfor = true;
311            $isIP = true;
312        } else {
313            // A user?
314            if ( $user ) {
315                $userIdentity = $this->userIdentityLookup->getUserIdentityByName( $user );
316            }
317        }
318
319        $this->showIntroductoryText();
320        $this->showForm( $user, $isIP );
321
322        // Perform one of the various submit operations...
323        if ( $request->wasPosted() ) {
324            $checkType = $this->opts->getValue( 'checktype' );
325            if ( !$this->getUser()->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) {
326                $out->wrapWikiMsg( '<div class="error">$1</div>', 'checkuser-token-fail' );
327            } elseif ( $this->opts->getValue( 'action' ) === 'block' ) {
328                $this->doMassUserBlock();
329            } elseif ( !$this->checkReason() ) {
330                $out->addWikiMsg( 'checkuser-noreason' );
331            } elseif ( $checkType == self::SUBTYPE_GET_IPS ) {
332                if ( $isIP || !$user ) {
333                    $out->addWikiMsg( 'nouserspecified' );
334                } elseif ( !$userIdentity || !$userIdentity->getId() ) {
335                    $out->addWikiMsg( 'nosuchusershort', $user );
336                } else {
337                    $pager = $this->getPager( self::SUBTYPE_GET_IPS, $userIdentity, 'userips' );
338                    $out->addHtml( $pager->getBody() );
339                }
340            } elseif ( $checkType == self::SUBTYPE_GET_ACTIONS ) {
341                if ( $isIP && $userIdentity ) {
342                    // Target is a IP or range
343                    if ( !$this->checkUserLookupUtils->isValidIPOrRange( $userIdentity->getName() ) ) {
344                        $out->addWikiMsg( 'checkuser-range-outside-limit', $userIdentity->getName() );
345                    } else {
346                        $logType = $xfor ? 'ipedits-xff' : 'ipedits';
347
348                        // Ordered in descent by timestamp. Can cause large filesorts on range scans.
349                        $pager = $this->getPager( self::SUBTYPE_GET_ACTIONS, $userIdentity, $logType, $xfor );
350                        $out->addHTML( $pager->getBody() );
351                    }
352                } else {
353                    // Target is a username
354                    if ( !$user ) {
355                        $out->addWikiMsg( 'nouserspecified' );
356                    } elseif ( !$userIdentity || !$userIdentity->getId() ) {
357                        $out->addHTML( $this->msg( 'nosuchusershort', $user )->parseAsBlock() );
358                    } else {
359                        // Sorting might take some time
360                        // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
361                        @set_time_limit( 60 );
362
363                        $pager = $this->getPager( self::SUBTYPE_GET_ACTIONS, $userIdentity, 'useredits' );
364                        $out->addHTML( $pager->getBody() );
365                    }
366                }
367            } elseif ( $checkType == self::SUBTYPE_GET_USERS ) {
368                if ( !$isIP || !$userIdentity ) {
369                    $out->addWikiMsg( 'badipaddress' );
370                } elseif ( !$this->checkUserLookupUtils->isValidIPOrRange( $userIdentity->getName() ) ) {
371                    $out->addWikiMsg( 'checkuser-range-outside-limit', $userIdentity->getName() );
372                } else {
373                    $logType = $xfor ? 'ipusers-xff' : 'ipusers';
374
375                    $pager = $this->getPager( self::SUBTYPE_GET_USERS, $userIdentity, $logType, $xfor );
376                    $out->addHTML( $pager->getBody() );
377                }
378            }
379        }
380        // Add CIDR calculation convenience JS form
381        $this->addJsCIDRForm();
382        $out->addJsConfigVars(
383            'wgCheckUserDisplayClientHints',
384            $this->getConfig()->get( 'CheckUserDisplayClientHints' )
385        );
386        $out->addModules( 'ext.checkUser' );
387        $out->addModuleStyles( [
388            'mediawiki.interface.helpers.styles',
389            'ext.checkUser.styles',
390        ] );
391    }
392
393    protected function showIntroductoryText() {
394        $config = $this->getConfig();
395        $cidrLimit = $config->get( 'CheckUserCIDRLimit' );
396        $maximumRowCount = $config->get( 'CheckUserMaximumRowCount' );
397        $this->getOutput()->addWikiMsg(
398            'checkuser-summary',
399            $cidrLimit['IPv4'],
400            $cidrLimit['IPv6'],
401            Message::numParam( $maximumRowCount )
402        );
403    }
404
405    /**
406     * Show the CheckUser query form
407     *
408     * @param string $user
409     * @param bool $isIP
410     */
411    protected function showForm( string $user, bool $isIP ) {
412        // Fill in requested type if it makes sense
413        $ipAllowed = true;
414        $checktype = $this->opts->getValue( 'checktype' );
415        if ( $checktype == self::SUBTYPE_GET_USERS && $isIP ) {
416            $checkTypeValidated = $checktype;
417            $ipAllowed = false;
418        } elseif ( $checktype == self::SUBTYPE_GET_IPS && !$isIP ) {
419            $checkTypeValidated = $checktype;
420        } elseif ( $checktype == self::SUBTYPE_GET_ACTIONS ) {
421            $checkTypeValidated = $checktype;
422        // Defaults otherwise
423        } elseif ( $isIP ) {
424            $checkTypeValidated = self::SUBTYPE_GET_ACTIONS;
425        } else {
426            $checkTypeValidated = self::SUBTYPE_GET_IPS;
427            $ipAllowed = false;
428        }
429
430        $fields = [
431            'target' => [
432                'type' => 'user',
433                // validation in execute() currently
434                'exists' => false,
435                'ipallowed' => $ipAllowed,
436                'iprange' => $ipAllowed,
437                'name' => 'user',
438                'label-message' => 'checkuser-target',
439                'default' => $user,
440                'id' => 'checktarget',
441            ],
442            'radiooptions' => [
443                'type' => 'radio',
444                'options-messages' => [
445                    'checkuser-ips' => self::SUBTYPE_GET_IPS,
446                    'checkuser-actions' => self::SUBTYPE_GET_ACTIONS,
447                    'checkuser-users' => self::SUBTYPE_GET_USERS,
448                ],
449                'id' => 'checkuserradios',
450                'default' => $checkTypeValidated,
451                'name' => 'checktype',
452                'nodata' => 'yes',
453                'flatlist' => true,
454            ],
455            'period' => [
456                'type' => 'select',
457                'id' => 'period',
458                'label-message' => 'checkuser-period',
459                'options-messages' => [
460                    'checkuser-week-1' => 7,
461                    'checkuser-week-2' => 14,
462                    'checkuser-month' => 30,
463                    'checkuser-month-2' => 60,
464                    'checkuser-all' => 0,
465                ],
466                'default' => $this->opts->getValue( 'period' ),
467                'name' => 'period',
468            ],
469            'reason' => [
470                'type' => 'text',
471                'default' => $this->opts->getValue( 'reason' ),
472                'label-message' => 'checkuser-reason',
473                'size' => 46,
474                'maxlength' => 150,
475                'id' => 'checkreason',
476                'name' => 'reason',
477            ],
478        ];
479
480        $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
481        $form->setMethod( 'post' )
482            ->setWrapperLegendMsg( 'checkuser-query' )
483            ->setSubmitTextMsg( 'checkuser-check' )
484            ->setId( 'checkuserform' )
485            ->setSubmitId( 'checkusersubmit' )
486            ->setSubmitName( 'checkusersubmit' )
487            ->prepareForm()
488            ->displayForm( false );
489    }
490
491    /**
492     * Make a quick JS form for admins to calculate block ranges
493     */
494    protected function addJsCIDRForm() {
495        $out = $this->getOutput();
496        $out->addHTML( ( new CIDRCalculator( $out ) )->getHtml() );
497    }
498
499    /**
500     * @return bool
501     */
502    protected function checkReason(): bool {
503        return ( !$this->getConfig()->get( 'CheckUserForceSummary' ) || strlen( $this->opts->getValue( 'reason' ) ) );
504    }
505
506    /**
507     * Block a list of selected users
508     * with options provided in the POST request.
509     */
510    protected function doMassUserBlock() {
511        $users = $this->opts->getValue( 'users' );
512        $reason = $this->opts->getValue( 'blockreason-other' );
513        $reasonPrefix = $this->opts->getValue( 'blockreason' );
514
515        if ( $reasonPrefix !== '' && $reasonPrefix !== 'other' ) {
516            $reason = $reasonPrefix . $this->msg( 'colon-separator' )->inContentLanguage()->text() . $reason;
517        }
518        $blockParams = [
519            'reason' => $reason,
520            'email' => $this->opts->getValue( 'blockemail' ),
521            'talk' => $this->opts->getValue( 'blocktalk' ),
522            'reblock' => $this->opts->getValue( 'reblock' ),
523        ];
524        $tag = $this->opts->getValue( 'usetag' ) ?
525            trim( $this->opts->getValue( 'blocktag' ) ) : '';
526        $talkTag = $this->opts->getValue( 'usettag' ) ?
527            trim( $this->opts->getValue( 'talktag' ) ) : '';
528        $usersCount = count( $users );
529
530        if (
531            !$usersCount
532            || !$this->permissionManager->userHasRight( $this->getUser(), 'block' )
533            || $this->getUser()->getBlock()
534        ) {
535            $this->getOutput()->addWikiMsg( 'checkuser-block-failure' );
536            return;
537        }
538
539        if ( $usersCount > $this->getConfig()->get( 'CheckUserMaxBlocks' ) ) {
540            $this->getOutput()->addWikiMsg( 'checkuser-block-limit' );
541            return;
542        }
543
544        if ( !$blockParams['reason'] ) {
545            $this->getOutput()->addWikiMsg( 'checkuser-block-noreason' );
546            return;
547        }
548
549        [ $blockedUsers, $taggedUsers ] = $this->doMassUserBlockInternal(
550            $users,
551            $blockParams,
552            $this->opts->getValue( 'usetag' ),
553            $tag,
554            $this->opts->getValue( 'usettag' ),
555            $talkTag
556        );
557        $blockedCount = count( $blockedUsers );
558        $taggedCount = count( $taggedUsers );
559        $lang = $this->getLanguage();
560        if ( $blockedCount > 0 ) {
561            $this->getOutput()->addWikiMsg( 'checkuser-block-success',
562                $lang->listToText( $blockedUsers ),
563                $lang->formatNum( $blockedCount )
564            );
565        }
566        if ( $taggedCount > 0 ) {
567            $this->getOutput()->addWikiMsg( 'checkuser-block-success-tagged',
568                $lang->listToText( $taggedUsers ),
569                $lang->formatNum( $taggedCount )
570            );
571        }
572        if ( $blockedCount === 0 && $taggedCount === 0 ) {
573            $this->getOutput()->addWikiMsg( 'checkuser-block-failure' );
574        }
575    }
576
577    /**
578     * Block a list of selected users
579     *
580     * @param string[] $users
581     * @param array $blockParams
582     * @param bool $useTag whether to perform the user page replacement
583     * @param string $tag replace the user page with this content
584     * @param bool $useTalkTag whether to perform the user talk page replacement
585     * @param string $talkTag replace the user talk page this with content
586     * @return string[][] List of html-safe usernames which were blocked at index 0 and tagged at index 1
587     */
588    protected function doMassUserBlockInternal(
589        array $users,
590        array $blockParams,
591        bool $useTag = false,
592        string $tag = '',
593        bool $useTalkTag = false,
594        string $talkTag = ''
595    ) {
596        $blockedUsers = [];
597        $taggedUsers = [];
598        foreach ( $users as $name ) {
599            $u = $this->userFactory->newFromName( $name, UserRigorOptions::RIGOR_NONE );
600            // Do some checks to make sure we can block this user first
601            if ( !$u ) {
602                // Invalid user
603                continue;
604            }
605            $isIP = IPUtils::isIPAddress( $u->getName() );
606            if ( !$u->getId() && !$isIP ) {
607                // Not a registered user or an IP
608                continue;
609            }
610
611            if (
612                !isset( $blockParams['email'] ) ||
613                $blockParams['email'] === false ||
614                $this->blockPermissionCheckerFactory
615                    ->newBlockPermissionChecker(
616                        $u,
617                        $this->getUser()
618                    )
619                    ->checkEmailPermissions()
620            ) {
621                $res = $this->blockUserFactory->newBlockUser(
622                    $u,
623                    $this->getAuthority(),
624                    $isIP ? '1 week' : 'indefinite',
625                    $blockParams['reason'],
626                    [
627                        'isCreateAccountBlocked' => true,
628                        'isEmailBlocked' => $blockParams['email'] ?? false,
629                        'isHardBlock' => !$isIP,
630                        'isAutoblocking' => true,
631                        'isUserTalkEditBlocked' => $blockParams['talk'] ?? false,
632                    ]
633                )->placeBlock( $blockParams['reblock'] );
634
635                if (
636                    $res->isGood() ||
637                    ( $res->getStatusValue()->hasMessage( 'ipb_already_blocked' ) && $blockParams['reblock'] )
638                ) {
639                    // Mark as blocked and then attempt to tag if the block went through or
640                    //  if reblock was enabled and there existed a block with the same parameters.
641                    $userPage = $u->getUserPage();
642                    $userText = "[[{$userPage->getPrefixedText()}|{$userPage->getText()}]]";
643
644                    $blockedUsers[] = $userText;
645
646                    if ( $useTag || $useTalkTag ) {
647                        $userPageTagSuccess = true;
648                        $userTalkPageTagSuccess = true;
649
650                        // Tag user page and user talk page
651                        if ( $useTag ) {
652                            $userPageTagStatus = $this->tagPage(
653                                $userPage,
654                                $tag,
655                                $blockParams['reason']
656                            );
657                            // Mark as a success if the edit went through or if the
658                            //  content that was used is the same as what is already on
659                            //  the page.
660                            $userPageTagSuccess = $userPageTagStatus->isGood() ||
661                                $userPageTagStatus->hasMessage( 'edit-no-change' );
662                        }
663                        if ( $useTalkTag ) {
664                            $userTalkPageTagStatus = $this->tagPage(
665                                $u->getTalkPage(),
666                                $talkTag,
667                                $blockParams['reason']
668                            );
669                            $userTalkPageTagSuccess = $userTalkPageTagStatus->isGood() ||
670                                $userTalkPageTagStatus->hasMessage( 'edit-no-change' );
671                        }
672                        if ( $userPageTagSuccess && $userTalkPageTagSuccess ) {
673                            // Only mark as tagged if all tags requested
674                            //  for this user was successfully added
675                            $taggedUsers[] = $userText;
676                        }
677                    }
678                }
679            }
680        }
681
682        return [ $blockedUsers, $taggedUsers ];
683    }
684
685    /**
686     * Make an edit to the given page with the tag provided
687     *
688     * @param Title $title
689     * @param string $tag
690     * @param string $summary
691     * @return Status the status of the edit to the $title
692     */
693    protected function tagPage( Title $title, string $tag, string $summary ) {
694        // Check length to avoid mistakes
695        if ( strlen( $tag ) > 2 ) {
696            $page = $this->wikiPageFactory->newFromTitle( $title );
697            $flags = 0;
698            if ( $page->exists() ) {
699                $flags |= EDIT_MINOR;
700            }
701            return $page->doUserEditContent(
702                new WikitextContent( $tag ),
703                $this->getUser(),
704                $summary,
705                $flags
706            );
707        }
708        return Status::newFatal( 'checkuser-block-failure-tag-too-small' );
709    }
710
711    /**
712     * Gets the pager for the specific check type.
713     * Returns null if the checktype is not recognised.
714     *
715     * @param string $checkType
716     * @param UserIdentity $userIdentity
717     * @param string $logType
718     * @param bool|null $xfor
719     * @return AbstractCheckUserPager|null
720     */
721    public function getPager( string $checkType, UserIdentity $userIdentity, string $logType, ?bool $xfor = null ) {
722        switch ( $checkType ) {
723            case self::SUBTYPE_GET_IPS:
724                return new CheckUserGetIPsPager(
725                    $this->opts,
726                    $userIdentity,
727                    $logType,
728                    $this->tokenQueryManager,
729                    $this->userGroupManager,
730                    $this->centralIdLookup,
731                    $this->dbProvider,
732                    $this->getSpecialPageFactory(),
733                    $this->userIdentityLookup,
734                    $this->checkUserLogService,
735                    $this->userFactory,
736                    $this->checkUserLookupUtils
737                );
738            case self::SUBTYPE_GET_USERS:
739                return new CheckUserGetUsersPager(
740                    $this->opts,
741                    $userIdentity,
742                    $xfor ?? false,
743                    $logType,
744                    $this->tokenQueryManager,
745                    $this->permissionManager,
746                    $this->blockPermissionCheckerFactory,
747                    $this->userGroupManager,
748                    $this->centralIdLookup,
749                    $this->dbProvider,
750                    $this->getSpecialPageFactory(),
751                    $this->userIdentityLookup,
752                    $this->userFactory,
753                    $this->checkUserLogService,
754                    $this->checkUserLookupUtils,
755                    $this->userEditTracker,
756                    $this->checkUserUtilityService,
757                    $this->clientHintsLookup,
758                    $this->clientHintsFormatter
759                );
760            case self::SUBTYPE_GET_ACTIONS:
761                return new CheckUserGetActionsPager(
762                    $this->opts,
763                    $userIdentity,
764                    $xfor,
765                    $logType,
766                    $this->tokenQueryManager,
767                    $this->userGroupManager,
768                    $this->centralIdLookup,
769                    $this->linkBatchFactory,
770                    $this->dbProvider,
771                    $this->getSpecialPageFactory(),
772                    $this->userIdentityLookup,
773                    $this->userFactory,
774                    $this->checkUserLookupUtils,
775                    $this->checkUserLogService,
776                    $this->commentFormatter,
777                    $this->userEditTracker,
778                    $this->hookRunner,
779                    $this->checkUserUtilityService,
780                    $this->commentStore,
781                    $this->clientHintsLookup,
782                    $this->clientHintsFormatter
783                );
784            default:
785                return null;
786        }
787    }
788
789    /**
790     * Return an array of subpages beginning with $search that this special page will accept.
791     *
792     * @param string $search Prefix to search for
793     * @param int $limit Maximum number of results to return (usually 10)
794     * @param int $offset Number of results to skip (usually 0)
795     * @return string[] Matching subpages
796     */
797    public function prefixSearchSubpages( $search, $limit, $offset ) {
798        if ( !$this->userNameUtils->isValid( $search ) ) {
799            // No prefix suggestion for invalid user
800            return [];
801        }
802        // Autocomplete subpage as user list - public to allow caching
803        return $this->userNamePrefixSearch->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
804    }
805
806    /**
807     * @inheritDoc
808     */
809    protected function getGroupName() {
810        return 'users';
811    }
812}