Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
34.07% |
154 / 452 |
|
38.46% |
5 / 13 |
CRAP | |
0.00% |
0 / 1 |
SpecialCheckUser | |
34.07% |
154 / 452 |
|
38.46% |
5 / 13 |
2793.35 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
1 | |||
doesWrites | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
0.00% |
0 / 161 |
|
0.00% |
0 / 1 |
1560 | |||
showIntroductoryText | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
showForm | |
0.00% |
0 / 70 |
|
0.00% |
0 / 1 |
56 | |||
addJsCIDRForm | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
checkReason | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
doMassUserBlock | |
0.00% |
0 / 50 |
|
0.00% |
0 / 1 |
210 | |||
doMassUserBlockInternal | |
96.49% |
55 / 57 |
|
0.00% |
0 / 1 |
20 | |||
tagPage | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
3 | |||
getPager | |
100.00% |
62 / 62 |
|
100.00% |
1 / 1 |
5 | |||
prefixSearchSubpages | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\CheckUser\CheckUser; |
4 | |
5 | use HTMLForm; |
6 | use MediaWiki\Block\BlockPermissionCheckerFactory; |
7 | use MediaWiki\Block\BlockUserFactory; |
8 | use MediaWiki\Cache\LinkBatchFactory; |
9 | use MediaWiki\CheckUser\CheckUser\Pagers\AbstractCheckUserPager; |
10 | use MediaWiki\CheckUser\CheckUser\Pagers\CheckUserGetActionsPager; |
11 | use MediaWiki\CheckUser\CheckUser\Pagers\CheckUserGetIPsPager; |
12 | use MediaWiki\CheckUser\CheckUser\Pagers\CheckUserGetUsersPager; |
13 | use MediaWiki\CheckUser\CheckUser\Widgets\CIDRCalculator; |
14 | use MediaWiki\CheckUser\Hook\HookRunner; |
15 | use MediaWiki\CheckUser\Services\CheckUserLogService; |
16 | use MediaWiki\CheckUser\Services\CheckUserLookupUtils; |
17 | use MediaWiki\CheckUser\Services\CheckUserUtilityService; |
18 | use MediaWiki\CheckUser\Services\TokenQueryManager; |
19 | use MediaWiki\CheckUser\Services\UserAgentClientHintsFormatter; |
20 | use MediaWiki\CheckUser\Services\UserAgentClientHintsLookup; |
21 | use MediaWiki\CommentFormatter\CommentFormatter; |
22 | use MediaWiki\CommentStore\CommentStore; |
23 | use MediaWiki\Html\FormOptions; |
24 | use MediaWiki\Html\Html; |
25 | use MediaWiki\Page\WikiPageFactory; |
26 | use MediaWiki\Permissions\PermissionManager; |
27 | use MediaWiki\SpecialPage\SpecialPage; |
28 | use MediaWiki\Status\Status; |
29 | use MediaWiki\Title\Title; |
30 | use MediaWiki\User\CentralId\CentralIdLookup; |
31 | use MediaWiki\User\CentralId\CentralIdLookupFactory; |
32 | use MediaWiki\User\UserEditTracker; |
33 | use MediaWiki\User\UserFactory; |
34 | use MediaWiki\User\UserGroupManager; |
35 | use MediaWiki\User\UserIdentity; |
36 | use MediaWiki\User\UserIdentityLookup; |
37 | use MediaWiki\User\UserIdentityValue; |
38 | use MediaWiki\User\UserNamePrefixSearch; |
39 | use MediaWiki\User\UserNameUtils; |
40 | use MediaWiki\User\UserRigorOptions; |
41 | use Message; |
42 | use OOUI\IconWidget; |
43 | use UserBlockedError; |
44 | use Wikimedia\IPUtils; |
45 | use Wikimedia\Rdbms\IConnectionProvider; |
46 | use WikitextContent; |
47 | |
48 | class SpecialCheckUser extends SpecialPage { |
49 | /** |
50 | * The possible subtypes represented as constants. |
51 | * The constants represent the old string values |
52 | * for backwards compatibility. |
53 | */ |
54 | public const SUBTYPE_GET_IPS = 'subuserips'; |
55 | |
56 | public const SUBTYPE_GET_ACTIONS = 'subactions'; |
57 | |
58 | public const SUBTYPE_GET_USERS = 'subipusers'; |
59 | |
60 | /** |
61 | * @var FormOptions the form parameters. |
62 | */ |
63 | protected $opts; |
64 | |
65 | private LinkBatchFactory $linkBatchFactory; |
66 | private BlockPermissionCheckerFactory $blockPermissionCheckerFactory; |
67 | private BlockUserFactory $blockUserFactory; |
68 | private UserGroupManager $userGroupManager; |
69 | private CentralIdLookup $centralIdLookup; |
70 | private WikiPageFactory $wikiPageFactory; |
71 | private PermissionManager $permissionManager; |
72 | private UserIdentityLookup $userIdentityLookup; |
73 | private TokenQueryManager $tokenQueryManager; |
74 | private IConnectionProvider $dbProvider; |
75 | private UserFactory $userFactory; |
76 | private CheckUserLogService $checkUserLogService; |
77 | private CommentFormatter $commentFormatter; |
78 | private UserEditTracker $userEditTracker; |
79 | private UserNamePrefixSearch $userNamePrefixSearch; |
80 | private UserNameUtils $userNameUtils; |
81 | private HookRunner $hookRunner; |
82 | private CheckUserUtilityService $checkUserUtilityService; |
83 | private CommentStore $commentStore; |
84 | private UserAgentClientHintsLookup $clientHintsLookup; |
85 | private UserAgentClientHintsFormatter $clientHintsFormatter; |
86 | private CheckUserLookupUtils $checkUserLookupUtils; |
87 | |
88 | /** |
89 | * @param LinkBatchFactory $linkBatchFactory |
90 | * @param BlockPermissionCheckerFactory $blockPermissionCheckerFactory |
91 | * @param BlockUserFactory $blockUserFactory |
92 | * @param UserGroupManager $userGroupManager |
93 | * @param CentralIdLookupFactory $centralIdLookupFactory |
94 | * @param WikiPageFactory $wikiPageFactory |
95 | * @param PermissionManager $permissionManager |
96 | * @param UserIdentityLookup $userIdentityLookup |
97 | * @param TokenQueryManager $tokenQueryManager |
98 | * @param IConnectionProvider $dbProvider |
99 | * @param UserFactory $userFactory |
100 | * @param CheckUserLogService $checkUserLogService |
101 | * @param CommentFormatter $commentFormatter |
102 | * @param UserEditTracker $userEditTracker |
103 | * @param UserNamePrefixSearch $userNamePrefixSearch |
104 | * @param UserNameUtils $userNameUtils |
105 | * @param HookRunner $hookRunner |
106 | * @param CheckUserUtilityService $checkUserUtilityService |
107 | * @param CommentStore $commentStore |
108 | * @param UserAgentClientHintsLookup $clientHintsLookup |
109 | * @param UserAgentClientHintsFormatter $clientHintsFormatter |
110 | * @param CheckUserLookupUtils $checkUserLookupUtils |
111 | */ |
112 | public function __construct( |
113 | LinkBatchFactory $linkBatchFactory, |
114 | BlockPermissionCheckerFactory $blockPermissionCheckerFactory, |
115 | BlockUserFactory $blockUserFactory, |
116 | UserGroupManager $userGroupManager, |
117 | CentralIdLookupFactory $centralIdLookupFactory, |
118 | WikiPageFactory $wikiPageFactory, |
119 | PermissionManager $permissionManager, |
120 | UserIdentityLookup $userIdentityLookup, |
121 | TokenQueryManager $tokenQueryManager, |
122 | IConnectionProvider $dbProvider, |
123 | UserFactory $userFactory, |
124 | CheckUserLogService $checkUserLogService, |
125 | CommentFormatter $commentFormatter, |
126 | UserEditTracker $userEditTracker, |
127 | UserNamePrefixSearch $userNamePrefixSearch, |
128 | UserNameUtils $userNameUtils, |
129 | HookRunner $hookRunner, |
130 | CheckUserUtilityService $checkUserUtilityService, |
131 | CommentStore $commentStore, |
132 | UserAgentClientHintsLookup $clientHintsLookup, |
133 | UserAgentClientHintsFormatter $clientHintsFormatter, |
134 | CheckUserLookupUtils $checkUserLookupUtils |
135 | ) { |
136 | parent::__construct( 'CheckUser', 'checkuser' ); |
137 | |
138 | $this->linkBatchFactory = $linkBatchFactory; |
139 | $this->blockPermissionCheckerFactory = $blockPermissionCheckerFactory; |
140 | $this->blockUserFactory = $blockUserFactory; |
141 | $this->userGroupManager = $userGroupManager; |
142 | $this->centralIdLookup = $centralIdLookupFactory->getLookup(); |
143 | $this->wikiPageFactory = $wikiPageFactory; |
144 | $this->permissionManager = $permissionManager; |
145 | $this->userIdentityLookup = $userIdentityLookup; |
146 | $this->tokenQueryManager = $tokenQueryManager; |
147 | $this->dbProvider = $dbProvider; |
148 | $this->userFactory = $userFactory; |
149 | $this->checkUserLogService = $checkUserLogService; |
150 | $this->commentFormatter = $commentFormatter; |
151 | $this->userEditTracker = $userEditTracker; |
152 | $this->userNamePrefixSearch = $userNamePrefixSearch; |
153 | $this->userNameUtils = $userNameUtils; |
154 | $this->hookRunner = $hookRunner; |
155 | $this->checkUserUtilityService = $checkUserUtilityService; |
156 | $this->commentStore = $commentStore; |
157 | $this->clientHintsLookup = $clientHintsLookup; |
158 | $this->clientHintsFormatter = $clientHintsFormatter; |
159 | $this->checkUserLookupUtils = $checkUserLookupUtils; |
160 | } |
161 | |
162 | public function doesWrites() { |
163 | // logging |
164 | return true; |
165 | } |
166 | |
167 | /** @inheritDoc */ |
168 | public function execute( $subPage ) { |
169 | $this->setHeaders(); |
170 | $this->addHelpLink( 'Extension:CheckUser' ); |
171 | $this->checkPermissions(); |
172 | // Logging and blocking requires writing so stop from here if read-only mode |
173 | $this->checkReadOnly(); |
174 | |
175 | // Blocked users are not allowed to run checkuser queries (bug T157883) |
176 | $block = $this->getUser()->getBlock(); |
177 | if ( $block && $block->isSitewide() ) { |
178 | throw new UserBlockedError( $block ); |
179 | } |
180 | |
181 | $request = $this->getRequest(); |
182 | |
183 | $opts = new FormOptions(); |
184 | $opts->add( 'reason', '' ); |
185 | $opts->add( 'checktype', '' ); |
186 | $opts->add( 'period', 0 ); |
187 | $opts->add( 'offset', '' ); |
188 | $opts->add( 'limit', 0 ); |
189 | $opts->add( 'dir', '' ); |
190 | $opts->add( 'token', '' ); |
191 | $opts->add( 'action', '' ); |
192 | $opts->add( 'users', [] ); |
193 | $opts->add( 'blockreason', 'other' ); |
194 | $opts->add( 'blockreason-other', '' ); |
195 | $opts->add( 'blocktalk', false ); |
196 | $opts->add( 'blockemail', false ); |
197 | $opts->add( 'reblock', false ); |
198 | $opts->add( 'usetag', false ); |
199 | $opts->add( 'usettag', false ); |
200 | $opts->add( 'blocktag', '' ); |
201 | $opts->add( 'talktag', '' ); |
202 | $opts->fetchValuesFromRequest( $request ); |
203 | |
204 | // If the client has provided a token, they are trying to paginate. |
205 | // If the token is valid, then use the values from this and later |
206 | // don't log this as a new check. |
207 | $tokenData = $this->tokenQueryManager->getDataFromRequest( $this->getRequest() ); |
208 | $validatedRequest = $this->getRequest(); |
209 | $user = ''; |
210 | if ( $tokenData ) { |
211 | foreach ( |
212 | array_diff( AbstractCheckUserPager::TOKEN_MANAGED_FIELDS, array_keys( $tokenData ) ) as $key |
213 | ) { |
214 | $opts->reset( $key ); |
215 | $validatedRequest->unsetVal( $key ); |
216 | } |
217 | foreach ( $tokenData as $key => $value ) { |
218 | // Update the FormOptions |
219 | if ( $key === 'user' ) { |
220 | $user = $value; |
221 | } else { |
222 | $opts->setValue( $key, $value, true ); |
223 | } |
224 | // Update the actual request so that IndexPager.php reads the validated values. |
225 | // (used for dir, offset and limit) |
226 | $validatedRequest->setVal( $key, $value ); |
227 | } |
228 | } else { |
229 | $user = trim( |
230 | $request->getText( 'user', $request->getText( 'ip', $subPage ?? '' ) ) |
231 | ); |
232 | } |
233 | $this->getContext()->setRequest( $validatedRequest ); |
234 | $this->opts = $opts; |
235 | |
236 | // Normalise 'user' parameter and ignore if not valid (T217713) |
237 | // It must be valid when making a link to Special:CheckUserLog/<user>. |
238 | $userTitle = Title::makeTitleSafe( NS_USER, $user ); |
239 | $user = $userTitle ? $userTitle->getText() : ''; |
240 | |
241 | $out = $this->getOutput(); |
242 | $links = []; |
243 | $out->enableOOUI(); |
244 | $out->addModuleStyles( 'oojs-ui.styles.icons-interactions' ); |
245 | $icon = new IconWidget( [ 'icon' => 'lightbulb' ] ); |
246 | $investigateLink = $this->getLinkRenderer()->makeKnownLink( |
247 | SpecialPage::getTitleFor( 'Investigate' ), |
248 | $this->msg( 'checkuser-link-investigate-label' )->text() |
249 | ); |
250 | $out->setIndicators( [ 'investigate-link' => $icon . $investigateLink ] ); |
251 | $query = []; |
252 | if ( $user !== '' ) { |
253 | $query['targets'] = $user; |
254 | } |
255 | $links[] = Html::rawElement( |
256 | 'span', |
257 | [], |
258 | $this->getLinkRenderer()->makeKnownLink( |
259 | SpecialPage::getTitleFor( 'Investigate' ), |
260 | $this->msg( $user ? 'checkuser-investigate-this-user' : 'checkuser-show-investigate' )->text(), |
261 | [], |
262 | $query |
263 | ) |
264 | ); |
265 | if ( $this->permissionManager->userHasRight( $this->getUser(), 'checkuser-log' ) ) { |
266 | $links[] = Html::rawElement( |
267 | 'span', |
268 | [], |
269 | $this->getLinkRenderer()->makeKnownLink( |
270 | SpecialPage::getTitleFor( 'CheckUserLog' ), |
271 | $this->msg( 'checkuser-showlog' )->text() |
272 | ) |
273 | ); |
274 | if ( $user !== '' ) { |
275 | $links[] = Html::rawElement( |
276 | 'span', |
277 | [], |
278 | $this->getLinkRenderer()->makeKnownLink( |
279 | SpecialPage::getTitleFor( 'CheckUserLog', $user ), |
280 | $this->msg( 'checkuser-recent-checks' )->text() |
281 | ) |
282 | ); |
283 | } |
284 | } |
285 | |
286 | if ( count( $links ) ) { |
287 | $out->addSubtitle( Html::rawElement( |
288 | 'span', |
289 | [ 'class' => 'mw-checkuser-links-no-parentheses' ], |
290 | Html::openElement( 'span' ) . |
291 | implode( |
292 | Html::closeElement( 'span' ) . Html::openElement( 'span' ), |
293 | $links |
294 | ) . |
295 | Html::closeElement( 'span' ) |
296 | ) ); |
297 | } |
298 | |
299 | $userIdentity = null; |
300 | $isIP = false; |
301 | $xfor = false; |
302 | $m = []; |
303 | if ( IPUtils::isIPAddress( $user ) ) { |
304 | // A single IP address or an IP range |
305 | $userIdentity = UserIdentityValue::newAnonymous( IPUtils::sanitizeIP( $user ) ); |
306 | $isIP = true; |
307 | } elseif ( preg_match( '/^(.+)\/xff$/', $user, $m ) && IPUtils::isIPAddress( $m[1] ) ) { |
308 | // A single IP address or range with XFF string included |
309 | $userIdentity = UserIdentityValue::newAnonymous( IPUtils::sanitizeIP( $m[1] ) ); |
310 | $xfor = true; |
311 | $isIP = true; |
312 | } else { |
313 | // A user? |
314 | if ( $user ) { |
315 | $userIdentity = $this->userIdentityLookup->getUserIdentityByName( $user ); |
316 | } |
317 | } |
318 | |
319 | $this->showIntroductoryText(); |
320 | $this->showForm( $user, $isIP ); |
321 | |
322 | // Perform one of the various submit operations... |
323 | if ( $request->wasPosted() ) { |
324 | $checkType = $this->opts->getValue( 'checktype' ); |
325 | if ( !$this->getUser()->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) { |
326 | $out->wrapWikiMsg( '<div class="error">$1</div>', 'checkuser-token-fail' ); |
327 | } elseif ( $this->opts->getValue( 'action' ) === 'block' ) { |
328 | $this->doMassUserBlock(); |
329 | } elseif ( !$this->checkReason() ) { |
330 | $out->addWikiMsg( 'checkuser-noreason' ); |
331 | } elseif ( $checkType == self::SUBTYPE_GET_IPS ) { |
332 | if ( $isIP || !$user ) { |
333 | $out->addWikiMsg( 'nouserspecified' ); |
334 | } elseif ( !$userIdentity || !$userIdentity->getId() ) { |
335 | $out->addWikiMsg( 'nosuchusershort', $user ); |
336 | } else { |
337 | $pager = $this->getPager( self::SUBTYPE_GET_IPS, $userIdentity, 'userips' ); |
338 | $out->addHtml( $pager->getBody() ); |
339 | } |
340 | } elseif ( $checkType == self::SUBTYPE_GET_ACTIONS ) { |
341 | if ( $isIP && $userIdentity ) { |
342 | // Target is a IP or range |
343 | if ( !$this->checkUserLookupUtils->isValidIPOrRange( $userIdentity->getName() ) ) { |
344 | $out->addWikiMsg( 'checkuser-range-outside-limit', $userIdentity->getName() ); |
345 | } else { |
346 | $logType = $xfor ? 'ipedits-xff' : 'ipedits'; |
347 | |
348 | // Ordered in descent by timestamp. Can cause large filesorts on range scans. |
349 | $pager = $this->getPager( self::SUBTYPE_GET_ACTIONS, $userIdentity, $logType, $xfor ); |
350 | $out->addHTML( $pager->getBody() ); |
351 | } |
352 | } else { |
353 | // Target is a username |
354 | if ( !$user ) { |
355 | $out->addWikiMsg( 'nouserspecified' ); |
356 | } elseif ( !$userIdentity || !$userIdentity->getId() ) { |
357 | $out->addHTML( $this->msg( 'nosuchusershort', $user )->parseAsBlock() ); |
358 | } else { |
359 | // Sorting might take some time |
360 | // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged |
361 | @set_time_limit( 60 ); |
362 | |
363 | $pager = $this->getPager( self::SUBTYPE_GET_ACTIONS, $userIdentity, 'useredits' ); |
364 | $out->addHTML( $pager->getBody() ); |
365 | } |
366 | } |
367 | } elseif ( $checkType == self::SUBTYPE_GET_USERS ) { |
368 | if ( !$isIP || !$userIdentity ) { |
369 | $out->addWikiMsg( 'badipaddress' ); |
370 | } elseif ( !$this->checkUserLookupUtils->isValidIPOrRange( $userIdentity->getName() ) ) { |
371 | $out->addWikiMsg( 'checkuser-range-outside-limit', $userIdentity->getName() ); |
372 | } else { |
373 | $logType = $xfor ? 'ipusers-xff' : 'ipusers'; |
374 | |
375 | $pager = $this->getPager( self::SUBTYPE_GET_USERS, $userIdentity, $logType, $xfor ); |
376 | $out->addHTML( $pager->getBody() ); |
377 | } |
378 | } |
379 | } |
380 | // Add CIDR calculation convenience JS form |
381 | $this->addJsCIDRForm(); |
382 | $out->addJsConfigVars( |
383 | 'wgCheckUserDisplayClientHints', |
384 | $this->getConfig()->get( 'CheckUserDisplayClientHints' ) |
385 | ); |
386 | $out->addModules( 'ext.checkUser' ); |
387 | $out->addModuleStyles( [ |
388 | 'mediawiki.interface.helpers.styles', |
389 | 'ext.checkUser.styles', |
390 | ] ); |
391 | } |
392 | |
393 | protected function showIntroductoryText() { |
394 | $config = $this->getConfig(); |
395 | $cidrLimit = $config->get( 'CheckUserCIDRLimit' ); |
396 | $maximumRowCount = $config->get( 'CheckUserMaximumRowCount' ); |
397 | $this->getOutput()->addWikiMsg( |
398 | 'checkuser-summary', |
399 | $cidrLimit['IPv4'], |
400 | $cidrLimit['IPv6'], |
401 | Message::numParam( $maximumRowCount ) |
402 | ); |
403 | } |
404 | |
405 | /** |
406 | * Show the CheckUser query form |
407 | * |
408 | * @param string $user |
409 | * @param bool $isIP |
410 | */ |
411 | protected function showForm( string $user, bool $isIP ) { |
412 | // Fill in requested type if it makes sense |
413 | $ipAllowed = true; |
414 | $checktype = $this->opts->getValue( 'checktype' ); |
415 | if ( $checktype == self::SUBTYPE_GET_USERS && $isIP ) { |
416 | $checkTypeValidated = $checktype; |
417 | $ipAllowed = false; |
418 | } elseif ( $checktype == self::SUBTYPE_GET_IPS && !$isIP ) { |
419 | $checkTypeValidated = $checktype; |
420 | } elseif ( $checktype == self::SUBTYPE_GET_ACTIONS ) { |
421 | $checkTypeValidated = $checktype; |
422 | // Defaults otherwise |
423 | } elseif ( $isIP ) { |
424 | $checkTypeValidated = self::SUBTYPE_GET_ACTIONS; |
425 | } else { |
426 | $checkTypeValidated = self::SUBTYPE_GET_IPS; |
427 | $ipAllowed = false; |
428 | } |
429 | |
430 | $fields = [ |
431 | 'target' => [ |
432 | 'type' => 'user', |
433 | // validation in execute() currently |
434 | 'exists' => false, |
435 | 'ipallowed' => $ipAllowed, |
436 | 'iprange' => $ipAllowed, |
437 | 'name' => 'user', |
438 | 'label-message' => 'checkuser-target', |
439 | 'default' => $user, |
440 | 'id' => 'checktarget', |
441 | ], |
442 | 'radiooptions' => [ |
443 | 'type' => 'radio', |
444 | 'options-messages' => [ |
445 | 'checkuser-ips' => self::SUBTYPE_GET_IPS, |
446 | 'checkuser-actions' => self::SUBTYPE_GET_ACTIONS, |
447 | 'checkuser-users' => self::SUBTYPE_GET_USERS, |
448 | ], |
449 | 'id' => 'checkuserradios', |
450 | 'default' => $checkTypeValidated, |
451 | 'name' => 'checktype', |
452 | 'nodata' => 'yes', |
453 | 'flatlist' => true, |
454 | ], |
455 | 'period' => [ |
456 | 'type' => 'select', |
457 | 'id' => 'period', |
458 | 'label-message' => 'checkuser-period', |
459 | 'options-messages' => [ |
460 | 'checkuser-week-1' => 7, |
461 | 'checkuser-week-2' => 14, |
462 | 'checkuser-month' => 30, |
463 | 'checkuser-month-2' => 60, |
464 | 'checkuser-all' => 0, |
465 | ], |
466 | 'default' => $this->opts->getValue( 'period' ), |
467 | 'name' => 'period', |
468 | ], |
469 | 'reason' => [ |
470 | 'type' => 'text', |
471 | 'default' => $this->opts->getValue( 'reason' ), |
472 | 'label-message' => 'checkuser-reason', |
473 | 'size' => 46, |
474 | 'maxlength' => 150, |
475 | 'id' => 'checkreason', |
476 | 'name' => 'reason', |
477 | ], |
478 | ]; |
479 | |
480 | $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() ); |
481 | $form->setMethod( 'post' ) |
482 | ->setWrapperLegendMsg( 'checkuser-query' ) |
483 | ->setSubmitTextMsg( 'checkuser-check' ) |
484 | ->setId( 'checkuserform' ) |
485 | ->setSubmitId( 'checkusersubmit' ) |
486 | ->setSubmitName( 'checkusersubmit' ) |
487 | ->prepareForm() |
488 | ->displayForm( false ); |
489 | } |
490 | |
491 | /** |
492 | * Make a quick JS form for admins to calculate block ranges |
493 | */ |
494 | protected function addJsCIDRForm() { |
495 | $out = $this->getOutput(); |
496 | $out->addHTML( ( new CIDRCalculator( $out ) )->getHtml() ); |
497 | } |
498 | |
499 | /** |
500 | * @return bool |
501 | */ |
502 | protected function checkReason(): bool { |
503 | return ( !$this->getConfig()->get( 'CheckUserForceSummary' ) || strlen( $this->opts->getValue( 'reason' ) ) ); |
504 | } |
505 | |
506 | /** |
507 | * Block a list of selected users |
508 | * with options provided in the POST request. |
509 | */ |
510 | protected function doMassUserBlock() { |
511 | $users = $this->opts->getValue( 'users' ); |
512 | $reason = $this->opts->getValue( 'blockreason-other' ); |
513 | $reasonPrefix = $this->opts->getValue( 'blockreason' ); |
514 | |
515 | if ( $reasonPrefix !== '' && $reasonPrefix !== 'other' ) { |
516 | $reason = $reasonPrefix . $this->msg( 'colon-separator' )->inContentLanguage()->text() . $reason; |
517 | } |
518 | $blockParams = [ |
519 | 'reason' => $reason, |
520 | 'email' => $this->opts->getValue( 'blockemail' ), |
521 | 'talk' => $this->opts->getValue( 'blocktalk' ), |
522 | 'reblock' => $this->opts->getValue( 'reblock' ), |
523 | ]; |
524 | $tag = $this->opts->getValue( 'usetag' ) ? |
525 | trim( $this->opts->getValue( 'blocktag' ) ) : ''; |
526 | $talkTag = $this->opts->getValue( 'usettag' ) ? |
527 | trim( $this->opts->getValue( 'talktag' ) ) : ''; |
528 | $usersCount = count( $users ); |
529 | |
530 | if ( |
531 | !$usersCount |
532 | || !$this->permissionManager->userHasRight( $this->getUser(), 'block' ) |
533 | || $this->getUser()->getBlock() |
534 | ) { |
535 | $this->getOutput()->addWikiMsg( 'checkuser-block-failure' ); |
536 | return; |
537 | } |
538 | |
539 | if ( $usersCount > $this->getConfig()->get( 'CheckUserMaxBlocks' ) ) { |
540 | $this->getOutput()->addWikiMsg( 'checkuser-block-limit' ); |
541 | return; |
542 | } |
543 | |
544 | if ( !$blockParams['reason'] ) { |
545 | $this->getOutput()->addWikiMsg( 'checkuser-block-noreason' ); |
546 | return; |
547 | } |
548 | |
549 | [ $blockedUsers, $taggedUsers ] = $this->doMassUserBlockInternal( |
550 | $users, |
551 | $blockParams, |
552 | $this->opts->getValue( 'usetag' ), |
553 | $tag, |
554 | $this->opts->getValue( 'usettag' ), |
555 | $talkTag |
556 | ); |
557 | $blockedCount = count( $blockedUsers ); |
558 | $taggedCount = count( $taggedUsers ); |
559 | $lang = $this->getLanguage(); |
560 | if ( $blockedCount > 0 ) { |
561 | $this->getOutput()->addWikiMsg( 'checkuser-block-success', |
562 | $lang->listToText( $blockedUsers ), |
563 | $lang->formatNum( $blockedCount ) |
564 | ); |
565 | } |
566 | if ( $taggedCount > 0 ) { |
567 | $this->getOutput()->addWikiMsg( 'checkuser-block-success-tagged', |
568 | $lang->listToText( $taggedUsers ), |
569 | $lang->formatNum( $taggedCount ) |
570 | ); |
571 | } |
572 | if ( $blockedCount === 0 && $taggedCount === 0 ) { |
573 | $this->getOutput()->addWikiMsg( 'checkuser-block-failure' ); |
574 | } |
575 | } |
576 | |
577 | /** |
578 | * Block a list of selected users |
579 | * |
580 | * @param string[] $users |
581 | * @param array $blockParams |
582 | * @param bool $useTag whether to perform the user page replacement |
583 | * @param string $tag replace the user page with this content |
584 | * @param bool $useTalkTag whether to perform the user talk page replacement |
585 | * @param string $talkTag replace the user talk page this with content |
586 | * @return string[][] List of html-safe usernames which were blocked at index 0 and tagged at index 1 |
587 | */ |
588 | protected function doMassUserBlockInternal( |
589 | array $users, |
590 | array $blockParams, |
591 | bool $useTag = false, |
592 | string $tag = '', |
593 | bool $useTalkTag = false, |
594 | string $talkTag = '' |
595 | ) { |
596 | $blockedUsers = []; |
597 | $taggedUsers = []; |
598 | foreach ( $users as $name ) { |
599 | $u = $this->userFactory->newFromName( $name, UserRigorOptions::RIGOR_NONE ); |
600 | // Do some checks to make sure we can block this user first |
601 | if ( !$u ) { |
602 | // Invalid user |
603 | continue; |
604 | } |
605 | $isIP = IPUtils::isIPAddress( $u->getName() ); |
606 | if ( !$u->getId() && !$isIP ) { |
607 | // Not a registered user or an IP |
608 | continue; |
609 | } |
610 | |
611 | if ( |
612 | !isset( $blockParams['email'] ) || |
613 | $blockParams['email'] === false || |
614 | $this->blockPermissionCheckerFactory |
615 | ->newBlockPermissionChecker( |
616 | $u, |
617 | $this->getUser() |
618 | ) |
619 | ->checkEmailPermissions() |
620 | ) { |
621 | $res = $this->blockUserFactory->newBlockUser( |
622 | $u, |
623 | $this->getAuthority(), |
624 | $isIP ? '1 week' : 'indefinite', |
625 | $blockParams['reason'], |
626 | [ |
627 | 'isCreateAccountBlocked' => true, |
628 | 'isEmailBlocked' => $blockParams['email'] ?? false, |
629 | 'isHardBlock' => !$isIP, |
630 | 'isAutoblocking' => true, |
631 | 'isUserTalkEditBlocked' => $blockParams['talk'] ?? false, |
632 | ] |
633 | )->placeBlock( $blockParams['reblock'] ); |
634 | |
635 | if ( |
636 | $res->isGood() || |
637 | ( $res->getStatusValue()->hasMessage( 'ipb_already_blocked' ) && $blockParams['reblock'] ) |
638 | ) { |
639 | // Mark as blocked and then attempt to tag if the block went through or |
640 | // if reblock was enabled and there existed a block with the same parameters. |
641 | $userPage = $u->getUserPage(); |
642 | $userText = "[[{$userPage->getPrefixedText()}|{$userPage->getText()}]]"; |
643 | |
644 | $blockedUsers[] = $userText; |
645 | |
646 | if ( $useTag || $useTalkTag ) { |
647 | $userPageTagSuccess = true; |
648 | $userTalkPageTagSuccess = true; |
649 | |
650 | // Tag user page and user talk page |
651 | if ( $useTag ) { |
652 | $userPageTagStatus = $this->tagPage( |
653 | $userPage, |
654 | $tag, |
655 | $blockParams['reason'] |
656 | ); |
657 | // Mark as a success if the edit went through or if the |
658 | // content that was used is the same as what is already on |
659 | // the page. |
660 | $userPageTagSuccess = $userPageTagStatus->isGood() || |
661 | $userPageTagStatus->hasMessage( 'edit-no-change' ); |
662 | } |
663 | if ( $useTalkTag ) { |
664 | $userTalkPageTagStatus = $this->tagPage( |
665 | $u->getTalkPage(), |
666 | $talkTag, |
667 | $blockParams['reason'] |
668 | ); |
669 | $userTalkPageTagSuccess = $userTalkPageTagStatus->isGood() || |
670 | $userTalkPageTagStatus->hasMessage( 'edit-no-change' ); |
671 | } |
672 | if ( $userPageTagSuccess && $userTalkPageTagSuccess ) { |
673 | // Only mark as tagged if all tags requested |
674 | // for this user was successfully added |
675 | $taggedUsers[] = $userText; |
676 | } |
677 | } |
678 | } |
679 | } |
680 | } |
681 | |
682 | return [ $blockedUsers, $taggedUsers ]; |
683 | } |
684 | |
685 | /** |
686 | * Make an edit to the given page with the tag provided |
687 | * |
688 | * @param Title $title |
689 | * @param string $tag |
690 | * @param string $summary |
691 | * @return Status the status of the edit to the $title |
692 | */ |
693 | protected function tagPage( Title $title, string $tag, string $summary ) { |
694 | // Check length to avoid mistakes |
695 | if ( strlen( $tag ) > 2 ) { |
696 | $page = $this->wikiPageFactory->newFromTitle( $title ); |
697 | $flags = 0; |
698 | if ( $page->exists() ) { |
699 | $flags |= EDIT_MINOR; |
700 | } |
701 | return $page->doUserEditContent( |
702 | new WikitextContent( $tag ), |
703 | $this->getUser(), |
704 | $summary, |
705 | $flags |
706 | ); |
707 | } |
708 | return Status::newFatal( 'checkuser-block-failure-tag-too-small' ); |
709 | } |
710 | |
711 | /** |
712 | * Gets the pager for the specific check type. |
713 | * Returns null if the checktype is not recognised. |
714 | * |
715 | * @param string $checkType |
716 | * @param UserIdentity $userIdentity |
717 | * @param string $logType |
718 | * @param bool|null $xfor |
719 | * @return AbstractCheckUserPager|null |
720 | */ |
721 | public function getPager( string $checkType, UserIdentity $userIdentity, string $logType, ?bool $xfor = null ) { |
722 | switch ( $checkType ) { |
723 | case self::SUBTYPE_GET_IPS: |
724 | return new CheckUserGetIPsPager( |
725 | $this->opts, |
726 | $userIdentity, |
727 | $logType, |
728 | $this->tokenQueryManager, |
729 | $this->userGroupManager, |
730 | $this->centralIdLookup, |
731 | $this->dbProvider, |
732 | $this->getSpecialPageFactory(), |
733 | $this->userIdentityLookup, |
734 | $this->checkUserLogService, |
735 | $this->userFactory, |
736 | $this->checkUserLookupUtils |
737 | ); |
738 | case self::SUBTYPE_GET_USERS: |
739 | return new CheckUserGetUsersPager( |
740 | $this->opts, |
741 | $userIdentity, |
742 | $xfor ?? false, |
743 | $logType, |
744 | $this->tokenQueryManager, |
745 | $this->permissionManager, |
746 | $this->blockPermissionCheckerFactory, |
747 | $this->userGroupManager, |
748 | $this->centralIdLookup, |
749 | $this->dbProvider, |
750 | $this->getSpecialPageFactory(), |
751 | $this->userIdentityLookup, |
752 | $this->userFactory, |
753 | $this->checkUserLogService, |
754 | $this->checkUserLookupUtils, |
755 | $this->userEditTracker, |
756 | $this->checkUserUtilityService, |
757 | $this->clientHintsLookup, |
758 | $this->clientHintsFormatter |
759 | ); |
760 | case self::SUBTYPE_GET_ACTIONS: |
761 | return new CheckUserGetActionsPager( |
762 | $this->opts, |
763 | $userIdentity, |
764 | $xfor, |
765 | $logType, |
766 | $this->tokenQueryManager, |
767 | $this->userGroupManager, |
768 | $this->centralIdLookup, |
769 | $this->linkBatchFactory, |
770 | $this->dbProvider, |
771 | $this->getSpecialPageFactory(), |
772 | $this->userIdentityLookup, |
773 | $this->userFactory, |
774 | $this->checkUserLookupUtils, |
775 | $this->checkUserLogService, |
776 | $this->commentFormatter, |
777 | $this->userEditTracker, |
778 | $this->hookRunner, |
779 | $this->checkUserUtilityService, |
780 | $this->commentStore, |
781 | $this->clientHintsLookup, |
782 | $this->clientHintsFormatter |
783 | ); |
784 | default: |
785 | return null; |
786 | } |
787 | } |
788 | |
789 | /** |
790 | * Return an array of subpages beginning with $search that this special page will accept. |
791 | * |
792 | * @param string $search Prefix to search for |
793 | * @param int $limit Maximum number of results to return (usually 10) |
794 | * @param int $offset Number of results to skip (usually 0) |
795 | * @return string[] Matching subpages |
796 | */ |
797 | public function prefixSearchSubpages( $search, $limit, $offset ) { |
798 | if ( !$this->userNameUtils->isValid( $search ) ) { |
799 | // No prefix suggestion for invalid user |
800 | return []; |
801 | } |
802 | // Autocomplete subpage as user list - public to allow caching |
803 | return $this->userNamePrefixSearch->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset ); |
804 | } |
805 | |
806 | /** |
807 | * @inheritDoc |
808 | */ |
809 | protected function getGroupName() { |
810 | return 'users'; |
811 | } |
812 | } |