Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.66% covered (warning)
89.66%
364 / 406
69.23% covered (warning)
69.23%
9 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
CheckUserGetActionsPager
89.66% covered (warning)
89.66%
364 / 406
69.23% covered (warning)
69.23%
9 / 13
87.09
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 formatRow
88.00% covered (warning)
88.00%
66 / 75
0.00% covered (danger)
0.00%
0 / 1
21.76
 getActionText
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getLinksFromRow
74.74% covered (warning)
74.74%
71 / 95
0.00% covered (danger)
0.00%
0 / 1
23.22
 preCacheMessages
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 buildUserLinks
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 getQueryInfo
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
7
 getQueryInfoForCuChanges
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
3
 getQueryInfoForCuLogEvent
100.00% covered (success)
100.00%
47 / 47
100.00% covered (success)
100.00%
1 / 1
3
 getQueryInfoForCuPrivateEvent
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
1 / 1
4
 getStartBody
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 preprocessResults
83.72% covered (warning)
83.72%
36 / 43
0.00% covered (danger)
0.00%
0 / 1
14.85
 isNavigationBarShown
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\CheckUser\CheckUser\Pagers;
4
5use HtmlArmor;
6use IContextSource;
7use LogEventsList;
8use LogFormatter;
9use LogicException;
10use LogPage;
11use ManualLogEntry;
12use MediaWiki\Cache\LinkBatchFactory;
13use MediaWiki\CheckUser\CheckUser\SpecialCheckUser;
14use MediaWiki\CheckUser\ClientHints\ClientHintsBatchFormatterResults;
15use MediaWiki\CheckUser\ClientHints\ClientHintsReferenceIds;
16use MediaWiki\CheckUser\Hook\HookRunner;
17use MediaWiki\CheckUser\Services\CheckUserLogService;
18use MediaWiki\CheckUser\Services\CheckUserLookupUtils;
19use MediaWiki\CheckUser\Services\CheckUserUtilityService;
20use MediaWiki\CheckUser\Services\TokenQueryManager;
21use MediaWiki\CheckUser\Services\UserAgentClientHintsFormatter;
22use MediaWiki\CheckUser\Services\UserAgentClientHintsLookup;
23use MediaWiki\CheckUser\Services\UserAgentClientHintsManager;
24use MediaWiki\CommentFormatter\CommentFormatter;
25use MediaWiki\CommentStore\CommentStore;
26use MediaWiki\Html\FormOptions;
27use MediaWiki\Html\Html;
28use MediaWiki\Linker\Linker;
29use MediaWiki\Linker\LinkRenderer;
30use MediaWiki\Logger\LoggerFactory;
31use MediaWiki\Revision\RevisionRecord;
32use MediaWiki\SpecialPage\SpecialPage;
33use MediaWiki\SpecialPage\SpecialPageFactory;
34use MediaWiki\Title\Title;
35use MediaWiki\User\CentralId\CentralIdLookup;
36use MediaWiki\User\UserEditTracker;
37use MediaWiki\User\UserFactory;
38use MediaWiki\User\UserGroupManager;
39use MediaWiki\User\UserIdentity;
40use MediaWiki\User\UserIdentityLookup;
41use MediaWiki\User\UserIdentityValue;
42use Psr\Log\LoggerInterface;
43use stdClass;
44use Wikimedia\IPUtils;
45use Wikimedia\Rdbms\IConnectionProvider;
46
47class CheckUserGetActionsPager extends AbstractCheckUserPager {
48
49    /**
50     * @var string[] Used to cache frequently used messages
51     */
52    protected array $message = [];
53
54    /**
55     * @var array The cached results of AbstractCheckUserPager::userBlockFlags with the key as
56     *  the row's user_text.
57     */
58    private array $flagCache = [];
59
60    /** @var array A map of revision IDs to the formatted comment associated with that revision. */
61    protected array $formattedRevisionComments = [];
62
63    /** @var array A map of revision IDs to whether the user is hidden. */
64    protected array $usernameVisibility = [];
65
66    /**
67     * @var ClientHintsBatchFormatterResults Formatted ClientHintsData objects that can be looked up by a reference ID.
68     */
69    protected ClientHintsBatchFormatterResults $formattedClientHintsData;
70
71    private LoggerInterface $logger;
72    private LinkBatchFactory $linkBatchFactory;
73    private CommentFormatter $commentFormatter;
74    private UserEditTracker $userEditTracker;
75    private HookRunner $hookRunner;
76    private CheckUserUtilityService $checkUserUtilityService;
77    private CommentStore $commentStore;
78    private UserAgentClientHintsLookup $clientHintsLookup;
79    private UserAgentClientHintsFormatter $clientHintsFormatter;
80
81    /**
82     * @param FormOptions $opts
83     * @param UserIdentity $target
84     * @param bool|null $xfor
85     * @param string $logType
86     * @param TokenQueryManager $tokenQueryManager
87     * @param UserGroupManager $userGroupManager
88     * @param CentralIdLookup $centralIdLookup
89     * @param LinkBatchFactory $linkBatchFactory
90     * @param IConnectionProvider $dbProvider
91     * @param SpecialPageFactory $specialPageFactory
92     * @param UserIdentityLookup $userIdentityLookup
93     * @param UserFactory $userFactory
94     * @param CheckUserLookupUtils $checkUserLookupUtils
95     * @param CheckUserLogService $checkUserLogService
96     * @param CommentFormatter $commentFormatter
97     * @param UserEditTracker $userEditTracker
98     * @param HookRunner $hookRunner
99     * @param CheckUserUtilityService $checkUserUtilityService
100     * @param CommentStore $commentStore
101     * @param UserAgentClientHintsLookup $clientHintsLookup
102     * @param UserAgentClientHintsFormatter $clientHintsFormatter
103     * @param IContextSource|null $context
104     * @param LinkRenderer|null $linkRenderer
105     * @param ?int $limit
106     */
107    public function __construct(
108        FormOptions $opts,
109        UserIdentity $target,
110        ?bool $xfor,
111        string $logType,
112        TokenQueryManager $tokenQueryManager,
113        UserGroupManager $userGroupManager,
114        CentralIdLookup $centralIdLookup,
115        LinkBatchFactory $linkBatchFactory,
116        IConnectionProvider $dbProvider,
117        SpecialPageFactory $specialPageFactory,
118        UserIdentityLookup $userIdentityLookup,
119        UserFactory $userFactory,
120        CheckUserLookupUtils $checkUserLookupUtils,
121        CheckUserLogService $checkUserLogService,
122        CommentFormatter $commentFormatter,
123        UserEditTracker $userEditTracker,
124        HookRunner $hookRunner,
125        CheckUserUtilityService $checkUserUtilityService,
126        CommentStore $commentStore,
127        UserAgentClientHintsLookup $clientHintsLookup,
128        UserAgentClientHintsFormatter $clientHintsFormatter,
129        IContextSource $context = null,
130        LinkRenderer $linkRenderer = null,
131        ?int $limit = null
132    ) {
133        parent::__construct( $opts, $target, $logType, $tokenQueryManager,
134            $userGroupManager, $centralIdLookup, $dbProvider, $specialPageFactory,
135            $userIdentityLookup, $checkUserLogService, $userFactory, $checkUserLookupUtils,
136            $context, $linkRenderer, $limit );
137        $this->checkType = SpecialCheckUser::SUBTYPE_GET_ACTIONS;
138        $this->logger = LoggerFactory::getInstance( 'CheckUser' );
139        $this->xfor = $xfor;
140        $this->linkBatchFactory = $linkBatchFactory;
141        $this->commentFormatter = $commentFormatter;
142        $this->userEditTracker = $userEditTracker;
143        $this->hookRunner = $hookRunner;
144        $this->checkUserUtilityService = $checkUserUtilityService;
145        $this->commentStore = $commentStore;
146        $this->clientHintsLookup = $clientHintsLookup;
147        $this->clientHintsFormatter = $clientHintsFormatter;
148        $this->preCacheMessages();
149        $this->mGroupByDate = true;
150    }
151
152    /**
153     * Get a streamlined recent changes line with IP data
154     *
155     * @inheritDoc
156     */
157    public function formatRow( $row ): string {
158        $templateParams = [];
159        // Show date
160        $templateParams['timestamp'] =
161            $this->getLanguage()->userTime( wfTimestamp( TS_MW, $row->timestamp ), $this->getUser() );
162        // Use the IP as the $user_text if the actor ID is NULL and the IP is not NULL (T353953).
163        if ( $row->actor === null && $row->ip ) {
164            $row->user_text = $row->ip;
165        }
166        // Normalise user text if IP for clarity and compatibility with ipLink below
167        $user_text = $row->user_text;
168        '@phan-var string $user_text';
169        if ( IPUtils::isIPAddress( $user_text ) ) {
170            $user_text = IPUtils::prettifyIP( $user_text ) ?? $user_text;
171        }
172        $user = new UserIdentityValue( $row->user ?? 0, $user_text );
173        // Get a ManualLogEntry instance if the row is a log entry
174        $logEntry = null;
175        if ( $row->type == RC_LOG && $this->eventTableReadNew && $row->log_type ) {
176            $logEntry = $this->checkUserLookupUtils->getManualLogEntryFromRow( $row, $user );
177        }
178        // Userlinks
179        $userIsHidden = false;
180        if ( $row->type == RC_EDIT || $row->type == RC_NEW ) {
181            $userIsHidden = !( $this->usernameVisibility[$row->this_oldid] ?? true );
182        } elseif ( $logEntry !== null ) {
183            // Specifically using LogEventsList::userCanBitfield here instead of ::userCan because we still want
184            // to show the username if the authority cannot see logs from this log type but the user is otherwise
185            // visible.
186            $userIsHidden = !LogEventsList::userCanBitfield(
187                $row->log_deleted,
188                LogPage::DELETED_USER,
189                $this->getAuthority()
190            );
191        }
192        if ( !$userIsHidden ) {
193            // If the user was not hidden for the specific edit or log, check if the user is hidden in general via
194            // a block with 'hideuser' enabled.
195            $userIsHidden = $this->userFactory->newFromUserIdentity( $user )->isHidden()
196                && !$this->getAuthority()->isAllowed( 'hideuser' );
197        }
198        // Create diff/hist/page links
199        $templateParams['links'] = $this->getLinksFromRow( $row, $user, $logEntry );
200        $templateParams['showLinks'] = $templateParams['links'] !== '';
201        if ( $userIsHidden ) {
202            $templateParams['userLink'] = Html::element(
203                'span',
204                [ 'class' => 'history-deleted' ],
205                $this->msg( 'rev-deleted-user' )->text()
206            );
207        } else {
208            if ( !IPUtils::isIPAddress( $user ) && !$user->isRegistered() ) {
209                $templateParams['userLinkClass'] = 'mw-checkuser-nonexistent-user';
210            }
211            $userLinks = self::buildUserLinks(
212                $user->getId(),
213                $user_text,
214                $this->userEditTracker->getUserEditCount( $user )
215            );
216            $templateParams['userLink'] = $userLinks['userLink'];
217            $templateParams['userToolLinks'] = $userLinks['userToolLinks'];
218            // Add any block information
219            $templateParams['flags'] = $this->flagCache[$row->user_text];
220        }
221        $templateParams['actionText'] = $this->getActionText( $row, $logEntry );
222        // Comment
223        if ( $row->type == RC_EDIT || $row->type == RC_NEW ) {
224            $templateParams['comment'] = $this->formattedRevisionComments[$row->this_oldid] ?? '';
225        } else {
226            // If we have a log entry, then check if the comment is hidden in the log entry. Otherwise, we should be
227            // okay to display it (because the checks for the comment being hidden for an edit is done in the lines
228            // above).
229            $commentVisible = $logEntry === null ||
230                LogEventsList::userCan( $row, LogPage::DELETED_COMMENT, $this->getAuthority() );
231            if ( $commentVisible ) {
232                $comment = $this->commentStore->getComment( 'comment', $row )->text;
233            } else {
234                $comment = '';
235            }
236            $templateParams['comment'] = $this->commentFormatter->formatBlock( $comment );
237        }
238        // IP
239        $ip = IPUtils::prettifyIP( $row->ip ) ?? $row->ip ?? '';
240        $templateParams['ipLink'] = $this->getSelfLink( $ip,
241            [
242                'user' => $ip,
243                'reason' => $this->opts->getValue( 'reason' )
244            ]
245        );
246        // XFF
247        if ( $row->xff != null ) {
248            // Flag our trusted proxies
249            [ $client ] = $this->checkUserUtilityService->getClientIPfromXFF( $row->xff );
250            // XFF was trusted if client came from it
251            $trusted = ( $client === $row->ip );
252            $templateParams['xffTrusted'] = $trusted;
253            $templateParams['xff'] = $this->getSelfLink( $row->xff,
254                [
255                    'user' => $client . '/xff',
256                    'reason' => $this->opts->getValue( 'reason' )
257                ]
258            );
259        }
260        // User agent
261        $templateParams['userAgent'] = $row->agent;
262        // Display Client Hints data if display is enabled
263        if ( $this->displayClientHints ) {
264            // If ::getStringForReferenceId returns null, the mustache template will
265            // interpret this as false and then not display the Client Hints data
266            // in the same way that if $this->displayClientHints data was false.
267            $templateParams['clientHints'] = $this->formattedClientHintsData->getStringForReferenceId(
268                $row->client_hints_reference_id,
269                $row->client_hints_reference_type
270            );
271        }
272
273        return $this->templateParser->processTemplate( 'GetActionsLine', $templateParams );
274    }
275
276    /**
277     * Gets the actiontext associated with the given $row.
278     *
279     * @param stdClass $row The database row
280     * @param ManualLogEntry|null $logEntry The log entry associated with this row, otherwise null.
281     * @return string The actiontext
282     */
283    private function getActionText( stdClass $row, ?ManualLogEntry $logEntry ): string {
284        if ( $logEntry !== null ) {
285            // Log action text taken from the LogFormatter for the entry being displayed.
286            $logFormatter = LogFormatter::newFromEntry( $logEntry );
287            $logFormatter->setAudience( LogFormatter::FOR_THIS_USER );
288            return $logFormatter->getActionText();
289        } else {
290            // Action text, hackish ...
291            return $this->commentFormatter->format( $row->actiontext ?? '' );
292        }
293    }
294
295    /**
296     * @param stdClass $row
297     * @param UserIdentity $performer The user that performed the action represented by this row.
298     * @param ?ManualLogEntry $logEntry The log entry associated with this row, otherwise null.
299     * @return string diff, hist and page other links related to the change
300     */
301    protected function getLinksFromRow( stdClass $row, UserIdentity $performer, ?ManualLogEntry $logEntry ): string {
302        $links = [];
303        // Log items
304        // Due to T315224 triple equals for type does not work for sqlite.
305        if ( $row->type == RC_LOG ) {
306            $title = Title::makeTitle( $row->namespace, $row->title );
307            $links['log'] = '';
308            if ( $this->eventTableReadNew && isset( $row->log_id ) && $row->log_id ) {
309                $links['log'] = Html::rawElement( 'span', [],
310                    $this->getLinkRenderer()->makeKnownLink(
311                        SpecialPage::getTitleFor( 'Log' ),
312                        new HtmlArmor( $this->message['checkuser-log-link-text'] ),
313                        [],
314                        [ 'logid' => $row->log_id ]
315                    )
316                );
317            }
318            // Hide the 'logs' link if the page is a username and the current authority does not have permission to see
319            // the username in question (T361479).
320            $hidden = false;
321            if ( $title->getNamespace() === NS_USER ) {
322                $user = $this->userFactory->newFromName( $title->getBaseText() );
323                if ( $logEntry !== null && $performer->getName() === $title->getText() ) {
324                    // If the username of the performer is the same as the title, we can also check whether the
325                    // performer of the log entry is hidden.
326                    $hidden = !LogEventsList::userCanBitfield(
327                        $logEntry->getDeleted(),
328                        LogPage::DELETED_USER,
329                        $this->getContext()->getAuthority()
330                    );
331                }
332                if ( $user !== null && !$hidden ) {
333                    // If LogEventsList::userCanBitfield said the log entry isn't hidden, then also check if the user
334                    // is hidden in general (via a block with 'hideuser' set).
335                    // LogEventsList::userCanBitfield can return false while this is true for events from
336                    // cu_private_event, as log_deleted is always 0 for those rows (as they cannot be revision deleted).
337                    $hidden = $user->isHidden() && !$this->getAuthority()->isAllowed( 'hideuser' );
338                }
339            }
340            if ( !$hidden ) {
341                $links['log'] .= Html::rawElement( 'span', [],
342                    $this->getLinkRenderer()->makeKnownLink(
343                        SpecialPage::getTitleFor( 'Log' ),
344                        new HtmlArmor( $this->message['checkuser-logs-link-text'] ),
345                        [],
346                        [ 'page' => $title->getPrefixedText() ]
347                    )
348                );
349            }
350            // Only add the log related links if we have any to add. There may be none for cu_private_event rows when
351            // the username listed as the title is blocked with 'hideuser' enabled.
352            if ( $links['log'] !== '' ) {
353                $links['log'] = Html::rawElement(
354                    'span',
355                    [ 'class' => 'mw-changeslist-links' ],
356                    $links['log']
357                );
358            }
359        } else {
360            $title = Title::makeTitle( $row->namespace, $row->title );
361            // New pages
362            if ( $row->type == RC_NEW ) {
363                $links['diffHistLinks'] = Html::rawElement( 'span', [], $this->message['diff'] );
364            } else {
365                // Diff link
366                $links['diffHistLinks'] = Html::rawElement( 'span', [],
367                    $this->getLinkRenderer()->makeKnownLink(
368                        $title,
369                        new HtmlArmor( $this->message['diff'] ),
370                        [],
371                        [
372                            'curid' => $row->page_id,
373                            'diff' => $row->this_oldid,
374                            'oldid' => $row->last_oldid
375                        ]
376                    )
377                );
378            }
379            // History link
380            $links['diffHistLinks'] .= ' ' . Html::rawElement( 'span', [],
381                $this->getLinkRenderer()->makeKnownLink(
382                    $title,
383                    new HtmlArmor( $this->message['hist'] ),
384                    [],
385                    [
386                        'curid' => $title->exists() ? $row->page_id : null,
387                        'action' => 'history'
388                    ]
389                )
390            );
391            $links['diffHistLinks'] = Html::rawElement(
392                'span',
393                [ 'class' => 'mw-changeslist-links' ],
394                $links['diffHistLinks']
395            );
396            $links['diffHistLinksSeparator'] = Html::element(
397                'span',
398                [ 'class' => 'mw-changeslist-separator' ]
399            );
400            // Some basic flags
401            if ( $row->type == RC_NEW ) {
402                $links['newpage'] = Html::rawElement(
403                    'abbr',
404                    [ 'class' => 'newpage' ],
405                    $this->message['newpageletter']
406                );
407            }
408            if ( $row->minor ) {
409                $links['minor'] = Html::rawElement(
410                    "abbr",
411                    [ 'class' => 'minoredit' ],
412                    $this->message['minoreditletter']
413                );
414            }
415            // Page link
416            $links['title'] = $this->getLinkRenderer()->makeLink( $title );
417        }
418
419        $this->hookRunner->onSpecialCheckUserGetLinksFromRow( $this, $row, $links );
420        if ( is_array( $links ) ) {
421            return implode( ' ', $links );
422        } else {
423            $this->logger->warning(
424                __METHOD__ . ': Expected array from SpecialCheckUserGetLinksFromRow $links param,'
425                . ' but received ' . gettype( $links )
426            );
427            return '';
428        }
429    }
430
431    /**
432     * As we use the same small set of messages in various methods and that
433     * they are called often, we call them once and save them in $this->message
434     */
435    protected function preCacheMessages() {
436        if ( $this->message === [] ) {
437            $msgKeys = [
438                'diff', 'hist', 'minoreditletter', 'newpageletter',
439                'blocklink', 'checkuser-log-link-text', 'checkuser-logs-link-text'
440            ];
441            foreach ( $msgKeys as $msg ) {
442                $this->message[$msg] = $this->msg( $msg )->escaped();
443            }
444        }
445    }
446
447    /**
448     * Build (or fetch, if previously built) links for a user
449     *
450     * @param int $userId User identifier
451     * @param string $userText User name or IP address
452     * @param ?int $edits User edit count
453     *
454     * @return array{userLink:string,userToolLinks:string} A map with two keys, forwarding the supplied arguments:
455     *   - userLink: (string) the result of Linker::userLink
456     *   - userToolLinks: (string) the result of Linker::userToolLinksRedContribs
457     */
458    protected static function buildUserLinks( int $userId, string $userText, ?int $edits ): array {
459        static $cache = [];
460        if ( !isset( $cache[$userText] ) ) {
461            // Simple enough to keep as an associative array instead of a data class
462            $cache[$userText] = [
463                "userLink" => Linker::userLink( $userId, $userText, $userText ),
464                "userToolLinks" => Linker::userToolLinksRedContribs(
465                    $userId,
466                    $userText,
467                    $edits,
468                    // don't render parentheses in HTML markup (CSS will provide)
469                    false
470                )
471            ];
472        }
473
474        return $cache[$userText];
475    }
476
477    /** @inheritDoc */
478    public function getQueryInfo( ?string $table = null ): array {
479        if ( $table === null ) {
480            throw new LogicException(
481                "This ::getQueryInfo method must be provided with the table to generate " .
482                "the correct query info"
483            );
484        }
485
486        if ( $table === self::CHANGES_TABLE ) {
487            $queryInfo = $this->getQueryInfoForCuChanges();
488        } elseif ( $table === self::LOG_EVENT_TABLE ) {
489            $queryInfo = $this->getQueryInfoForCuLogEvent();
490        } elseif ( $table === self::PRIVATE_LOG_EVENT_TABLE ) {
491            $queryInfo = $this->getQueryInfoForCuPrivateEvent();
492        }
493
494        $queryInfo['options']['USE INDEX'] = [
495            $table => $this->checkUserLookupUtils->getIndexName( $this->xfor, $table )
496        ];
497
498        if ( $this->xfor === null ) {
499            $queryInfo['conds']['actor_user'] = $this->target->getId();
500        } else {
501            $ipExpr = $this->checkUserLookupUtils->getIPTargetExpr( $this->target->getName(), $this->xfor, $table );
502            if ( $ipExpr !== null ) {
503                $queryInfo['conds'][] = $ipExpr;
504            }
505        }
506        return $queryInfo;
507    }
508
509    /** @inheritDoc */
510    protected function getQueryInfoForCuChanges(): array {
511        $commentQuery = $this->commentStore->getJoin( 'cuc_comment' );
512        $queryInfo = [
513            'fields' => [
514                'namespace' => 'cuc_namespace',
515                'title' => 'cuc_title',
516                'actiontext' => 'cuc_actiontext',
517                'timestamp' => 'cuc_timestamp',
518                'minor' => 'cuc_minor',
519                'page_id' => 'cuc_page_id',
520                'type' => 'cuc_type',
521                'this_oldid' => 'cuc_this_oldid',
522                'last_oldid' => 'cuc_last_oldid',
523                'ip' => 'cuc_ip',
524                'xff' => 'cuc_xff',
525                'agent' => 'cuc_agent',
526                'actor' => 'cuc_actor',
527                'user' => 'actor_user',
528                'user_text' => 'actor_name',
529                // Needed for rows with cuc_type as RC_LOG.
530                'comment_text',
531                'comment_data',
532            ],
533            'tables' => [ 'cu_changes', 'actor_cuc_user' => 'actor' ] + $commentQuery['tables'],
534            'conds' => [],
535            'join_conds' => [
536                'actor_cuc_user' => [ 'JOIN', 'actor_cuc_user.actor_id=cuc_actor' ]
537            ] + $commentQuery['joins'],
538            'options' => [],
539        ];
540        // When reading new, only select results from cu_changes that are
541        // for read new (defined as those with cuc_only_for_read_old set to 0).
542        if ( $this->eventTableReadNew ) {
543            $queryInfo['conds']['cuc_only_for_read_old'] = 0;
544        }
545        // When displaying Client Hints data, add the reference type and reference ID to each row.
546        if ( $this->displayClientHints ) {
547            $queryInfo['fields']['client_hints_reference_id'] =
548                UserAgentClientHintsManager::IDENTIFIER_TO_COLUMN_NAME_MAP[
549                    UserAgentClientHintsManager::IDENTIFIER_CU_CHANGES
550                ];
551            $queryInfo['fields']['client_hints_reference_type'] = UserAgentClientHintsManager::IDENTIFIER_CU_CHANGES;
552        }
553        return $queryInfo;
554    }
555
556    /** @inheritDoc */
557    protected function getQueryInfoForCuLogEvent(): array {
558        $commentQuery = $this->commentStore->getJoin( 'log_comment' );
559        $queryInfo = [
560            'fields' => [
561                'timestamp' => 'cule_timestamp',
562                'title' => 'log_title',
563                'page_id' => 'log_page',
564                'namespace' => 'log_namespace',
565                'ip' => 'cule_ip',
566                'ip_hex' => 'cule_ip_hex',
567                'xff' => 'cule_xff',
568                'xff_hex' => 'cule_xff_hex',
569                'agent' => 'cule_agent',
570                'actor' => 'cule_actor',
571                'user' => 'actor_user',
572                'user_text' => 'actor_name',
573                'comment_text',
574                'comment_data',
575                'log_type' => 'log_type',
576                'log_action' => 'log_action',
577                'log_params' => 'log_params',
578                'log_deleted' => 'log_deleted',
579                'log_id' => 'cule_log_id',
580            ],
581            'tables' => [
582                'cu_log_event', 'logging_cule_log_id' => 'logging', 'actor_log_actor' => 'actor'
583            ] + $commentQuery['tables'],
584            'conds' => [],
585            'join_conds' => [
586                'logging_cule_log_id' => [ 'JOIN', 'logging_cule_log_id.log_id=cule_log_id' ],
587                'actor_log_actor' => [ 'JOIN', 'actor_log_actor.actor_id=cule_actor' ],
588            ] + $commentQuery['joins'],
589            'options' => [],
590        ];
591        if ( $this->mDb->getType() == 'postgres' ) {
592            // On postgres the cuc_type type is a smallint.
593            $queryInfo['fields'] += [
594                'type' => 'CAST(' . RC_LOG . ' AS smallint)'
595            ];
596        } else {
597            // Other DBs can handle converting RC_LOG to the
598            // correct type.
599            $queryInfo['fields'] += [
600                'type' => RC_LOG
601            ];
602        }
603        // When displaying Client Hints data, add the reference type and reference ID to each row.
604        if ( $this->displayClientHints ) {
605            $queryInfo['fields']['client_hints_reference_id'] =
606                UserAgentClientHintsManager::IDENTIFIER_TO_COLUMN_NAME_MAP[
607                    UserAgentClientHintsManager::IDENTIFIER_CU_LOG_EVENT
608                ];
609            $queryInfo['fields']['client_hints_reference_type'] = UserAgentClientHintsManager::IDENTIFIER_CU_LOG_EVENT;
610        }
611        return $queryInfo;
612    }
613
614    /** @inheritDoc */
615    protected function getQueryInfoForCuPrivateEvent(): array {
616        // We only need a JOIN if the target of the check is a username. For an IP we need a LEFT JOIN as
617        // the cupe_actor column may be NULL for rows we want to select.
618        $joinType = $this->xfor === null ? 'JOIN' : 'LEFT JOIN';
619        $commentQuery = $this->commentStore->getJoin( 'cupe_comment' );
620        $queryInfo = [
621            'fields' => [
622                'timestamp' => 'cupe_timestamp',
623                'title' => 'cupe_title',
624                'page_id' => 'cupe_page',
625                'namespace' => 'cupe_namespace',
626                'ip' => 'cupe_ip',
627                'ip_hex' => 'cupe_ip_hex',
628                'xff' => 'cupe_xff',
629                'xff_hex' => 'cupe_xff_hex',
630                'agent' => 'cupe_agent',
631                'actor' => 'cupe_actor',
632                'user' => 'actor_user',
633                'user_text' => 'actor_name',
634                'comment_text',
635                'comment_data',
636                'log_type' => 'cupe_log_type',
637                'log_action' => 'cupe_log_action',
638                'log_params' => 'cupe_params',
639                // cu_private_event log events cannot be deleted or suppressed.
640                'log_deleted' => 0,
641            ],
642            'tables' => [ 'cu_private_event', 'actor_cupe_actor' => 'actor' ] + $commentQuery['tables'],
643            'conds' => [],
644            'join_conds' => [
645                'actor_cupe_actor' => [ $joinType, 'actor_cupe_actor.actor_id=cupe_actor' ]
646            ] + $commentQuery['joins'],
647            'options' => [],
648        ];
649        if ( $this->mDb->getType() == 'postgres' ) {
650            // On postgres the cuc_type type is a smallint.
651            $queryInfo['fields'] += [
652                'type' => 'CAST(' . RC_LOG . ' AS smallint)'
653            ];
654        } else {
655            // Other DBs can handle converting RC_LOG to the
656            // correct type.
657            $queryInfo['fields'] += [
658                'type' => RC_LOG
659            ];
660        }
661        // When displaying Client Hints data, add the reference type and reference ID to each row.
662        if ( $this->displayClientHints ) {
663            $queryInfo['fields']['client_hints_reference_id'] =
664                UserAgentClientHintsManager::IDENTIFIER_TO_COLUMN_NAME_MAP[
665                    UserAgentClientHintsManager::IDENTIFIER_CU_PRIVATE_EVENT
666                ];
667            $queryInfo['fields']['client_hints_reference_type'] =
668                UserAgentClientHintsManager::IDENTIFIER_CU_PRIVATE_EVENT;
669        }
670        return $queryInfo;
671    }
672
673    /** @inheritDoc */
674    protected function getStartBody(): string {
675        return $this->getCheckUserHelperFieldsetHTML() . $this->getNavigationBar()
676            . '<div id="checkuserresults" class="mw-checkuser-get-actions-results mw-checkuser-get-edits-results">';
677    }
678
679    /** @inheritDoc */
680    protected function preprocessResults( $result ) {
681        $lb = $this->linkBatchFactory->newLinkBatch();
682        $lb->setCaller( __METHOD__ );
683        $revisions = [];
684        $referenceIds = new ClientHintsReferenceIds();
685        foreach ( $result as $row ) {
686            // Use the IP as the user_text if the actor ID is NULL and the IP is not NULL (T353953).
687            if ( $row->actor === null && $row->ip ) {
688                $row->user_text = $row->ip;
689            }
690            if ( $this->displayClientHints ) {
691                $referenceIds->addReferenceIds( $row->client_hints_reference_id, $row->client_hints_reference_type );
692            }
693            if ( $row->title !== '' ) {
694                $lb->add( $row->namespace, $row->title );
695            }
696            if ( $this->xfor === null ) {
697                $userText = str_replace( ' ', '_', $row->user_text );
698                $lb->add( NS_USER, $userText );
699                $lb->add( NS_USER_TALK, $userText );
700            }
701            // Add the row to the flag cache
702            if ( !isset( $this->flagCache[$row->user_text] ) ) {
703                $user = new UserIdentityValue( $row->user ?? 0, $row->user_text );
704                $ip = IPUtils::isIPAddress( $row->user_text ) ? $row->user_text : '';
705                $flags = $this->userBlockFlags( $ip, $user );
706                $this->flagCache[$row->user_text] = $flags;
707            }
708            // Batch process comments
709            if (
710                ( $row->type == RC_EDIT || $row->type == RC_NEW ) &&
711                !array_key_exists( $row->this_oldid, $revisions )
712            ) {
713                $revRecord = $this->checkUserLookupUtils->getRevisionRecordFromRow( $row );
714                if ( $revRecord !== null ) {
715                    $revisions[$row->this_oldid] = $revRecord;
716
717                    $this->usernameVisibility[$row->this_oldid] = RevisionRecord::userCanBitfield(
718                        $revRecord->getVisibility(),
719                        RevisionRecord::DELETED_USER,
720                        $this->getAuthority()
721                    );
722                }
723            }
724        }
725        // Batch format revision comments
726        $this->formattedRevisionComments = $this->commentFormatter->createRevisionBatch()
727            ->revisions( $revisions )
728            ->authority( $this->getAuthority() )
729            ->samePage( false )
730            ->useParentheses( false )
731            ->indexById()
732            ->execute();
733        $lb->execute();
734        // Lookup the Client Hints data objects from the DB
735        // and then batch format the ClientHintsData objects
736        // for display.
737        if ( $this->displayClientHints ) {
738            // When no Client Hints data was found for a edit or for all edits in the results,
739            // no associated formatted Client Hints data string will be stored in
740            // $this->formattedClientHintsData for the edits without Client Hints data.
741            // Calling the getter method will handle this by returning null.
742            $clientHintsData = $this->clientHintsLookup->getClientHintsByReferenceIds( $referenceIds );
743            $this->formattedClientHintsData = $this->clientHintsFormatter
744                ->batchFormatClientHintsData( $clientHintsData );
745        }
746        $result->seek( 0 );
747    }
748
749    /**
750     * Always show the navigation bar on the 'Get actions' screen
751     * so that the user can reduce the size of the page if they
752     * are interested in one or two items from the top. The only
753     * exception to this is when there are no results.
754     *
755     * @return bool
756     */
757    protected function isNavigationBarShown(): bool {
758        return $this->getNumRows() !== 0;
759    }
760}