Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
15.93% |
29 / 182 |
|
16.67% |
2 / 12 |
CRAP | |
0.00% |
0 / 1 |
CheckUserLogPager | |
15.93% |
29 / 182 |
|
16.67% |
2 / 12 |
942.63 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
30 | |||
generateTimestampLink | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
30 | |||
formatRow | |
0.00% |
0 / 75 |
|
0.00% |
0 / 1 |
72 | |||
getStartBody | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getEndBody | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getEmptyBody | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getQueryInfo | |
70.00% |
21 / 30 |
|
0.00% |
0 / 1 |
5.68 | |||
getIndexField | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
selectFields | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
preprocessResults | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
getPerformerSearchConds | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getQueryInfoForReasonSearch | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\CheckUser\CheckUser\Pagers; |
4 | |
5 | use IContextSource; |
6 | use MediaWiki\Cache\LinkBatchFactory; |
7 | use MediaWiki\CheckUser\Services\CheckUserLogService; |
8 | use MediaWiki\CommentFormatter\CommentFormatter; |
9 | use MediaWiki\CommentStore\CommentStore; |
10 | use MediaWiki\Html\Html; |
11 | use MediaWiki\Linker\Linker; |
12 | use MediaWiki\Pager\RangeChronologicalPager; |
13 | use MediaWiki\SpecialPage\SpecialPage; |
14 | use MediaWiki\User\ActorStore; |
15 | use MediaWiki\User\UserFactory; |
16 | use MediaWiki\User\UserIdentityValue; |
17 | use Wikimedia\IPUtils; |
18 | use Wikimedia\Rdbms\IResultWrapper; |
19 | |
20 | class CheckUserLogPager extends RangeChronologicalPager { |
21 | |
22 | /** @var array The options provided to the CheckUserLog form. May be empty. */ |
23 | private array $opts; |
24 | |
25 | private LinkBatchFactory $linkBatchFactory; |
26 | private CommentFormatter $commentFormatter; |
27 | private CheckUserLogService $checkUserLogService; |
28 | private CommentStore $commentStore; |
29 | private UserFactory $userFactory; |
30 | private ActorStore $actorStore; |
31 | |
32 | /** |
33 | * @param IContextSource $context |
34 | * @param array $opts A array of keys that can include 'target', 'initiator', 'start', 'end' |
35 | * 'year' and 'month'. Target should be a user, IP address or IP range. Initiator should be a user. |
36 | * Start and end should be timestamps. Year and month are converted to end but ignored if end is |
37 | * provided. |
38 | * @param LinkBatchFactory $linkBatchFactory |
39 | * @param CommentStore $commentStore |
40 | * @param CommentFormatter $commentFormatter |
41 | * @param CheckUserLogService $checkUserLogService |
42 | * @param UserFactory $userFactory |
43 | * @param ActorStore $actorStore |
44 | */ |
45 | public function __construct( |
46 | IContextSource $context, |
47 | array $opts, |
48 | LinkBatchFactory $linkBatchFactory, |
49 | CommentStore $commentStore, |
50 | CommentFormatter $commentFormatter, |
51 | CheckUserLogService $checkUserLogService, |
52 | UserFactory $userFactory, |
53 | ActorStore $actorStore |
54 | ) { |
55 | parent::__construct( $context ); |
56 | $this->linkBatchFactory = $linkBatchFactory; |
57 | $this->commentStore = $commentStore; |
58 | $this->commentFormatter = $commentFormatter; |
59 | $this->checkUserLogService = $checkUserLogService; |
60 | $this->userFactory = $userFactory; |
61 | $this->actorStore = $actorStore; |
62 | $this->opts = $opts; |
63 | |
64 | // Date filtering: use timestamp if available - From SpecialContributions.php |
65 | $startTimestamp = ''; |
66 | $endTimestamp = ''; |
67 | if ( isset( $opts['start'] ) && $opts['start'] ) { |
68 | $startTimestamp = $opts['start'] . ' 00:00:00'; |
69 | } |
70 | if ( isset( $opts['end'] ) && $opts['end'] ) { |
71 | $endTimestamp = $opts['end'] . ' 23:59:59'; |
72 | } |
73 | $this->getDateRangeCond( $startTimestamp, $endTimestamp ); |
74 | } |
75 | |
76 | /** |
77 | * If appropriate, generate a link that wraps around the provided date, time, or |
78 | * date and time. The date and time is escaped by this function. |
79 | * |
80 | * @param string $dateAndTime The string representation of the date, time or date and time. |
81 | * @param array|\stdClass $row The current row being formatted in formatRow(). |
82 | * @return string|null The date and time wrapped in a link if appropriate. |
83 | */ |
84 | protected function generateTimestampLink( string $dateAndTime, $row ) { |
85 | $highlight = $this->getRequest()->getVal( 'highlight' ); |
86 | // Add appropriate classes to the date and time. |
87 | $dateAndTimeClasses = []; |
88 | if ( |
89 | $highlight === strval( $row->cul_timestamp ) |
90 | ) { |
91 | $dateAndTimeClasses[] = 'mw-checkuser-log-highlight-entry'; |
92 | } |
93 | // If the CU log search has a specified target or initiator then |
94 | // provide a link to this log entry without the current filtering |
95 | // for these values. |
96 | if ( |
97 | $this->opts['target'] || |
98 | $this->opts['initiator'] |
99 | ) { |
100 | return $this->getLinkRenderer()->makeLink( |
101 | SpecialPage::getTitleFor( 'CheckUserLog' ), |
102 | $dateAndTime, |
103 | [ |
104 | 'class' => $dateAndTimeClasses, |
105 | ], |
106 | [ |
107 | // offset is used by IndexPager, it does not know this is a timestamp, |
108 | // so provide in database format to make it working as string there. |
109 | 'offset' => $this->getDatabase()->timestamp( |
110 | (int)wfTimestamp( TS_UNIX, $row->cul_timestamp ) + 3600 ), |
111 | 'highlight' => $row->cul_timestamp, |
112 | ] |
113 | ); |
114 | } elseif ( $dateAndTimeClasses ) { |
115 | return Html::element( |
116 | 'span', |
117 | [ 'class' => $dateAndTimeClasses ], |
118 | $dateAndTime |
119 | ); |
120 | } else { |
121 | return htmlspecialchars( $dateAndTime ); |
122 | } |
123 | } |
124 | |
125 | /** |
126 | * @inheritDoc |
127 | */ |
128 | public function formatRow( $row ) { |
129 | if ( $row->actor_user ) { |
130 | $performerHidden = $this->userFactory->newFromUserIdentity( |
131 | UserIdentityValue::newRegistered( $row->actor_user, $row->actor_name ) |
132 | )->isHidden(); |
133 | } else { |
134 | $performerHidden = $this->userFactory->newFromActorId( $row->actor_id )->isHidden(); |
135 | } |
136 | if ( $performerHidden && !$this->getAuthority()->isAllowed( 'hideuser' ) ) { |
137 | // Performer of the check is hidden and the logged in user does not have |
138 | // right to see hidden users. |
139 | $user = Html::element( |
140 | 'span', |
141 | [ 'class' => 'history-deleted' ], |
142 | $this->msg( 'rev-deleted-user' )->text() |
143 | ); |
144 | } else { |
145 | $user = Linker::userLink( $row->actor_user, $row->actor_name ); |
146 | if ( $performerHidden ) { |
147 | // Performer is hidden, but current user has rights to see it. |
148 | // Mark the username has hidden by wrapping it in a history-deleted span. |
149 | $user = Html::rawElement( |
150 | 'span', |
151 | [ 'class' => 'history-deleted' ], |
152 | $user |
153 | ); |
154 | } |
155 | $user .= $this->msg( 'word-separator' )->escaped() |
156 | . Html::rawElement( 'span', [ 'classes' => 'mw-usertoollinks' ], |
157 | $this->msg( 'parentheses' )->rawParams( $this->getLinkRenderer()->makeLink( |
158 | SpecialPage::getTitleFor( 'CheckUserLog' ), |
159 | $this->msg( 'checkuser-log-checks-by' )->text(), |
160 | [], |
161 | [ |
162 | 'cuInitiator' => $row->actor_name, |
163 | ] |
164 | ) )->escaped() |
165 | ); |
166 | } |
167 | |
168 | $targetHidden = $this->userFactory->newFromUserIdentity( |
169 | new UserIdentityValue( $row->cul_target_id, $row->cul_target_text ) |
170 | )->isHidden(); |
171 | if ( $targetHidden && !$this->getAuthority()->isAllowed( 'hideuser' ) ) { |
172 | // Target of the check is hidden and the logged in user does not have |
173 | // right to see hidden users. |
174 | $target = Html::element( |
175 | 'span', |
176 | [ 'class' => 'history-deleted' ], |
177 | $this->msg( 'rev-deleted-user' )->text() |
178 | ); |
179 | } else { |
180 | $target = Linker::userLink( $row->cul_target_id, $row->cul_target_text ); |
181 | if ( $targetHidden ) { |
182 | // Target is hidden, but current user has rights to see it. |
183 | // Mark the username has hidden by wrapping it in a history-deleted span. |
184 | $target = Html::rawElement( |
185 | 'span', |
186 | [ 'class' => 'history-deleted' ], |
187 | $target |
188 | ); |
189 | } |
190 | $target .= Linker::userToolLinks( $row->cul_target_id, trim( $row->cul_target_text ) ); |
191 | } |
192 | |
193 | $lang = $this->getLanguage(); |
194 | $contextUser = $this->getUser(); |
195 | // The following messages are generated here: |
196 | // * checkuser-log-entry-userips |
197 | // * checkuser-log-entry-ipactions |
198 | // * checkuser-log-entry-ipusers |
199 | // * checkuser-log-entry-ipactions-xff |
200 | // * checkuser-log-entry-ipusers-xff |
201 | // * checkuser-log-entry-useractions |
202 | // * checkuser-log-entry-investigate |
203 | $cul_type = [ |
204 | 'ipedits' => 'ipactions', |
205 | 'ipedits-xff' => 'ipactions-xff', |
206 | 'useredits' => 'useractions' |
207 | ][$row->cul_type] ?? $row->cul_type; |
208 | $rowContent = $this->msg( 'checkuser-log-entry-' . $cul_type ) |
209 | ->rawParams( |
210 | $user, |
211 | $target, |
212 | $this->generateTimestampLink( |
213 | $lang->userTimeAndDate( |
214 | wfTimestamp( TS_MW, $row->cul_timestamp ), $contextUser |
215 | ), |
216 | $row |
217 | ), |
218 | $this->generateTimestampLink( |
219 | $lang->userDate( wfTimestamp( TS_MW, $row->cul_timestamp ), $contextUser ), |
220 | $row |
221 | ), |
222 | $this->generateTimestampLink( |
223 | $lang->userTime( wfTimestamp( TS_MW, $row->cul_timestamp ), $contextUser ), |
224 | $row |
225 | ) |
226 | )->parse(); |
227 | $rowContent .= $this->commentFormatter->formatBlock( |
228 | $this->commentStore->getComment( 'cul_reason', $row )->text |
229 | ); |
230 | |
231 | $attribs = [ |
232 | 'data-mw-culogid' => $row->cul_id, |
233 | ]; |
234 | return Html::rawElement( 'li', $attribs, $rowContent ) . "\n"; |
235 | } |
236 | |
237 | /** |
238 | * @return string |
239 | */ |
240 | public function getStartBody() { |
241 | if ( $this->getNumRows() ) { |
242 | return '<ul>'; |
243 | } |
244 | |
245 | return ''; |
246 | } |
247 | |
248 | /** |
249 | * @return string |
250 | */ |
251 | public function getEndBody() { |
252 | if ( $this->getNumRows() ) { |
253 | return '</ul>'; |
254 | } |
255 | |
256 | return ''; |
257 | } |
258 | |
259 | /** |
260 | * @return string |
261 | */ |
262 | public function getEmptyBody() { |
263 | return '<p>' . $this->msg( 'checkuser-empty' )->escaped() . '</p>'; |
264 | } |
265 | |
266 | /** @inheritDoc */ |
267 | public function getQueryInfo() { |
268 | $queryInfo = [ |
269 | 'tables' => [ 'cu_log', 'cu_log_actor' => 'actor' ], |
270 | 'fields' => $this->selectFields(), |
271 | 'conds' => [], |
272 | 'join_conds' => [ 'cu_log_actor' => [ 'JOIN', [ 'actor_id = cul_actor' ] ] ], |
273 | 'options' => [], |
274 | ]; |
275 | |
276 | $reasonCommentQuery = $this->commentStore->getJoin( 'cul_reason' ); |
277 | $queryInfo['tables'] += $reasonCommentQuery['tables']; |
278 | $queryInfo['fields'] += $reasonCommentQuery['fields']; |
279 | $queryInfo['join_conds'] += $reasonCommentQuery['joins']; |
280 | |
281 | if ( $this->opts['target'] !== '' ) { |
282 | $queryInfo['conds'] = array_merge( |
283 | $queryInfo['conds'], |
284 | $this->checkUserLogService->getTargetSearchConds( $this->opts['target'] ) ?? [] |
285 | ); |
286 | if ( IPUtils::isIPAddress( $this->opts['target'] ) ) { |
287 | // Use the cul_target_hex index on the query if the target is an IP |
288 | // otherwise the query could take a long time (T342639) |
289 | $queryInfo['options']['USE INDEX'] = [ 'cu_log' => 'cul_target_hex' ]; |
290 | } |
291 | } |
292 | |
293 | if ( $this->opts['initiator'] !== '' ) { |
294 | $queryInfo['conds'] = array_merge( |
295 | $queryInfo['conds'], |
296 | $this->getPerformerSearchConds( $this->opts['initiator'] ) ?? [] |
297 | ); |
298 | } |
299 | |
300 | if ( $this->opts['reason'] !== '' ) { |
301 | $reasonSearchQuery = $this->getQueryInfoForReasonSearch( $this->opts['reason'] ); |
302 | $queryInfo['tables'] += $reasonSearchQuery['tables']; |
303 | $queryInfo['fields'] += $reasonSearchQuery['fields']; |
304 | $queryInfo['conds'] = array_merge( $queryInfo['conds'], $reasonSearchQuery['conds'] ); |
305 | $queryInfo['join_conds'] += $reasonSearchQuery['join_conds']; |
306 | } |
307 | |
308 | return $queryInfo; |
309 | } |
310 | |
311 | /** |
312 | * @inheritDoc |
313 | */ |
314 | public function getIndexField() { |
315 | return 'cul_timestamp'; |
316 | } |
317 | |
318 | /** |
319 | * Gets the fields for a select on the cu_log table. |
320 | * |
321 | * @return string[] |
322 | */ |
323 | public function selectFields(): array { |
324 | return [ |
325 | 'cul_id', 'cul_timestamp', 'cul_type', 'cul_target_id', |
326 | 'cul_target_text', 'actor_name', 'actor_user', 'actor_id' |
327 | ]; |
328 | } |
329 | |
330 | /** |
331 | * Do a batch query for links' existence and add it to LinkCache |
332 | * |
333 | * @param IResultWrapper $result |
334 | */ |
335 | protected function preprocessResults( $result ) { |
336 | if ( $this->getNumRows() === 0 ) { |
337 | return; |
338 | } |
339 | |
340 | $lb = $this->linkBatchFactory->newLinkBatch(); |
341 | $lb->setCaller( __METHOD__ ); |
342 | foreach ( $result as $row ) { |
343 | // Performer |
344 | $lb->add( NS_USER, $row->actor_name ); |
345 | |
346 | if ( $row->cul_type == 'userips' || $row->cul_type == 'useredits' ) { |
347 | $lb->add( NS_USER, $row->cul_target_text ); |
348 | $lb->add( NS_USER_TALK, $row->cul_target_text ); |
349 | } |
350 | } |
351 | $lb->execute(); |
352 | $result->seek( 0 ); |
353 | } |
354 | |
355 | /** |
356 | * Get DB search conditions for the initiator |
357 | * |
358 | * @param string $initiator the username of the initiator. |
359 | * @return array|null array if valid target, null if invalid |
360 | */ |
361 | private function getPerformerSearchConds( string $initiator ): ?array { |
362 | $initiatorId = $this->actorStore->findActorIdByName( $initiator, $this->mDb ) ?? false; |
363 | if ( $initiatorId !== false ) { |
364 | return [ 'cul_actor' => $initiatorId ]; |
365 | } |
366 | return null; |
367 | } |
368 | |
369 | /** |
370 | * Get the query info for a reason search |
371 | * |
372 | * @param string $reason The reason to search for |
373 | * @return string[][] With three keys to arrays for tables, fields and joins. |
374 | */ |
375 | public function getQueryInfoForReasonSearch( string $reason ): array { |
376 | $queryInfo = [ 'tables' => [], 'fields' => [], 'join_conds' => [] ]; |
377 | $plaintextReason = $this->checkUserLogService->getPlaintextReason( $reason ); |
378 | |
379 | if ( $plaintextReason == '' ) { |
380 | return $queryInfo; |
381 | } |
382 | |
383 | $plaintextReasonCommentQuery = $this->commentStore->getJoin( 'cul_reason_plaintext' ); |
384 | $queryInfo['tables'] += $plaintextReasonCommentQuery['tables']; |
385 | $queryInfo['fields'] += $plaintextReasonCommentQuery['fields']; |
386 | $queryInfo['join_conds'] += $plaintextReasonCommentQuery['joins']; |
387 | |
388 | $queryInfo['conds'] = [ 'comment_cul_reason_plaintext.comment_text' => $plaintextReason ]; |
389 | |
390 | return $queryInfo; |
391 | } |
392 | } |