Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
97.83% |
180 / 184 |
|
76.92% |
10 / 13 |
CRAP | |
0.00% |
0 / 1 |
CheckUserGetIPsPager | |
97.83% |
180 / 184 |
|
76.92% |
10 / 13 |
51 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
formatRow | |
100.00% |
35 / 35 |
|
100.00% |
1 / 1 |
6 | |||
getIPBlockInfo | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
4.05 | |||
getCountForIPActions | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
7 | |||
getCountForIPActionsPerTable | |
100.00% |
31 / 31 |
|
100.00% |
1 / 1 |
7 | |||
groupResultsByIndexField | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
14 | |||
getQueryInfo | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
5 | |||
getQueryInfoForCuChanges | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
2 | |||
getQueryInfoForCuLogEvent | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
1 | |||
getQueryInfoForCuPrivateEvent | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
1 | |||
getIndexField | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getStartBody | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
isNavigationBarShown | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\CheckUser\CheckUser\Pagers; |
4 | |
5 | use ExtensionRegistry; |
6 | use IContextSource; |
7 | use LogicException; |
8 | use MediaWiki\Block\DatabaseBlock; |
9 | use MediaWiki\CheckUser\CheckUser\SpecialCheckUser; |
10 | use MediaWiki\CheckUser\Services\CheckUserLogService; |
11 | use MediaWiki\CheckUser\Services\CheckUserLookupUtils; |
12 | use MediaWiki\CheckUser\Services\TokenQueryManager; |
13 | use MediaWiki\Extension\TorBlock\TorExitNodes; |
14 | use MediaWiki\Html\FormOptions; |
15 | use MediaWiki\Html\Html; |
16 | use MediaWiki\Linker\LinkRenderer; |
17 | use MediaWiki\SpecialPage\SpecialPage; |
18 | use MediaWiki\SpecialPage\SpecialPageFactory; |
19 | use MediaWiki\User\CentralId\CentralIdLookup; |
20 | use MediaWiki\User\UserFactory; |
21 | use MediaWiki\User\UserGroupManager; |
22 | use MediaWiki\User\UserIdentity; |
23 | use MediaWiki\User\UserIdentityLookup; |
24 | use Wikimedia\IPUtils; |
25 | use Wikimedia\Rdbms\IConnectionProvider; |
26 | |
27 | class CheckUserGetIPsPager extends AbstractCheckUserPager { |
28 | |
29 | /** |
30 | * @param FormOptions $opts |
31 | * @param UserIdentity $target |
32 | * @param string $logType |
33 | * @param TokenQueryManager $tokenQueryManager |
34 | * @param UserGroupManager $userGroupManager |
35 | * @param CentralIdLookup $centralIdLookup |
36 | * @param IConnectionProvider $dbProvider |
37 | * @param SpecialPageFactory $specialPageFactory |
38 | * @param UserIdentityLookup $userIdentityLookup |
39 | * @param CheckUserLogService $checkUserLogService |
40 | * @param UserFactory $userFactory |
41 | * @param CheckUserLookupUtils $checkUserLookupUtils |
42 | * @param IContextSource|null $context |
43 | * @param LinkRenderer|null $linkRenderer |
44 | * @param ?int $limit |
45 | */ |
46 | public function __construct( |
47 | FormOptions $opts, |
48 | UserIdentity $target, |
49 | string $logType, |
50 | TokenQueryManager $tokenQueryManager, |
51 | UserGroupManager $userGroupManager, |
52 | CentralIdLookup $centralIdLookup, |
53 | IConnectionProvider $dbProvider, |
54 | SpecialPageFactory $specialPageFactory, |
55 | UserIdentityLookup $userIdentityLookup, |
56 | CheckUserLogService $checkUserLogService, |
57 | UserFactory $userFactory, |
58 | CheckUserLookupUtils $checkUserLookupUtils, |
59 | IContextSource $context = null, |
60 | LinkRenderer $linkRenderer = null, |
61 | ?int $limit = null |
62 | ) { |
63 | parent::__construct( $opts, $target, $logType, $tokenQueryManager, $userGroupManager, $centralIdLookup, |
64 | $dbProvider, $specialPageFactory, $userIdentityLookup, $checkUserLogService, $userFactory, |
65 | $checkUserLookupUtils, $context, $linkRenderer, $limit ); |
66 | $this->checkType = SpecialCheckUser::SUBTYPE_GET_IPS; |
67 | } |
68 | |
69 | /** @inheritDoc */ |
70 | public function formatRow( $row ): string { |
71 | $lang = $this->getLanguage(); |
72 | $ip = IPUtils::prettifyIP( $row->ip ); |
73 | $templateParams = []; |
74 | $templateParams['ipLink'] = $this->getSelfLink( $ip, |
75 | [ |
76 | 'user' => $ip, |
77 | 'reason' => $this->opts->getValue( 'reason' ), |
78 | ] |
79 | ); |
80 | |
81 | // If we get some results, it helps to know if the IP in general |
82 | // has a lot more edits, e.g. "tip of the iceberg"... |
83 | $ipEdits = $this->getCountForIPActions( $ip ); |
84 | if ( $ipEdits ) { |
85 | $templateParams['ipEditCount'] = |
86 | $this->msg( 'checkuser-ipeditcount' )->numParams( $ipEdits )->escaped(); |
87 | $templateParams['showIpCounts'] = true; |
88 | } |
89 | |
90 | if ( IPUtils::isValidIPv6( $ip ) ) { |
91 | $templateParams['ip64Link'] = $this->getSelfLink( '/64', |
92 | [ |
93 | 'user' => $ip . '/64', |
94 | 'reason' => $this->opts->getValue( 'reason' ), |
95 | ] |
96 | ); |
97 | $ipEdits64 = $this->getCountForIPActions( $ip . '/64' ); |
98 | if ( $ipEdits64 && ( !$ipEdits || $ipEdits64 > $ipEdits ) ) { |
99 | $templateParams['ip64EditCount'] = |
100 | $this->msg( 'checkuser-ipeditcount-64' )->numParams( $ipEdits64 )->escaped(); |
101 | $templateParams['showIpCounts'] = true; |
102 | } |
103 | } |
104 | $templateParams['blockLink'] = $this->getLinkRenderer()->makeKnownLink( |
105 | SpecialPage::getTitleFor( 'Block', $ip ), |
106 | $this->msg( 'blocklink' )->text() |
107 | ); |
108 | $templateParams['timeRange'] = $this->getTimeRangeString( $row->first, $row->last ); |
109 | $templateParams['editCount'] = $lang->formatNum( $row->count ); |
110 | |
111 | // If this IP is blocked, give a link to the block log |
112 | $templateParams['blockInfo'] = $this->getIPBlockInfo( $ip ); |
113 | $templateParams['toolLinks'] = $this->msg( 'checkuser-toollinks', urlencode( $ip ) )->parse(); |
114 | return $this->templateParser->processTemplate( 'GetIPsLine', $templateParams ); |
115 | } |
116 | |
117 | /** |
118 | * Get information about any active blocks on a IP. |
119 | * |
120 | * @param string $ip the IP to get block info on. |
121 | * @return string |
122 | */ |
123 | protected function getIPBlockInfo( string $ip ): string { |
124 | $block = DatabaseBlock::newFromTarget( null, $ip ); |
125 | if ( $block instanceof DatabaseBlock ) { |
126 | return $this->getBlockFlag( $block ); |
127 | } elseif ( |
128 | ExtensionRegistry::getInstance()->isLoaded( 'TorBlock' ) && |
129 | TorExitNodes::isExitNode( $ip ) |
130 | ) { |
131 | return Html::rawElement( 'strong', [], '(' . $this->msg( 'checkuser-torexitnode' )->escaped() . ')' ); |
132 | } |
133 | return ''; |
134 | } |
135 | |
136 | /** |
137 | * Return "checkuser-ipeditcount" number or false |
138 | * if the number is the same as the number of edits |
139 | * made by the user on the IP. |
140 | * |
141 | * @param string $ip_or_range |
142 | * @return int|false |
143 | */ |
144 | protected function getCountForIPActions( string $ip_or_range ) { |
145 | $count = false; |
146 | $tables = self::RESULT_TABLES; |
147 | if ( !$this->eventTableReadNew ) { |
148 | $tables = [ self::CHANGES_TABLE ]; |
149 | } |
150 | $countsPerTable = []; |
151 | // Get the total count and counts by this user. |
152 | foreach ( $tables as $table ) { |
153 | $countsPerTable[$table] = $this->getCountForIPActionsPerTable( $ip_or_range, $table ); |
154 | } |
155 | // Display the count if at least one of the counts for a table has more actions |
156 | // performed by all users than the current target user. |
157 | $shouldDisplayCount = count( array_filter( $countsPerTable, static function ( $countsForTable ) { |
158 | return $countsForTable !== null && $countsForTable['total'] > $countsForTable['by_this_target']; |
159 | } ) ); |
160 | if ( $shouldDisplayCount ) { |
161 | // If displaying the count, then sum the |
162 | // 'total' count for all three tables. |
163 | foreach ( $countsPerTable as $countsForTable ) { |
164 | if ( $countsForTable !== null ) { |
165 | $count += $countsForTable['total']; |
166 | } |
167 | } |
168 | } |
169 | return $count; |
170 | } |
171 | |
172 | /** |
173 | * Return the number of actions performed by all users |
174 | * and the current target on a given IP or IP range. |
175 | * |
176 | * @param string $ipOrRange The IP or IP range to get the counts from. |
177 | * @param string $table The table to get these results from (valid tables in self::RESULT_TABLES). |
178 | * @return array<string, integer>|null |
179 | */ |
180 | protected function getCountForIPActionsPerTable( string $ipOrRange, string $table ): ?array { |
181 | // Get the IExpression which allows selecting results for the IP or IP range. |
182 | $expr = $this->checkUserLookupUtils->getIPTargetExpr( $ipOrRange, false, $table ); |
183 | if ( $expr === null ) { |
184 | // Return null if no target conditions could be generated. |
185 | return null; |
186 | } |
187 | // We are only using startOffset for the period feature. |
188 | if ( $this->startOffset ) { |
189 | $expr = $this->mDb->expr( $this->getTimestampField( $table ), '>=', $this->startOffset ) |
190 | ->andExpr( $expr ); |
191 | } |
192 | |
193 | // If the $table is cu_changes and event table migration |
194 | // is set to read new, then only include rows that have |
195 | // cuc_only_for_read_old equal to 0 to prevent duplicate |
196 | // rows appearing. |
197 | if ( $this->eventTableReadNew && $table === self::CHANGES_TABLE ) { |
198 | $expr = $this->mDb->expr( 'cuc_only_for_read_old', '=', 0 ) |
199 | ->andExpr( $expr ); |
200 | } |
201 | |
202 | // Get counts for this IP / IP range |
203 | $query = $this->mDb->newSelectQueryBuilder() |
204 | ->table( $table ) |
205 | ->conds( $expr ) |
206 | ->caller( __METHOD__ ); |
207 | $ipEdits = $query->estimateRowCount(); |
208 | // If small enough, get a more accurate count |
209 | if ( $ipEdits <= 1000 ) { |
210 | $ipEdits = $query->fetchRowCount(); |
211 | } |
212 | |
213 | // Get counts for the target on this IP / IP range |
214 | $expr = $this->mDb->expr( 'actor_user', '=', $this->target->getId() ) |
215 | ->andExpr( $expr ); |
216 | $query = $this->mDb->newSelectQueryBuilder() |
217 | ->table( $table ) |
218 | ->join( |
219 | 'actor', |
220 | "{$table}_actor", |
221 | "{$table}_actor.actor_id = {$this::RESULT_TABLE_TO_PREFIX[$table]}actor" |
222 | ) |
223 | ->conds( $expr ) |
224 | ->caller( __METHOD__ ); |
225 | $userOnIpEdits = $query->estimateRowCount(); |
226 | // If small enough, get a more accurate count |
227 | if ( $userOnIpEdits <= 1000 ) { |
228 | $userOnIpEdits = $query->fetchRowCount(); |
229 | } |
230 | |
231 | return [ 'total' => $ipEdits, 'by_this_target' => $userOnIpEdits ]; |
232 | } |
233 | |
234 | /** @inheritDoc */ |
235 | protected function groupResultsByIndexField( array $results ): array { |
236 | // Group rows that have the same 'ip' and 'ip_hex' value. |
237 | $resultsGroupedByIPAndIPHex = []; |
238 | foreach ( $results as $row ) { |
239 | if ( !array_key_exists( $row->ip, $resultsGroupedByIPAndIPHex ) ) { |
240 | $resultsGroupedByIPAndIPHex[$row->ip] = []; |
241 | } |
242 | if ( !array_key_exists( $row->ip_hex, $resultsGroupedByIPAndIPHex[$row->ip] ) ) { |
243 | $resultsGroupedByIPAndIPHex[$row->ip][$row->ip_hex] = []; |
244 | } |
245 | $resultsGroupedByIPAndIPHex[$row->ip][$row->ip_hex][] = $row; |
246 | } |
247 | // Combine the rows that have the same 'ip' and 'ip_hex' value. |
248 | $groupedResults = []; |
249 | $indexField = $this->getIndexField(); |
250 | foreach ( $resultsGroupedByIPAndIPHex as $ip => $ipHexArray ) { |
251 | foreach ( $ipHexArray as $ipHex => $rows ) { |
252 | $combinedRow = [ |
253 | 'ip' => $ip, |
254 | 'ip_hex' => $ipHex, |
255 | 'count' => 0, |
256 | 'first' => '', |
257 | 'last' => '', |
258 | ]; |
259 | foreach ( $rows as $row ) { |
260 | $combinedRow['count'] += $row->count; |
261 | if ( $row->first && ( $combinedRow['first'] > $row->first || !$combinedRow['first'] ) ) { |
262 | $combinedRow['first'] = $row->first; |
263 | } |
264 | if ( $row->last && ( $combinedRow['last'] < $row->last || !$combinedRow['last'] ) ) { |
265 | $combinedRow['last'] = $row->last; |
266 | } |
267 | } |
268 | $combinedRow = (object)$combinedRow; |
269 | if ( array_key_exists( $combinedRow->$indexField, $groupedResults ) ) { |
270 | $groupedResults[$combinedRow->$indexField][] = $combinedRow; |
271 | } else { |
272 | $groupedResults[$combinedRow->$indexField] = [ $combinedRow ]; |
273 | } |
274 | } |
275 | } |
276 | return $groupedResults; |
277 | } |
278 | |
279 | /** @inheritDoc */ |
280 | public function getQueryInfo( ?string $table = null ): array { |
281 | if ( $table === null ) { |
282 | throw new LogicException( |
283 | "This ::getQueryInfo method must be provided with the table to generate " . |
284 | "the correct query info" |
285 | ); |
286 | } |
287 | |
288 | if ( $table === self::CHANGES_TABLE ) { |
289 | $queryInfo = $this->getQueryInfoForCuChanges(); |
290 | } elseif ( $table === self::LOG_EVENT_TABLE ) { |
291 | $queryInfo = $this->getQueryInfoForCuLogEvent(); |
292 | } elseif ( $table === self::PRIVATE_LOG_EVENT_TABLE ) { |
293 | $queryInfo = $this->getQueryInfoForCuPrivateEvent(); |
294 | } |
295 | |
296 | // Apply index, group by IP / IP hex, and filter results to just the target user. |
297 | $queryInfo['options']['USE INDEX'] = [ |
298 | $table => $this->checkUserLookupUtils->getIndexName( $this->xfor, $table ) |
299 | ]; |
300 | $queryInfo['options']['GROUP BY'] = [ 'ip', 'ip_hex' ]; |
301 | $queryInfo['conds']['actor_user'] = $this->target->getId(); |
302 | |
303 | return $queryInfo; |
304 | } |
305 | |
306 | /** @inheritDoc */ |
307 | protected function getQueryInfoForCuChanges(): array { |
308 | $queryInfo = [ |
309 | 'fields' => [ |
310 | 'ip' => 'cuc_ip', |
311 | 'ip_hex' => 'cuc_ip_hex', |
312 | 'count' => 'COUNT(*)', |
313 | 'first' => 'MIN(cuc_timestamp)', |
314 | 'last' => 'MAX(cuc_timestamp)', |
315 | ], |
316 | 'tables' => [ 'cu_changes', 'actor_cuc_actor' => 'actor' ], |
317 | 'conds' => [], |
318 | 'join_conds' => [ 'actor_cuc_actor' => [ 'JOIN', 'actor_cuc_actor.actor_id=cuc_actor' ] ], |
319 | 'options' => [], |
320 | ]; |
321 | // When reading new, only select results from cu_changes that are |
322 | // for read new (defined as those with cuc_only_for_read_old set to 0). |
323 | if ( $this->eventTableReadNew ) { |
324 | $queryInfo['conds']['cuc_only_for_read_old'] = 0; |
325 | } |
326 | return $queryInfo; |
327 | } |
328 | |
329 | /** @inheritDoc */ |
330 | protected function getQueryInfoForCuLogEvent(): array { |
331 | return [ |
332 | 'fields' => [ |
333 | 'ip' => 'cule_ip', |
334 | 'ip_hex' => 'cule_ip_hex', |
335 | 'count' => 'COUNT(*)', |
336 | 'first' => 'MIN(cule_timestamp)', |
337 | 'last' => 'MAX(cule_timestamp)', |
338 | ], |
339 | 'tables' => [ 'cu_log_event', 'actor_cule_actor' => 'actor' ], |
340 | 'conds' => [], |
341 | 'join_conds' => [ 'actor_cule_actor' => [ 'JOIN', 'actor_cule_actor.actor_id=cule_actor' ] ], |
342 | 'options' => [], |
343 | ]; |
344 | } |
345 | |
346 | /** @inheritDoc */ |
347 | protected function getQueryInfoForCuPrivateEvent(): array { |
348 | return [ |
349 | 'fields' => [ |
350 | 'ip' => 'cupe_ip', |
351 | 'ip_hex' => 'cupe_ip_hex', |
352 | 'count' => 'COUNT(*)', |
353 | 'first' => 'MIN(cupe_timestamp)', |
354 | 'last' => 'MAX(cupe_timestamp)', |
355 | ], |
356 | 'tables' => [ 'cu_private_event', 'actor_cupe_actor' => 'actor' ], |
357 | 'conds' => [], |
358 | 'join_conds' => [ 'actor_cupe_actor' => [ 'JOIN', 'actor_cupe_actor.actor_id=cupe_actor' ] ], |
359 | 'options' => [], |
360 | ]; |
361 | } |
362 | |
363 | /** @inheritDoc */ |
364 | public function getIndexField(): string { |
365 | return 'last'; |
366 | } |
367 | |
368 | /** @inheritDoc */ |
369 | protected function getStartBody(): string { |
370 | return $this->getNavigationBar() |
371 | . '<div id="checkuserresults" class="mw-checkuser-get-ips-results"><ul>'; |
372 | } |
373 | |
374 | /** |
375 | * Temporary measure until Get IPs query is fixed for pagination (T315612). |
376 | * |
377 | * @return bool |
378 | */ |
379 | protected function isNavigationBarShown() { |
380 | return false; |
381 | } |
382 | } |