Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
89.66% |
364 / 406 |
|
69.23% |
9 / 13 |
CRAP | |
0.00% |
0 / 1 |
CheckUserGetActionsPager | |
89.66% |
364 / 406 |
|
69.23% |
9 / 13 |
87.09 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
1 | |||
formatRow | |
88.00% |
66 / 75 |
|
0.00% |
0 / 1 |
21.76 | |||
getActionText | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getLinksFromRow | |
74.74% |
71 / 95 |
|
0.00% |
0 / 1 |
23.22 | |||
preCacheMessages | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
buildUserLinks | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 | |||
getQueryInfo | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
7 | |||
getQueryInfoForCuChanges | |
100.00% |
37 / 37 |
|
100.00% |
1 / 1 |
3 | |||
getQueryInfoForCuLogEvent | |
100.00% |
47 / 47 |
|
100.00% |
1 / 1 |
3 | |||
getQueryInfoForCuPrivateEvent | |
100.00% |
45 / 45 |
|
100.00% |
1 / 1 |
4 | |||
getStartBody | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
preprocessResults | |
83.72% |
36 / 43 |
|
0.00% |
0 / 1 |
14.85 | |||
isNavigationBarShown | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\CheckUser\CheckUser\Pagers; |
4 | |
5 | use HtmlArmor; |
6 | use IContextSource; |
7 | use LogEventsList; |
8 | use LogFormatter; |
9 | use LogicException; |
10 | use LogPage; |
11 | use ManualLogEntry; |
12 | use MediaWiki\Cache\LinkBatchFactory; |
13 | use MediaWiki\CheckUser\CheckUser\SpecialCheckUser; |
14 | use MediaWiki\CheckUser\ClientHints\ClientHintsBatchFormatterResults; |
15 | use MediaWiki\CheckUser\ClientHints\ClientHintsReferenceIds; |
16 | use MediaWiki\CheckUser\Hook\HookRunner; |
17 | use MediaWiki\CheckUser\Services\CheckUserLogService; |
18 | use MediaWiki\CheckUser\Services\CheckUserLookupUtils; |
19 | use MediaWiki\CheckUser\Services\CheckUserUtilityService; |
20 | use MediaWiki\CheckUser\Services\TokenQueryManager; |
21 | use MediaWiki\CheckUser\Services\UserAgentClientHintsFormatter; |
22 | use MediaWiki\CheckUser\Services\UserAgentClientHintsLookup; |
23 | use MediaWiki\CheckUser\Services\UserAgentClientHintsManager; |
24 | use MediaWiki\CommentFormatter\CommentFormatter; |
25 | use MediaWiki\CommentStore\CommentStore; |
26 | use MediaWiki\Html\FormOptions; |
27 | use MediaWiki\Html\Html; |
28 | use MediaWiki\Linker\Linker; |
29 | use MediaWiki\Linker\LinkRenderer; |
30 | use MediaWiki\Logger\LoggerFactory; |
31 | use MediaWiki\Revision\RevisionRecord; |
32 | use MediaWiki\SpecialPage\SpecialPage; |
33 | use MediaWiki\SpecialPage\SpecialPageFactory; |
34 | use MediaWiki\Title\Title; |
35 | use MediaWiki\User\CentralId\CentralIdLookup; |
36 | use MediaWiki\User\UserEditTracker; |
37 | use MediaWiki\User\UserFactory; |
38 | use MediaWiki\User\UserGroupManager; |
39 | use MediaWiki\User\UserIdentity; |
40 | use MediaWiki\User\UserIdentityLookup; |
41 | use MediaWiki\User\UserIdentityValue; |
42 | use Psr\Log\LoggerInterface; |
43 | use stdClass; |
44 | use Wikimedia\IPUtils; |
45 | use Wikimedia\Rdbms\IConnectionProvider; |
46 | |
47 | class 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 | } |