Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
51.15% covered (warning)
51.15%
223 / 436
63.64% covered (warning)
63.64%
7 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
CheckUserGetUsersPager
51.15% covered (warning)
51.15%
223 / 436
63.64% covered (warning)
63.64%
7 / 11
768.29
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
4
 formatRow
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBody
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 formatUserRow
53.73% covered (warning)
53.73%
72 / 134
0.00% covered (danger)
0.00%
0 / 1
112.30
 preprocessResults
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
1 / 1
11
 getQueryInfo
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
6
 getQueryInfoForCuChanges
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
3
 getQueryInfoForCuLogEvent
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
2
 getQueryInfoForCuPrivateEvent
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
2
 getStartBody
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
20
 getEndBody
0.00% covered (danger)
0.00%
0 / 108
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2
3namespace MediaWiki\CheckUser\CheckUser\Pagers;
4
5use ExtensionRegistry;
6use IContextSource;
7use LogicException;
8use MediaWiki\Block\BlockPermissionCheckerFactory;
9use MediaWiki\CheckUser\CheckUser\SpecialCheckUser;
10use MediaWiki\CheckUser\CheckUser\Widgets\HTMLFieldsetCheckUser;
11use MediaWiki\CheckUser\ClientHints\ClientHintsLookupResults;
12use MediaWiki\CheckUser\ClientHints\ClientHintsReferenceIds;
13use MediaWiki\CheckUser\Services\CheckUserLogService;
14use MediaWiki\CheckUser\Services\CheckUserLookupUtils;
15use MediaWiki\CheckUser\Services\CheckUserUtilityService;
16use MediaWiki\CheckUser\Services\TokenQueryManager;
17use MediaWiki\CheckUser\Services\UserAgentClientHintsFormatter;
18use MediaWiki\CheckUser\Services\UserAgentClientHintsLookup;
19use MediaWiki\CheckUser\Services\UserAgentClientHintsManager;
20use MediaWiki\Config\ConfigException;
21use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
22use MediaWiki\Html\FormOptions;
23use MediaWiki\Html\Html;
24use MediaWiki\Html\ListToggle;
25use MediaWiki\Linker\Linker;
26use MediaWiki\Linker\LinkRenderer;
27use MediaWiki\Permissions\PermissionManager;
28use MediaWiki\SpecialPage\SpecialPageFactory;
29use MediaWiki\User\CentralId\CentralIdLookup;
30use MediaWiki\User\UserEditTracker;
31use MediaWiki\User\UserFactory;
32use MediaWiki\User\UserGroupManager;
33use MediaWiki\User\UserIdentity;
34use MediaWiki\User\UserIdentityLookup;
35use MediaWiki\User\UserIdentityValue;
36use MediaWiki\WikiMap\WikiMap;
37use Wikimedia\IPUtils;
38use Wikimedia\Rdbms\IConnectionProvider;
39use Xml;
40
41class CheckUserGetUsersPager extends AbstractCheckUserPager {
42    /** @var bool Whether the user performing this check has the block right. */
43    protected bool $canPerformBlocks;
44
45    /** @var array[] */
46    protected $userSets;
47
48    /** @var string|false */
49    private $centralAuthToollink;
50
51    /** @var array|false */
52    private $globalBlockingToollink;
53
54    /** @var string[][] */
55    private $aliases;
56
57    private ClientHintsLookupResults $clientHintsLookupResults;
58
59    private BlockPermissionCheckerFactory $blockPermissionCheckerFactory;
60    private PermissionManager $permissionManager;
61    private UserEditTracker $userEditTracker;
62    private CheckUserUtilityService $checkUserUtilityService;
63    private UserAgentClientHintsLookup $clientHintsLookup;
64    private UserAgentClientHintsFormatter $clientHintsFormatter;
65
66    /**
67     * @param FormOptions $opts
68     * @param UserIdentity $target
69     * @param bool $xfor
70     * @param string $logType
71     * @param TokenQueryManager $tokenQueryManager
72     * @param PermissionManager $permissionManager
73     * @param BlockPermissionCheckerFactory $blockPermissionCheckerFactory
74     * @param UserGroupManager $userGroupManager
75     * @param CentralIdLookup $centralIdLookup
76     * @param IConnectionProvider $dbProvider
77     * @param SpecialPageFactory $specialPageFactory
78     * @param UserIdentityLookup $userIdentityLookup
79     * @param UserFactory $userFactory
80     * @param CheckUserLogService $checkUserLogService
81     * @param CheckUserLookupUtils $checkUserLookupUtils
82     * @param UserEditTracker $userEditTracker
83     * @param CheckUserUtilityService $checkUserUtilityService
84     * @param UserAgentClientHintsLookup $clientHintsLookup
85     * @param UserAgentClientHintsFormatter $clientHintsFormatter
86     * @param IContextSource|null $context
87     * @param LinkRenderer|null $linkRenderer
88     * @param ?int $limit
89     */
90    public function __construct(
91        FormOptions $opts,
92        UserIdentity $target,
93        bool $xfor,
94        string $logType,
95        TokenQueryManager $tokenQueryManager,
96        PermissionManager $permissionManager,
97        BlockPermissionCheckerFactory $blockPermissionCheckerFactory,
98        UserGroupManager $userGroupManager,
99        CentralIdLookup $centralIdLookup,
100        IConnectionProvider $dbProvider,
101        SpecialPageFactory $specialPageFactory,
102        UserIdentityLookup $userIdentityLookup,
103        UserFactory $userFactory,
104        CheckUserLogService $checkUserLogService,
105        CheckUserLookupUtils $checkUserLookupUtils,
106        UserEditTracker $userEditTracker,
107        CheckUserUtilityService $checkUserUtilityService,
108        UserAgentClientHintsLookup $clientHintsLookup,
109        UserAgentClientHintsFormatter $clientHintsFormatter,
110        IContextSource $context = null,
111        LinkRenderer $linkRenderer = null,
112        ?int $limit = null
113    ) {
114        parent::__construct( $opts, $target, $logType, $tokenQueryManager,
115            $userGroupManager, $centralIdLookup, $dbProvider, $specialPageFactory,
116            $userIdentityLookup, $checkUserLogService, $userFactory, $checkUserLookupUtils,
117            $context, $linkRenderer, $limit );
118        $this->checkType = SpecialCheckUser::SUBTYPE_GET_USERS;
119        $this->xfor = $xfor;
120        $this->canPerformBlocks = $permissionManager->userHasRight( $this->getUser(), 'block' )
121            && !$this->getUser()->getBlock();
122        $this->centralAuthToollink = ExtensionRegistry::getInstance()->isLoaded( 'CentralAuth' )
123            ? $this->getConfig()->get( 'CheckUserCAtoollink' ) : false;
124        $this->globalBlockingToollink = ExtensionRegistry::getInstance()->isLoaded( 'GlobalBlocking' )
125            ? $this->getConfig()->get( 'CheckUserGBtoollink' ) : false;
126        $this->aliases = $this->getLanguage()->getSpecialPageAliases();
127        $this->blockPermissionCheckerFactory = $blockPermissionCheckerFactory;
128        $this->permissionManager = $permissionManager;
129        $this->userEditTracker = $userEditTracker;
130        $this->checkUserUtilityService = $checkUserUtilityService;
131        $this->clientHintsLookup = $clientHintsLookup;
132        $this->clientHintsFormatter = $clientHintsFormatter;
133    }
134
135    /**
136     * Returns nothing as formatUserRow
137     * is instead used.
138     *
139     * @inheritDoc
140     */
141    public function formatRow( $row ): string {
142        return '';
143    }
144
145    /** @inheritDoc */
146    public function getBody() {
147        $this->getOutput()->addModuleStyles( $this->getModuleStyles() );
148        if ( !$this->mQueryDone ) {
149            $this->doQuery();
150        }
151
152        if ( $this->mResult->numRows() ) {
153            # Do any special query batches before display
154            $this->doBatchLookups();
155        }
156
157        # Don't use any extra rows returned by the query
158        $numRows = count( $this->userSets['ids'] );
159
160        $s = $this->getStartBody();
161        if ( $numRows ) {
162            $keys = array_keys( $this->userSets['ids'] );
163            if ( $this->mIsBackwards ) {
164                $keys = array_reverse( $keys );
165            }
166            foreach ( $keys as $user_text ) {
167                $s .= $this->formatUserRow( $user_text );
168            }
169            $s .= $this->getFooter();
170        } else {
171            $s .= $this->getEmptyBody();
172        }
173        $s .= $this->getEndBody();
174        return $s;
175    }
176
177    /**
178     * Gets a row for the results for 'Get users'
179     *
180     * @param string $user_text the username for the current row.
181     * @return string
182     */
183    public function formatUserRow( string $user_text ): string {
184        $templateParams = [];
185
186        $userIsIP = IPUtils::isIPAddress( $user_text );
187
188        // Load user object
189        $user = new UserIdentityValue(
190            $this->userSets['ids'][$user_text],
191            $userIsIP ? IPUtils::prettifyIP( $user_text ) ?? $user_text : $user_text
192        );
193        $hidden = $this->userFactory->newFromUserIdentity( $user )->isHidden()
194            && !$this->getAuthority()->isAllowed( 'hideuser' );
195        if ( $hidden ) {
196            // User is hidden from the current authority, so the current authority cannot block this user either.
197            // As such, the checkbox (used for blocking the user) should not be shown.
198            $templateParams['canPerformBlocks'] = false;
199            $templateParams['userText'] = '';
200            $templateParams['userLink'] = Html::element(
201                'span',
202                [ 'class' => 'history-deleted' ],
203                $this->msg( 'rev-deleted-user' )->text()
204            );
205        } else {
206            $templateParams['canPerformBlocks'] = $this->canPerformBlocks;
207            $templateParams['userText'] = $user->getName();
208            $userNonExistent = !IPUtils::isIPAddress( $user ) && !$user->isRegistered();
209            if ( $userNonExistent ) {
210                $templateParams['userLinkClass'] = 'mw-checkuser-nonexistent-user';
211            }
212            $templateParams['userLink'] = Linker::userLink( $user->getId(), $user, $user );
213            $templateParams['userToolLinks'] = Linker::userToolLinksRedContribs(
214                $user->getId(),
215                $user,
216                $this->userEditTracker->getUserEditCount( $user ),
217                // don't render parentheses in HTML markup (CSS will provide)
218                false
219            );
220            if ( $userIsIP ) {
221                $templateParams['userLinks'] = $this->msg( 'checkuser-userlinks-ip', $user )->parse();
222            } elseif ( !$userNonExistent ) {
223                if ( $this->msg( 'checkuser-userlinks' )->exists() ) {
224                    $templateParams['userLinks'] =
225                        $this->msg( 'checkuser-userlinks', htmlspecialchars( $user ) )->parse();
226                }
227            }
228            // Add global user tools links
229            // Add CentralAuth link for real registered users
230            if ( $this->centralAuthToollink !== false
231                && !$userIsIP
232                && !$userNonExistent
233            ) {
234                // Get CentralAuth SpecialPage name in UserLang from the first Alias name
235                $spca = $this->aliases['CentralAuth'][0];
236                $calinkAlias = str_replace( '_', ' ', $spca );
237                $centralCAUrl = WikiMap::getForeignURL(
238                    $this->centralAuthToollink,
239                    'Special:CentralAuth'
240                );
241                if ( $centralCAUrl === false ) {
242                    throw new ConfigException(
243                        "Could not retrieve URL for CentralAuth: $this->centralAuthToollink"
244                    );
245                }
246                $linkCA = Html::element( 'a',
247                    [
248                        'href' => $centralCAUrl . "/" . $user,
249                        'title' => $this->msg( 'centralauth' )->text(),
250                    ],
251                    $calinkAlias
252                );
253                $templateParams['centralAuthLink'] = $this->msg( 'parentheses' )->rawParams( $linkCA )->escaped();
254            }
255            // Add GlobalBlocking link to CentralWiki
256            if ( $this->globalBlockingToollink !== false
257                && IPUtils::isIPAddress( $user )
258            ) {
259                // Get GlobalBlock SpecialPage name in UserLang from the first Alias name
260                $centralGBUrl = WikiMap::getForeignURL(
261                    $this->globalBlockingToollink['centralDB'],
262                    'Special:GlobalBlock'
263                );
264                $spgb = $this->aliases['GlobalBlock'][0];
265                $gblinkAlias = str_replace( '_', ' ', $spgb );
266                if ( ExtensionRegistry::getInstance()->isLoaded( 'CentralAuth' ) ) {
267                    $gbUserGroups = CentralAuthUser::getInstance( $this->getUser() )->getGlobalGroups();
268                    // Link to GB via WikiMap since CA require it
269                    if ( $centralGBUrl === false ) {
270                        throw new ConfigException(
271                            'Could not retrieve URL for global blocking toollink'
272                        );
273                    }
274                    $linkGB = Html::element( 'a',
275                        [
276                            'href' => $centralGBUrl . "/" . $user,
277                            'title' => $this->msg( 'globalblocking-block-submit' )->text(),
278                        ],
279                        $gblinkAlias
280                    );
281                } elseif ( $centralGBUrl !== false ) {
282                    // Case wikimap configured without CentralAuth extension
283                    // Get effective Local user groups since there is a wikimap but there is no CA
284                    $gbUserGroups = $this->userGroupManager->getUserEffectiveGroups( $this->getUser() );
285                    $linkGB = Html::element( 'a',
286                        [
287                            'href' => $centralGBUrl . "/" . $user,
288                            'title' => $this->msg( 'globalblocking-block-submit' )->text(),
289                        ],
290                        $gblinkAlias
291                    );
292                } else {
293                    // Load local user group instead
294                    $gbUserGroups = [ '' ];
295                    $gbtitle = $this->getPageTitle( 'GlobalBlock' );
296                    $linkGB = $this->getLinkRenderer()->makeKnownLink(
297                        $gbtitle,
298                        $gblinkAlias,
299                        [ 'title' => $this->msg( 'globalblocking-block-submit' ) ]
300                    );
301                    $gbUserCanDo = $this->permissionManager->userHasRight( $this->getUser(), 'globalblock' );
302                    if ( $gbUserCanDo ) {
303                        $this->globalBlockingToollink['groups'] = $gbUserGroups;
304                    }
305                }
306                // Only load the script for users in the configured global(local) group(s) or
307                // for local user with globalblock permission if there is no WikiMap
308                if ( count( array_intersect( $this->globalBlockingToollink['groups'], $gbUserGroups ) ) ) {
309                    $templateParams['globalBlockLink'] .= $this->msg( 'parentheses' )->rawParams( $linkGB )->escaped();
310                }
311            }
312            // Check if this user or IP is blocked. If so, give a link to the block log...
313            $templateParams['flags'] = $this->userBlockFlags( $userIsIP ? $user : '', $user );
314        }
315        // Show edit time range
316        $templateParams['timeRange'] = $this->getTimeRangeString(
317            $this->userSets['first'][$user_text],
318            $this->userSets['last'][$user_text]
319        );
320        // Total edit count
321        $templateParams['editCount'] = $this->userSets['edits'][$user_text];
322        // List out each IP/XFF combo for this username
323        $templateParams['infoSets'] = [];
324        for ( $i = ( count( $this->userSets['infosets'][$user_text] ) - 1 ); $i >= 0; $i-- ) {
325            // users_infosets[$name][$i] is array of [ $row->ip, XFF ];
326            $row = [];
327            [ $clientIP, $xffString ] = $this->userSets['infosets'][$user_text][$i];
328            // IP link
329            $row['ipLink'] = $this->getSelfLink( $clientIP, [ 'user' => $clientIP ] );
330            // XFF string, link to /xff search
331            if ( $xffString ) {
332                // Flag our trusted proxies
333                [ $client ] = $this->checkUserUtilityService->getClientIPfromXFF( $xffString );
334                // XFF was trusted if client came from it
335                $trusted = ( $client === $clientIP );
336                $row['xffTrusted'] = $trusted;
337                $row['xff'] = $this->getSelfLink( $xffString, [ 'user' => $client . '/xff' ] );
338            }
339            $templateParams['infoSets'][] = $row;
340        }
341        // List out each agent for this username
342        for ( $i = ( count( $this->userSets['agentsets'][$user_text] ) - 1 ); $i >= 0; $i-- ) {
343            $templateParams['agentsList'][] = $this->userSets['agentsets'][$user_text][$i];
344        }
345        // Show Client Hints data if display is enabled.
346        $templateParams['displayClientHints'] = $this->displayClientHints;
347        if ( $this->displayClientHints ) {
348            $templateParams['clientHintsList'] = [];
349            [ $usagesOfClientHints, $clientHintsDataObjects ] = $this->clientHintsLookupResults
350                ->getGroupedClientHintsDataForReferenceIds( $this->userSets['clienthints'][$user_text] );
351            // Sort the $usagesOfClientHints array such that the ClientHintsData object that is most used
352            // by the user referenced in $user_text is shown first and the ClientHintsData object least used is
353            // shown last. This is done to be consistent with the way that User-Agent strings are shown as well
354            // as ensuring that if there are more than 10 items the ClientHintsData objects used on the most reference
355            // IDs are shown.
356            arsort( $usagesOfClientHints, SORT_NUMERIC );
357            // Limit the number displayed to at most 10 starting at the
358            // ClientHintsData object associated with the most rows
359            // in the results. This is to be consistent with User-Agent
360            // strings which are also limited to 10 strings.
361            $i = 0;
362            foreach ( array_keys( $usagesOfClientHints ) as $clientHintsDataIndex ) {
363                // If 10 Client Hints data objects have been displayed,
364                // then don't show any more (similar to User-Agent strings).
365                if ( $i === 10 ) {
366                    break;
367                }
368                $clientHintsDataObject = $clientHintsDataObjects[$clientHintsDataIndex];
369                if ( $clientHintsDataObject ) {
370                    $formattedClientHintsData = $this->clientHintsFormatter
371                        ->formatClientHintsDataObject( $clientHintsDataObject );
372                    if ( $formattedClientHintsData ) {
373                        // If the Client Hints data object is valid and evaluates to a non-empty
374                        // human readable string, then add it to the list to display.
375                        $i++;
376                        $templateParams['clientHintsList'][] = $formattedClientHintsData;
377                    }
378                }
379            }
380        }
381        return $this->templateParser->processTemplate( 'GetUsersLine', $templateParams );
382    }
383
384    /** @inheritDoc */
385    protected function preprocessResults( $result ) {
386        $this->userSets = [
387            'first' => [],
388            'last' => [],
389            'edits' => [],
390            'ids' => [],
391            'infosets' => [],
392            'agentsets' => [],
393            'clienthints' => [],
394        ];
395        $referenceIdsForLookup = new ClientHintsReferenceIds();
396
397        foreach ( $result as $row ) {
398            // Use the IP as the user_text if the actor ID is NULL and the IP is not NULL (T353953).
399            if ( $row->actor === null && $row->ip ) {
400                $row->user_text = $row->ip;
401            }
402
403            if ( !array_key_exists( $row->user_text, $this->userSets['edits'] ) ) {
404                $this->userSets['last'][$row->user_text] = $row->timestamp;
405                $this->userSets['edits'][$row->user_text] = 0;
406                $this->userSets['ids'][$row->user_text] = $row->user ?? 0;
407                $this->userSets['infosets'][$row->user_text] = [];
408                $this->userSets['agentsets'][$row->user_text] = [];
409                $this->userSets['clienthints'][$row->user_text] = new ClientHintsReferenceIds();
410            }
411            if ( $this->displayClientHints ) {
412                $referenceIdsForLookup->addReferenceIds(
413                    $row->client_hints_reference_id,
414                    $row->client_hints_reference_type
415                );
416                $this->userSets['clienthints'][$row->user_text]->addReferenceIds(
417                    $row->client_hints_reference_id,
418                    $row->client_hints_reference_type
419                );
420            }
421            $this->userSets['edits'][$row->user_text]++;
422            $this->userSets['first'][$row->user_text] = $row->timestamp;
423            // Prettify IP
424            $formattedIP = IPUtils::prettifyIP( $row->ip ) ?? $row->ip;
425            // Treat blank or NULL xffs as empty strings
426            $xff = empty( $row->xff ) ? null : $row->xff;
427            $xff_ip_combo = [ $formattedIP, $xff ];
428            // Add this IP/XFF combo for this username if it's not already there
429            if ( !in_array( $xff_ip_combo, $this->userSets['infosets'][$row->user_text] ) ) {
430                $this->userSets['infosets'][$row->user_text][] = $xff_ip_combo;
431            }
432            // Add this agent string if it's not already there; 10 max.
433            if ( count( $this->userSets['agentsets'][$row->user_text] ) < 10 ) {
434                if ( !in_array( $row->agent, $this->userSets['agentsets'][$row->user_text] ) ) {
435                    $this->userSets['agentsets'][$row->user_text][] = $row->agent;
436                }
437            }
438        }
439
440        // Lookup the Client Hints data objects from the DB
441        // and then batch format the ClientHintsData objects
442        // for display.
443        if ( $this->displayClientHints ) {
444            $this->clientHintsLookupResults = $this->clientHintsLookup
445                ->getClientHintsByReferenceIds( $referenceIdsForLookup );
446        }
447    }
448
449    /** @inheritDoc */
450    public function getQueryInfo( ?string $table = null ): array {
451        if ( $table === null ) {
452            throw new LogicException(
453                "This ::getQueryInfo method must be provided with the table to generate " .
454                "the correct query info"
455            );
456        }
457
458        if ( $table === self::CHANGES_TABLE ) {
459            $queryInfo = $this->getQueryInfoForCuChanges();
460        } elseif ( $table === self::LOG_EVENT_TABLE ) {
461            $queryInfo = $this->getQueryInfoForCuLogEvent();
462        } elseif ( $table === self::PRIVATE_LOG_EVENT_TABLE ) {
463            $queryInfo = $this->getQueryInfoForCuPrivateEvent();
464        }
465
466        // Apply index and IP WHERE conditions.
467        $queryInfo['options']['USE INDEX'] = [
468            $table => $this->checkUserLookupUtils->getIndexName( $this->xfor, $table )
469        ];
470        $ipExpr = $this->checkUserLookupUtils->getIPTargetExpr( $this->target->getName(), $this->xfor, $table );
471        if ( $ipExpr !== null ) {
472            $queryInfo['conds'][] = $ipExpr;
473        }
474
475        return $queryInfo;
476    }
477
478    /** @inheritDoc */
479    protected function getQueryInfoForCuChanges(): array {
480        $queryInfo = [
481            'fields' => [
482                'timestamp' => 'cuc_timestamp',
483                'ip' => 'cuc_ip',
484                'agent' => 'cuc_agent',
485                'xff' => 'cuc_xff',
486                'actor' => 'cuc_actor',
487                'user' => 'actor_cuc_actor.actor_user',
488                'user_text' => 'actor_cuc_actor.actor_name',
489            ],
490            'tables' => [ 'cu_changes', 'actor_cuc_actor' => 'actor' ],
491            'conds' => [],
492            'join_conds' => [ 'actor_cuc_actor' => [ 'JOIN', 'actor_cuc_actor.actor_id=cuc_actor' ] ],
493            'options' => [],
494        ];
495        // When reading new, only select results from cu_changes that are
496        // for read new (defined as those with cuc_only_for_read_old set to 0).
497        if ( $this->eventTableReadNew ) {
498            $queryInfo['conds']['cuc_only_for_read_old'] = 0;
499        }
500        // When displaying Client Hints data, add the reference type and reference ID to each row.
501        if ( $this->displayClientHints ) {
502            $queryInfo['fields']['client_hints_reference_id'] =
503                UserAgentClientHintsManager::IDENTIFIER_TO_COLUMN_NAME_MAP[
504                    UserAgentClientHintsManager::IDENTIFIER_CU_CHANGES
505                ];
506            $queryInfo['fields']['client_hints_reference_type'] =
507                UserAgentClientHintsManager::IDENTIFIER_CU_CHANGES;
508        }
509        return $queryInfo;
510    }
511
512    /** @inheritDoc */
513    protected function getQueryInfoForCuLogEvent(): array {
514        $queryInfo = [
515            'fields' => [
516                'timestamp' => 'cule_timestamp',
517                'ip' => 'cule_ip',
518                'agent' => 'cule_agent',
519                'xff' => 'cule_xff',
520                'actor' => 'cule_actor',
521                'user' => 'actor_cule_actor.actor_user',
522                'user_text' => 'actor_cule_actor.actor_name',
523            ],
524            'tables' => [ 'cu_log_event', 'actor_cule_actor' => 'actor' ],
525            'conds' => [],
526            'join_conds' => [ 'actor_cule_actor' => [ 'JOIN', 'actor_cule_actor.actor_id=cule_actor' ] ],
527            'options' => [],
528        ];
529        // When displaying Client Hints data, add the reference type and reference ID to each row.
530        if ( $this->displayClientHints ) {
531            $queryInfo['fields']['client_hints_reference_id'] =
532                UserAgentClientHintsManager::IDENTIFIER_TO_COLUMN_NAME_MAP[
533                    UserAgentClientHintsManager::IDENTIFIER_CU_LOG_EVENT
534                ];
535            $queryInfo['fields']['client_hints_reference_type'] =
536                UserAgentClientHintsManager::IDENTIFIER_CU_LOG_EVENT;
537        }
538        return $queryInfo;
539    }
540
541    /** @inheritDoc */
542    protected function getQueryInfoForCuPrivateEvent(): array {
543        $queryInfo = [
544            'fields' => [
545                'timestamp' => 'cupe_timestamp',
546                'ip' => 'cupe_ip',
547                'agent' => 'cupe_agent',
548                'xff' => 'cupe_xff',
549                'actor' => 'cupe_actor',
550                'user' => 'actor_cupe_actor.actor_user',
551                'user_text' => 'actor_cupe_actor.actor_name',
552            ],
553            'tables' => [ 'cu_private_event', 'actor_cupe_actor' => 'actor' ],
554            'conds' => [],
555            'join_conds' => [ 'actor_cupe_actor' => [ 'LEFT JOIN', 'actor_cupe_actor.actor_id=cupe_actor' ] ],
556            'options' => [],
557        ];
558        // When displaying Client Hints data, add the reference type and reference ID to each row.
559        if ( $this->displayClientHints ) {
560            $queryInfo['fields']['client_hints_reference_id'] =
561                UserAgentClientHintsManager::IDENTIFIER_TO_COLUMN_NAME_MAP[
562                    UserAgentClientHintsManager::IDENTIFIER_CU_PRIVATE_EVENT
563                ];
564            $queryInfo['fields']['client_hints_reference_type'] =
565                UserAgentClientHintsManager::IDENTIFIER_CU_PRIVATE_EVENT;
566        }
567        return $queryInfo;
568    }
569
570    /** @inheritDoc */
571    protected function getStartBody(): string {
572        $s = $this->getCheckUserHelperFieldsetHTML() . $this->getNavigationBar();
573        if ( $this->mResult->numRows() ) {
574            $s .= ( new ListToggle( $this->getOutput() ) )->getHTML();
575        }
576        if ( $this->canPerformBlocks ) {
577            $s .= Xml::openElement(
578                'form',
579                [
580                    'action' => $this->getPageTitle()->getLocalURL( 'action=block' ),
581                    'id' => 'checkuserblock',
582                    'name' => 'checkuserblock',
583                    'class' => 'mw-htmlform-ooui mw-htmlform',
584                    'method' => 'post',
585                ]
586            );
587        }
588
589        $divClasses = [ 'mw-checkuser-get-users-results' ];
590
591        if ( $this->displayClientHints ) {
592            // Class used to indicate whether Client Hints are enabled
593            // TODO: Remove this class and old CSS code once display
594            // is on all wikis (T341110).
595            $divClasses[] = 'mw-checkuser-clienthints-enabled-temporary-class';
596        }
597
598        $s .= Xml::openElement(
599            'div',
600            [
601                'id' => 'checkuserresults',
602                'class' => implode( ' ', $divClasses )
603            ]
604        );
605
606        $s .= '<ul>';
607
608        return $s;
609    }
610
611    /** @inheritDoc */
612    protected function getEndBody(): string {
613        $fieldset = new HTMLFieldsetCheckUser( [], $this->getContext(), '' );
614        $s = '</ul></div>';
615        if ( $this->mResult->numRows() ) {
616            $s .= ( new ListToggle( $this->getOutput() ) )->getHTML();
617        }
618        // T314217 - cannot have forms inside of forms.
619        // $s .= $this->getNavigationBar();
620        if ( $this->canPerformBlocks ) {
621            $config = $this->getConfig();
622            $checkUserCAMultiLock = $config->get( 'CheckUserCAMultiLock' );
623            if ( $checkUserCAMultiLock !== false ) {
624                if ( !ExtensionRegistry::getInstance()->isLoaded( 'CentralAuth' ) ) {
625                    // $wgCheckUserCAMultiLock shouldn't be enabled if CA is not loaded
626                    throw new ConfigException( '$wgCheckUserCAMultiLock requires CentralAuth extension.' );
627                }
628
629                $caUserGroups = CentralAuthUser::getInstance( $this->getUser() )->getGlobalGroups();
630                // Only load the script for users in the configured global group(s)
631                if ( count( array_intersect( $checkUserCAMultiLock['groups'], $caUserGroups ) ) ) {
632                    $out = $this->getOutput();
633                    $centralMLUrl = WikiMap::getForeignURL(
634                        $checkUserCAMultiLock['centralDB'],
635                        // Use canonical name instead of local name so that it works
636                        // even if the local language is different from central wiki
637                        'Special:MultiLock'
638                    );
639                    if ( $centralMLUrl === false ) {
640                        throw new ConfigException(
641                            "Could not retrieve URL for {$checkUserCAMultiLock['centralDB']}"
642                        );
643                    }
644                    $out->addJsConfigVars( 'wgCUCAMultiLockCentral', $centralMLUrl );
645                    $out->addModules( 'ext.checkUser' );
646                }
647            }
648
649            $fields = [
650                'usetag' => [
651                    'type' => 'check',
652                    'default' => false,
653                    'label-message' => 'checkuser-blocktag',
654                    'id' => 'usetag',
655                    'name' => 'usetag',
656                    'size' => 46,
657                ],
658                'tag' => [
659                    'type' => 'text',
660                    'id' => 'blocktag',
661                    'name' => 'blocktag',
662                    'minlength' => 3,
663                ],
664                'talkusetag' => [
665                    'type' => 'check',
666                    'default' => false,
667                    'label-message' => 'checkuser-blocktag-talk',
668                    'id' => 'usettag',
669                    'name' => 'usettag',
670                ],
671                'talktag' => [
672                    'type' => 'text',
673                    'id' => 'talktag',
674                    'name' => 'talktag',
675                    'size' => 46,
676                    'minlength' => 3,
677                ],
678            ];
679
680            $fieldset->addFields( $fields )
681                ->setWrapperLegendMsg( 'checkuser-massblock' )
682                ->setSubmitTextMsg( 'checkuser-massblock-commit' )
683                ->setSubmitId( 'checkuserblocksubmit' )
684                ->setSubmitName( 'checkuserblock' )
685                ->setHeaderHtml( $this->msg( 'checkuser-massblock-text' )->escaped() );
686
687            if ( $config->get( 'BlockAllowsUTEdit' ) ) {
688                $fieldset->addFields( [
689                    'blocktalk' => [
690                        'type' => 'check',
691                        'default' => false,
692                        'label-message' => 'checkuser-blocktalk',
693                        'id' => 'blocktalk',
694                        'name' => 'blocktalk',
695                    ]
696                ] );
697            }
698
699            if (
700                $this->blockPermissionCheckerFactory
701                    ->newBlockPermissionChecker(
702                        null,
703                        $this->getUser()
704                    )
705                    ->checkEmailPermissions()
706            ) {
707                $fieldset->addFields( [
708                    'blockemail' => [
709                        'type' => 'check',
710                        'default' => false,
711                        'label-message' => 'checkuser-blockemail',
712                        'id' => 'blockemail',
713                        'name' => 'blockemail',
714                    ]
715                ] );
716            }
717
718            $s .= $fieldset
719                ->addFields( [
720                    'reblock' => [
721                        'type' => 'check',
722                        'default' => false,
723                        'label-message' => 'checkuser-reblock',
724                        'id' => 'reblock',
725                        'name' => 'reblock',
726                    ],
727                    'reason' => [
728                        'type' => 'selectandother',
729                        'options-message' => 'checkuser-block-reason-dropdown',
730                        'label-message' => 'checkuser-reason',
731                        'size' => 46,
732                        'maxlength' => 150,
733                        'id' => 'blockreason',
734                        'name' => 'blockreason',
735                        'cssclass' => 'ext-checkuser-checkuserblock-block-reason'
736                    ],
737                ] )
738                ->prepareForm()
739                ->getHtml( false );
740            $s .= '</form>';
741        }
742
743        return $s;
744    }
745}