Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
83.04% |
612 / 737 |
|
56.76% |
21 / 37 |
CRAP | |
0.00% |
0 / 1 |
SpecialCentralAuth | |
83.04% |
612 / 737 |
|
56.76% |
21 / 37 |
233.27 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
90.59% |
77 / 85 |
|
0.00% |
0 / 1 |
25.52 | |||
showNonexistentError | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
showRenameInProgressError | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
doSubmit | |
95.45% |
42 / 44 |
|
0.00% |
0 / 1 |
16 | |||
showStatusError | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
showError | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
showSuccess | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
showUsernameForm | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
3 | |||
showInfo | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
2 | |||
getInfoFields | |
57.47% |
50 / 87 |
|
0.00% |
0 / 1 |
35.69 | |||
showGlobalBlockingExemptWikisList | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
2 | |||
getGlobalBlockingExemptWikiTableRows | |
88.89% |
16 / 18 |
|
0.00% |
0 / 1 |
5.03 | |||
getFieldLayoutForHtmlContent | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
showWikiLists | |
100.00% |
30 / 30 |
|
100.00% |
1 / 1 |
4 | |||
getWikiListsTable | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
3 | |||
listAccounts | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
listWikiItem | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
4 | |||
getAttachedTimestampField | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
formatMergeMethod | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
1 | |||
formatBlockStatus | |
83.33% |
25 / 30 |
|
0.00% |
0 / 1 |
5.12 | |||
formatBlockParams | |
83.87% |
26 / 31 |
|
0.00% |
0 / 1 |
8.27 | |||
getRestrictionListHTML | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
20 | |||
formatEditcount | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
2.00 | |||
formatGroups | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
4.13 | |||
foreignLink | |
92.86% |
13 / 14 |
|
0.00% |
0 / 1 |
4.01 | |||
foreignUserLink | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
2.00 | |||
adminCheck | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
showDeleteGlobalAccountForm | |
100.00% |
32 / 32 |
|
100.00% |
1 / 1 |
1 | |||
showStatusForm | |
100.00% |
65 / 65 |
|
100.00% |
1 / 1 |
2 | |||
getFramedFieldsetLayout | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
1 | |||
showLogExtract | |
62.07% |
18 / 29 |
|
0.00% |
0 / 1 |
7.96 | |||
evaluateTotalEditcount | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getMergeMethodDescriptions | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
prefixSearchSubpages | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\CentralAuth\Special; |
4 | |
5 | use DateInterval; |
6 | use Exception; |
7 | use InvalidArgumentException; |
8 | use LogEventsList; |
9 | use MediaWiki\Block\Restriction\ActionRestriction; |
10 | use MediaWiki\Block\Restriction\NamespaceRestriction; |
11 | use MediaWiki\Block\Restriction\PageRestriction; |
12 | use MediaWiki\CommentFormatter\CommentFormatter; |
13 | use MediaWiki\CommentStore\CommentStore; |
14 | use MediaWiki\Context\DerivativeContext; |
15 | use MediaWiki\Extension\CentralAuth\CentralAuthDatabaseManager; |
16 | use MediaWiki\Extension\CentralAuth\CentralAuthUIService; |
17 | use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameFactory; |
18 | use MediaWiki\Extension\CentralAuth\Hooks\CentralAuthHookRunner; |
19 | use MediaWiki\Extension\CentralAuth\User\CentralAuthGlobalRegistrationProvider; |
20 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
21 | use MediaWiki\Extension\CentralAuth\Widget\HTMLGlobalUserTextField; |
22 | use MediaWiki\Extension\GlobalBlocking\GlobalBlockingServices; |
23 | use MediaWiki\Extension\GlobalBlocking\Services\GlobalBlockLookup; |
24 | use MediaWiki\Html\Html; |
25 | use MediaWiki\HTMLForm\HTMLForm; |
26 | use MediaWiki\MainConfigNames; |
27 | use MediaWiki\MediaWikiServices; |
28 | use MediaWiki\Message\Message; |
29 | use MediaWiki\Parser\Sanitizer; |
30 | use MediaWiki\Registration\ExtensionRegistry; |
31 | use MediaWiki\SpecialPage\SpecialPage; |
32 | use MediaWiki\Title\NamespaceInfo; |
33 | use MediaWiki\Title\Title; |
34 | use MediaWiki\User\Registration\UserRegistrationLookup; |
35 | use MediaWiki\User\TempUser\TempUserConfig; |
36 | use MediaWiki\User\User; |
37 | use MediaWiki\User\UserFactory; |
38 | use MediaWiki\User\UserGroupMembership; |
39 | use MediaWiki\User\UserNameUtils; |
40 | use MediaWiki\Utils\MWTimestamp; |
41 | use MediaWiki\WikiMap\WikiMap; |
42 | use MediaWiki\WikiMap\WikiReference; |
43 | use MediaWiki\Xml\Xml; |
44 | use OOUI\FieldLayout; |
45 | use OOUI\FieldsetLayout; |
46 | use OOUI\HtmlSnippet; |
47 | use OOUI\PanelLayout; |
48 | use OOUI\Widget; |
49 | use Wikimedia\Message\MessageSpecifier; |
50 | use Wikimedia\Rdbms\IExpression; |
51 | use Wikimedia\Rdbms\LikeValue; |
52 | |
53 | class SpecialCentralAuth extends SpecialPage { |
54 | |
55 | /** @var string */ |
56 | private $mUserName; |
57 | /** @var bool */ |
58 | private $mCanUnmerge; |
59 | /** @var bool */ |
60 | private $mCanLock; |
61 | /** @var bool */ |
62 | private $mCanSuppress; |
63 | /** @var bool */ |
64 | private $mCanEdit; |
65 | /** @var bool */ |
66 | private $mCanChangeGroups; |
67 | |
68 | /** |
69 | * @var CentralAuthUser |
70 | */ |
71 | private $mGlobalUser; |
72 | |
73 | /** |
74 | * @var array[] |
75 | */ |
76 | private $mAttachedLocalAccounts; |
77 | |
78 | /** |
79 | * @var array |
80 | */ |
81 | private $mUnattachedLocalAccounts; |
82 | |
83 | /** @var string */ |
84 | private $mMethod; |
85 | |
86 | /** @var bool */ |
87 | private $mPosted; |
88 | |
89 | /** @var string[] */ |
90 | private $mWikis; |
91 | |
92 | private CommentFormatter $commentFormatter; |
93 | private NamespaceInfo $namespaceInfo; |
94 | private TempUserConfig $tempUserConfig; |
95 | private UserFactory $userFactory; |
96 | private UserNameUtils $userNameUtils; |
97 | private UserRegistrationLookup $userRegistrationLookup; |
98 | private CentralAuthDatabaseManager $databaseManager; |
99 | private CentralAuthUIService $uiService; |
100 | private GlobalRenameFactory $globalRenameFactory; |
101 | |
102 | public function __construct( |
103 | CommentFormatter $commentFormatter, |
104 | NamespaceInfo $namespaceInfo, |
105 | TempUserConfig $tempUserConfig, |
106 | UserFactory $userFactory, |
107 | UserNameUtils $userNameUtils, |
108 | UserRegistrationLookup $userRegistrationLookup, |
109 | CentralAuthDatabaseManager $databaseManager, |
110 | CentralAuthUIService $uiService, |
111 | GlobalRenameFactory $globalRenameFactory |
112 | ) { |
113 | parent::__construct( 'CentralAuth' ); |
114 | $this->commentFormatter = $commentFormatter; |
115 | $this->namespaceInfo = $namespaceInfo; |
116 | $this->tempUserConfig = $tempUserConfig; |
117 | $this->userFactory = $userFactory; |
118 | $this->userNameUtils = $userNameUtils; |
119 | $this->userRegistrationLookup = $userRegistrationLookup; |
120 | $this->databaseManager = $databaseManager; |
121 | $this->uiService = $uiService; |
122 | $this->globalRenameFactory = $globalRenameFactory; |
123 | } |
124 | |
125 | public function doesWrites() { |
126 | return true; |
127 | } |
128 | |
129 | /** @inheritDoc */ |
130 | public function execute( $subpage ) { |
131 | $this->setHeaders(); |
132 | $this->addHelpLink( 'Extension:CentralAuth' ); |
133 | |
134 | $authority = $this->getContext()->getAuthority(); |
135 | $this->mCanUnmerge = $authority->isAllowed( 'centralauth-unmerge' ); |
136 | $this->mCanLock = $authority->isAllowed( 'centralauth-lock' ); |
137 | $this->mCanSuppress = $authority->isAllowed( 'centralauth-suppress' ); |
138 | $this->mCanEdit = $this->mCanUnmerge || $this->mCanLock || $this->mCanSuppress; |
139 | $this->mCanChangeGroups = $authority->isAllowed( 'globalgroupmembership' ); |
140 | |
141 | $this->getOutput()->setPageTitleMsg( |
142 | $this->msg( $this->mCanEdit ? 'centralauth' : 'centralauth-ro' ) |
143 | ); |
144 | $this->getOutput()->addModules( 'ext.centralauth' ); |
145 | $this->getOutput()->addModuleStyles( 'ext.centralauth.misc.styles' ); |
146 | $this->getOutput()->addJsConfigVars( |
147 | 'wgMergeMethodDescriptions', $this->getMergeMethodDescriptions() |
148 | ); |
149 | |
150 | $this->mUserName = trim( |
151 | str_replace( |
152 | '_', |
153 | ' ', |
154 | $this->getRequest()->getText( 'target', $subpage ?? '' ) |
155 | ) |
156 | ); |
157 | |
158 | $this->mUserName = $this->getContentLanguage()->ucfirst( $this->mUserName ); |
159 | |
160 | $this->mPosted = $this->getRequest()->wasPosted(); |
161 | $this->mMethod = $this->getRequest()->getVal( 'wpMethod' ); |
162 | $this->mWikis = (array)$this->getRequest()->getArray( 'wpWikis' ); |
163 | |
164 | // If wpReasonList is specified then for backwards compatability with the old format of the admin status form, |
165 | // the value of wpReason needs to be moved to wpReason-other and the value of wpReasonList needs to be moved |
166 | // to wpReason. |
167 | if ( $this->getRequest()->getVal( 'wpReasonList' ) ) { |
168 | $this->getRequest()->setVal( 'wpReason-other', $this->getRequest()->getVal( 'wpReason' ) ); |
169 | $this->getRequest()->setVal( 'wpReason', $this->getRequest()->getVal( 'wpReasonList' ) ); |
170 | $this->getRequest()->unsetVal( 'wpReasonList' ); |
171 | } |
172 | |
173 | // Possible demo states |
174 | |
175 | // success, all accounts merged |
176 | // successful login, some accounts merged, others left |
177 | // successful login, others left |
178 | // not account owner, others left |
179 | |
180 | // is owner / is not owner |
181 | // did / did not merge some accounts |
182 | // do / don't have more accounts to merge |
183 | |
184 | if ( $this->mUserName === '' ) { |
185 | # First time through |
186 | $this->getOutput()->addWikiMsg( 'centralauth-admin-intro' ); |
187 | $this->showUsernameForm(); |
188 | return; |
189 | } |
190 | |
191 | $userPage = Title::makeTitleSafe( NS_USER, $this->mUserName ); |
192 | if ( $userPage ) { |
193 | $localUser = User::newFromName( $userPage->getText(), false ); |
194 | $this->getSkin()->setRelevantUser( $localUser ); |
195 | } |
196 | |
197 | // per T49991 |
198 | $this->getOutput()->setHTMLTitle( $this->msg( |
199 | 'pagetitle', |
200 | $this->msg( |
201 | $this->mCanEdit ? 'centralauth-admin-title' : 'centralauth-admin-title-ro', |
202 | $this->mUserName |
203 | )->plain() |
204 | ) ); |
205 | |
206 | $canonUsername = $this->userNameUtils->getCanonical( $this->mUserName ); |
207 | if ( $canonUsername === false ) { |
208 | $this->showNonexistentError(); |
209 | return; |
210 | } |
211 | |
212 | $globalUser = $this->getRequest()->wasPosted() |
213 | ? CentralAuthUser::getPrimaryInstanceByName( $this->mUserName ) |
214 | : CentralAuthUser::getInstanceByName( $this->mUserName ); |
215 | $this->mGlobalUser = $globalUser; |
216 | |
217 | if ( ( $globalUser->isSuppressed() || $globalUser->isHidden() ) && |
218 | !$this->mCanSuppress |
219 | ) { |
220 | // Claim that there's nothing if the global account is hidden and the user is not |
221 | // allowed to see it. |
222 | $this->showNonexistentError(); |
223 | return; |
224 | } |
225 | |
226 | $continue = true; |
227 | if ( $this->mCanEdit && $this->mPosted ) { |
228 | $this->databaseManager->assertNotReadOnly(); |
229 | $continue = $this->doSubmit(); |
230 | } |
231 | |
232 | // Show just a user friendly message when a rename is in progress |
233 | try { |
234 | $this->mAttachedLocalAccounts = $globalUser->queryAttached(); |
235 | } catch ( Exception $e ) { |
236 | if ( $globalUser->renameInProgress() ) { |
237 | $this->showRenameInProgressError(); |
238 | return; |
239 | } |
240 | // Rethrow |
241 | throw $e; |
242 | } |
243 | |
244 | $this->mUnattachedLocalAccounts = $globalUser->queryUnattached(); |
245 | |
246 | if ( !$globalUser->exists() && !count( $this->mUnattachedLocalAccounts ) ) { |
247 | // Nothing to see here |
248 | $this->showNonexistentError(); |
249 | return; |
250 | } |
251 | |
252 | $this->showUsernameForm(); |
253 | if ( $continue && $globalUser->exists() ) { |
254 | $this->showInfo(); |
255 | $this->showGlobalBlockingExemptWikisList(); |
256 | if ( $this->mCanLock ) { |
257 | $this->showStatusForm(); |
258 | } |
259 | if ( $this->mCanUnmerge ) { |
260 | $this->showDeleteGlobalAccountForm(); |
261 | } |
262 | $this->showLogExtract(); |
263 | $this->showWikiLists(); |
264 | } elseif ( $continue && !$globalUser->exists() ) { |
265 | // No global account, but we can still list the local ones |
266 | $this->showError( 'centralauth-admin-nonexistent', $this->mUserName ); |
267 | $this->showWikiLists(); |
268 | } |
269 | } |
270 | |
271 | private function showNonexistentError() { |
272 | $this->showError( 'centralauth-admin-nonexistent', $this->mUserName ); |
273 | $this->showUsernameForm(); |
274 | } |
275 | |
276 | private function showRenameInProgressError() { |
277 | $this->showError( 'centralauth-admin-rename-in-progress', $this->mUserName ); |
278 | $renameStatus = $this->globalRenameFactory->newGlobalRenameUserStatus( $this->mUserName ); |
279 | $names = $renameStatus->getNames(); |
280 | $this->uiService->showRenameLogExtract( $this->getContext(), $names[1] ); |
281 | } |
282 | |
283 | /** |
284 | * @return bool Returns true if the normal form should be displayed |
285 | */ |
286 | public function doSubmit() { |
287 | $deleted = false; |
288 | $globalUser = $this->mGlobalUser; |
289 | $request = $this->getRequest(); |
290 | |
291 | $givenState = $request->getVal( 'wpUserState' ); |
292 | $stateCheck = $givenState === $globalUser->getStateHash( true ); |
293 | |
294 | if ( !$this->getUser()->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) { |
295 | $this->showError( 'centralauth-token-mismatch' ); |
296 | } elseif ( $this->mMethod == 'unmerge' && $this->mCanUnmerge ) { |
297 | $status = $globalUser->adminUnattach( $this->mWikis ); |
298 | if ( !$status->isGood() ) { |
299 | $this->showStatusError( $status->getWikiText() ); |
300 | } else { |
301 | $this->showSuccess( 'centralauth-admin-unmerge-success', |
302 | $this->getLanguage()->formatNum( $status->successCount ), |
303 | /* deprecated */ $status->successCount ); |
304 | } |
305 | } elseif ( $this->mMethod == 'delete' && $this->mCanUnmerge ) { |
306 | $status = $globalUser->adminDelete( $request->getVal( 'reason' ), $this->getUser() ); |
307 | if ( !$status->isGood() ) { |
308 | $this->showStatusError( $status->getWikiText() ); |
309 | } else { |
310 | $this->showSuccess( 'centralauth-admin-delete-success', $this->mUserName ); |
311 | $deleted = true; |
312 | } |
313 | } elseif ( $this->mMethod == 'set-status' && !$stateCheck ) { |
314 | $this->showError( 'centralauth-state-mismatch' ); |
315 | } elseif ( $this->mMethod == 'set-status' && $this->mCanLock ) { |
316 | $setLocked = $request->getBool( 'wpStatusLocked' ); |
317 | $setHidden = $request->getInt( 'wpStatusHidden', -1 ); |
318 | $reason = $request->getText( 'wpReason' ); |
319 | $reasonDetail = $request->getText( 'wpReason-other' ); |
320 | |
321 | if ( $reason == 'other' ) { |
322 | $reason = $reasonDetail; |
323 | } elseif ( $reasonDetail ) { |
324 | $reason .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . |
325 | $reasonDetail; |
326 | } |
327 | |
328 | $status = $globalUser->adminLockHide( |
329 | $setLocked, |
330 | $setHidden, |
331 | $reason, |
332 | $this->getContext() |
333 | ); |
334 | |
335 | // Tell the user what happened |
336 | if ( !$status->isGood() ) { |
337 | $this->showStatusError( $status->getWikiText() ); |
338 | } elseif ( $status->successCount > 0 ) { |
339 | $this->showSuccess( 'centralauth-admin-setstatus-success', $this->mUserName ); |
340 | } |
341 | } else { |
342 | $this->showError( 'centralauth-admin-bad-input' ); |
343 | } |
344 | return !$deleted; |
345 | } |
346 | |
347 | /** |
348 | * @param string $wikitext |
349 | */ |
350 | private function showStatusError( $wikitext ) { |
351 | $out = $this->getOutput(); |
352 | $out->addHTML( |
353 | Html::errorBox( |
354 | $out->parseAsInterface( $wikitext ) |
355 | ) |
356 | ); |
357 | } |
358 | |
359 | /** |
360 | * @param string $key |
361 | * @param mixed ...$params |
362 | */ |
363 | private function showError( $key, ...$params ) { |
364 | $this->getOutput()->addHTML( Html::errorBox( $this->msg( $key, ...$params )->parse() ) ); |
365 | } |
366 | |
367 | /** |
368 | * @param string $key |
369 | * @param mixed ...$params |
370 | */ |
371 | private function showSuccess( $key, ...$params ) { |
372 | $this->getOutput()->addHTML( Html::successBox( $this->msg( $key, ...$params )->parse() ) ); |
373 | } |
374 | |
375 | private function showUsernameForm() { |
376 | $lookup = $this->msg( |
377 | $this->mCanEdit ? 'centralauth-admin-lookup-rw' : 'centralauth-admin-lookup-ro' |
378 | )->text(); |
379 | |
380 | $formDescriptor = [ |
381 | 'user' => [ |
382 | 'class' => HTMLGlobalUserTextField::class, |
383 | 'name' => 'target', |
384 | 'label-message' => 'centralauth-admin-username', |
385 | 'size' => 25, |
386 | 'id' => 'target', |
387 | 'default' => $this->mUserName, |
388 | 'required' => true |
389 | ] |
390 | ]; |
391 | |
392 | $legend = $this->msg( $this->mCanEdit ? 'centralauth-admin-manage' : 'centralauth-admin-view' )->text(); |
393 | |
394 | $context = new DerivativeContext( $this->getContext() ); |
395 | // Remove subpage |
396 | $context->setTitle( $this->getPageTitle() ); |
397 | $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $context ); |
398 | $htmlForm |
399 | ->setMethod( 'get' ) |
400 | ->setSubmitText( $lookup ) |
401 | ->setSubmitID( 'centralauth-submit-find' ) |
402 | ->setWrapperLegend( $legend ) |
403 | ->prepareForm() |
404 | ->displayForm( false ); |
405 | } |
406 | |
407 | private function showInfo() { |
408 | $attribs = $this->getInfoFields(); |
409 | |
410 | // Give grep a chance to find the usages: |
411 | // centralauth-admin-info-username, centralauth-admin-info-registered, |
412 | // centralauth-admin-info-editcount, centralauth-admin-info-locked, |
413 | // centralauth-admin-info-hidden, centralauth-admin-info-groups |
414 | $content = Xml::openElement( "ul" ); |
415 | foreach ( $attribs as $key => [ 'label' => $msg, 'data' => $data ] ) { |
416 | $content .= Html::rawElement( |
417 | 'li', |
418 | [ 'id' => "mw-centralauth-admin-info-$key" ], |
419 | $this->msg( 'centralauth-admin-info-line' ) |
420 | ->params( $this->msg( $msg )->escaped() ) |
421 | ->rawParams( $data ) |
422 | ->parse() |
423 | ); |
424 | } |
425 | $content .= Xml::closeElement( "ul" ); |
426 | |
427 | $this->getOutput()->addHTML( $this->getFramedFieldsetLayout( |
428 | $content, 'centralauth-admin-info-header', 'mw-centralauth-info' |
429 | ) ); |
430 | } |
431 | |
432 | /** |
433 | * @return array Array of arrays where each array has two keys: 'label' containing the message |
434 | * key or Message object to be used as the label, and 'data' which is the already HTML escaped |
435 | * value that is associated with the label. The first dimension keys are field names, but are currently |
436 | * not used in the UI or for generating messages. |
437 | * @phan-return array<string,array{label:string|Message,data:string}> |
438 | */ |
439 | private function getInfoFields() { |
440 | $globalUser = $this->mGlobalUser; |
441 | |
442 | $reg = $globalUser->getRegistration(); |
443 | $age = $this->uiService->prettyTimespan( |
444 | $this->getContext(), |
445 | (int)wfTimestamp( TS_UNIX ) - (int)wfTimestamp( TS_UNIX, $reg ) |
446 | ); |
447 | $attribs = [ |
448 | 'username' => htmlspecialchars( $globalUser->getName() ), |
449 | 'registered' => htmlspecialchars( |
450 | $this->getLanguage()->timeanddate( $reg, true ) . " ($age)" ), |
451 | 'editcount' => htmlspecialchars( |
452 | $this->getLanguage()->formatNum( $this->evaluateTotalEditcount() ) ), |
453 | 'attached' => htmlspecialchars( |
454 | $this->getLanguage()->formatNum( count( $this->mAttachedLocalAccounts ) ) ), |
455 | ]; |
456 | |
457 | if ( |
458 | // Renaming self is not allowed. |
459 | $globalUser->getName() !== $this->getContext()->getUser()->getName() |
460 | && $this->getContext()->getAuthority()->isAllowed( 'centralauth-rename' ) |
461 | ) { |
462 | $renameLink = $this->getLinkRenderer()->makeKnownLink( |
463 | SpecialPage::getTitleFor( 'GlobalRenameUser', $globalUser->getName() ), |
464 | $this->msg( 'centralauth-admin-info-username-rename' )->text() |
465 | ); |
466 | |
467 | $attribs['username'] .= $this->msg( 'word-separator' )->escaped(); |
468 | $attribs['username'] .= $this->msg( 'parentheses' )->rawParams( $renameLink )->escaped(); |
469 | } |
470 | |
471 | if ( count( $this->mUnattachedLocalAccounts ) ) { |
472 | $attribs['unattached'] = htmlspecialchars( |
473 | $this->getLanguage()->formatNum( count( $this->mUnattachedLocalAccounts ) ) ); |
474 | } |
475 | |
476 | if ( $globalUser->isLocked() ) { |
477 | $attribs['locked'] = $this->msg( 'centralauth-admin-yes' )->escaped(); |
478 | } |
479 | |
480 | if ( $this->mCanSuppress ) { |
481 | $attribs['hidden'] = $this->uiService->formatHiddenLevel( |
482 | $this->getContext(), |
483 | $globalUser->getHiddenLevelInt() |
484 | ); |
485 | } |
486 | |
487 | if ( $this->tempUserConfig->isTempName( $globalUser->getName() ) ) { |
488 | $localUser = $this->userFactory->newFromName( $globalUser->getName() ); |
489 | // if the central user is valid, the local username is too, but Phan doesn't know that |
490 | '@phan-var User $localUser'; |
491 | $registrationDate = $this->userRegistrationLookup |
492 | ->getRegistration( $localUser, CentralAuthGlobalRegistrationProvider::TYPE ); |
493 | $expirationDays = $this->tempUserConfig->getExpireAfterDays(); |
494 | if ( $registrationDate && $expirationDays ) { |
495 | // Add one day to account for the expiration script running daily |
496 | $expirationDate = MWTimestamp::getInstance( $registrationDate ) |
497 | ->add( new DateInterval( 'P' . ( $expirationDays + 1 ) . 'D' ) ); |
498 | if ( $expirationDate->getTimestamp() < MWTimestamp::time() ) { |
499 | $attribs['expired'] = htmlspecialchars( $this->getLanguage() |
500 | ->userTimeAndDate( $expirationDate->timestamp, $localUser ) ); |
501 | } |
502 | } |
503 | } |
504 | |
505 | // Convert the values of the existing $attribs array into arrays, where the value is placed in the 'data' |
506 | // key and the 'label' is the message key generated from the associated key. |
507 | $attribsWithMessageKeys = []; |
508 | foreach ( $attribs as $key => $value ) { |
509 | $attribsWithMessageKeys[$key] = [ |
510 | 'label' => "centralauth-admin-info-$key", |
511 | 'data' => $value, |
512 | ]; |
513 | } |
514 | |
515 | $groups = $globalUser->getGlobalGroupsWithExpiration(); |
516 | if ( $groups ) { |
517 | $groupLinks = []; |
518 | // Ensure temporary groups are displayed first, to avoid ambiguity like |
519 | // "first, second (expires at some point)" (unclear if only second expires or if both expire) |
520 | uasort( $groups, static fn ( $first, $second ) => (bool)$second <=> (bool)$first ); |
521 | |
522 | $uiLanguage = $this->getLanguage(); |
523 | $uiUser = $this->getUser(); |
524 | |
525 | foreach ( $groups as $group => $expiry ) { |
526 | $link = $this->getLinkRenderer()->makeLink( |
527 | SpecialPage::getTitleFor( 'GlobalGroupPermissions', $group ), |
528 | $uiLanguage->getGroupName( $group ) |
529 | ); |
530 | |
531 | if ( $expiry ) { |
532 | $link = $this->msg( 'group-membership-link-with-expiry' ) |
533 | ->rawParams( $link ) |
534 | ->params( $uiLanguage->userTimeAndDate( $expiry, $uiUser ) ) |
535 | ->escaped(); |
536 | } |
537 | |
538 | $groupLinks[] = $link; |
539 | } |
540 | |
541 | $attribsWithMessageKeys['groups'] = [ |
542 | 'label' => $this->msg( 'centralauth-admin-info-groups' )->numParams( count( $groups ) ), |
543 | 'data' => $uiLanguage->commaList( $groupLinks ), |
544 | ]; |
545 | } |
546 | |
547 | if ( $this->mCanChangeGroups ) { |
548 | if ( !isset( $attribsWithMessageKeys['groups'] ) ) { |
549 | $attribsWithMessageKeys['groups'] = [ |
550 | 'label' => $this->msg( 'centralauth-admin-info-groups' )->numParams( 0 ), |
551 | 'data' => $this->msg( 'rightsnone' )->escaped(), |
552 | ]; |
553 | } |
554 | |
555 | $manageGroupsLink = $this->getLinkRenderer()->makeKnownLink( |
556 | SpecialPage::getTitleFor( 'GlobalGroupMembership', $globalUser->getName() ), |
557 | $this->msg( 'centralauth-admin-info-groups-manage' )->text() |
558 | ); |
559 | |
560 | $attribsWithMessageKeys['groups']['data'] .= $this->msg( 'word-separator' )->escaped(); |
561 | $attribsWithMessageKeys['groups']['data'] .= $this->msg( 'parentheses' ) |
562 | ->rawParams( $manageGroupsLink )->escaped(); |
563 | } |
564 | |
565 | $caHookRunner = new CentralAuthHookRunner( $this->getHookContainer() ); |
566 | $caHookRunner->onCentralAuthInfoFields( $globalUser, $this->getContext(), $attribsWithMessageKeys ); |
567 | |
568 | return $attribsWithMessageKeys; |
569 | } |
570 | |
571 | private function showGlobalBlockingExemptWikisList() { |
572 | $tableRows = $this->getGlobalBlockingExemptWikiTableRows(); |
573 | if ( !$tableRows ) { |
574 | return; |
575 | } |
576 | |
577 | $header = Xml::openElement( 'thead' ) . Xml::openElement( 'tr' ); |
578 | $header .= Html::element( |
579 | 'th', |
580 | [], |
581 | $this->msg( 'centralauth-admin-globalblock-exempt-list-wiki-heading' )->text() |
582 | ); |
583 | $header .= Html::element( |
584 | 'th', |
585 | [ 'class' => 'unsortable' ], |
586 | $this->msg( 'centralauth-admin-globalblock-exempt-list-reason-heading' )->text() |
587 | ); |
588 | $header .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'thead' ); |
589 | |
590 | $body = Html::rawElement( 'tbody', [], $tableRows ); |
591 | |
592 | $tableHtml = Html::rawElement( |
593 | 'table', |
594 | [ 'class' => 'wikitable sortable mw-centralauth-globalblock-exempt-list-table' ], |
595 | $header . $body |
596 | ); |
597 | $this->getOutput()->addHTML( $this->getFramedFieldsetLayout( |
598 | $tableHtml, 'centralauth-admin-globalblock-exempt-list', |
599 | 'mw-centralauth-globalblock-exempt-list' |
600 | ) ); |
601 | } |
602 | |
603 | private function getGlobalBlockingExemptWikiTableRows(): string { |
604 | // There can be no global blocks if the GlobalBlocking extension is not loaded. |
605 | if ( !ExtensionRegistry::getInstance()->isLoaded( 'GlobalBlocking' ) ) { |
606 | return ''; |
607 | } |
608 | |
609 | $globalBlockingServices = GlobalBlockingServices::wrap( MediaWikiServices::getInstance() ); |
610 | $globalBlock = $globalBlockingServices->getGlobalBlockLookup() |
611 | ->getGlobalBlockingBlock( null, $this->mGlobalUser->getId(), GlobalBlockLookup::SKIP_LOCAL_DISABLE_CHECK ); |
612 | // If the user is not globally blocked, then their global block cannot be locally disabled (so there will be |
613 | // no rows to display). |
614 | if ( $globalBlock === null ) { |
615 | return ''; |
616 | } |
617 | |
618 | $html = ''; |
619 | $queryWikis = array_merge( $this->mAttachedLocalAccounts, $this->mUnattachedLocalAccounts ); |
620 | foreach ( $queryWikis as $wiki ) { |
621 | // Check if the global block is disabled on the given wiki, and if it is then add it to the table HTML. |
622 | $localBlockStatus = $globalBlockingServices->getGlobalBlockLocalStatusLookup() |
623 | ->getLocalWhitelistInfo( $globalBlock->gb_id, null, $wiki['wiki'] ); |
624 | if ( $localBlockStatus === false ) { |
625 | continue; |
626 | } |
627 | |
628 | $row = Html::rawElement( 'td', [], $this->foreignUserLink( $wiki['wiki'] ) ) . |
629 | Html::element( 'td', [], $localBlockStatus['reason'] ); |
630 | $html .= Html::rawElement( 'tr', [], $row ); |
631 | } |
632 | return $html; |
633 | } |
634 | |
635 | /** |
636 | * Generates a {@link FieldLayout} that can be used in a HTMLFormInfo field instance when 'rawrow' is true. |
637 | * This is useful in the case that the HTML contains elements which cannot appear inside a label element. |
638 | * |
639 | * @param string $html The HTML that we want to use in the 'info' field |
640 | * @return FieldLayout An instance suitable for use in an 'info' field with 'rawrow' set to true |
641 | */ |
642 | private function getFieldLayoutForHtmlContent( string $html ): FieldLayout { |
643 | return new FieldLayout( new Widget( [ 'content' => new HtmlSnippet( $html ) ] ) ); |
644 | } |
645 | |
646 | private function showWikiLists() { |
647 | $showUnmergeCheckboxes = $this->mCanUnmerge && $this->mGlobalUser->exists(); |
648 | |
649 | $formDescriptor = [ |
650 | 'wikilist' => [ |
651 | 'type' => 'info', |
652 | 'raw' => true, |
653 | // We need to use a "rawrow" here to prevent the table element being wrapped by a label element. |
654 | 'rawrow' => true, |
655 | // When using "rawrow" we need to provide the HTML content via a FieldLayout. |
656 | 'default' => $this->getFieldLayoutForHtmlContent( $this->getWikiListsTable( $showUnmergeCheckboxes ) ), |
657 | ], |
658 | 'Method' => [ |
659 | 'type' => 'hidden', |
660 | 'default' => 'unmerge', |
661 | ], |
662 | ]; |
663 | |
664 | if ( $showUnmergeCheckboxes ) { |
665 | $formDescriptor['submit'] = [ |
666 | 'type' => 'submit', |
667 | 'buttonlabel-message' => 'centralauth-admin-unmerge', |
668 | 'flags' => [ 'progressive' ], |
669 | ]; |
670 | } |
671 | |
672 | $context = new DerivativeContext( $this->getContext() ); |
673 | $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $context ); |
674 | $htmlForm |
675 | ->setAction( $this->getPageTitle()->getFullURL( [ 'target' => $this->mUserName ] ) ) |
676 | ->suppressDefaultSubmit() |
677 | ->setWrapperLegendMsg( |
678 | $showUnmergeCheckboxes ? 'centralauth-admin-list-legend-rw' : 'centralauth-admin-list-legend-ro' |
679 | ) |
680 | ->setId( 'mw-centralauth-merged' ) |
681 | ->prepareForm() |
682 | ->displayForm( false ); |
683 | } |
684 | |
685 | /** |
686 | * @param bool $showUnmergeCheckboxes Whether the checkboxes to allow the user to unmerge a local account should |
687 | * be shown in the table. |
688 | * @return string The HTML for the table of local accounts |
689 | */ |
690 | private function getWikiListsTable( bool $showUnmergeCheckboxes ): string { |
691 | $columns = [ |
692 | // centralauth-admin-list-localwiki |
693 | "localwiki", |
694 | // centralauth-admin-list-attached-on |
695 | "attached-on", |
696 | // centralauth-admin-list-method |
697 | "method", |
698 | // centralauth-admin-list-blocked |
699 | "blocked", |
700 | // centralauth-admin-list-editcount |
701 | "editcount", |
702 | // centralauth-admin-list-groups |
703 | "groups", |
704 | ]; |
705 | |
706 | $header = Xml::openElement( 'thead' ) . Xml::openElement( 'tr' ); |
707 | if ( $showUnmergeCheckboxes ) { |
708 | $header .= Html::element( 'th', [ 'class' => 'unsortable' ] ); |
709 | } |
710 | foreach ( $columns as $c ) { |
711 | $header .= Html::element( 'th', [], $this->msg( "centralauth-admin-list-$c" )->text() ); |
712 | } |
713 | $header .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'thead' ); |
714 | |
715 | $body = Html::rawElement( |
716 | 'tbody', [], |
717 | $this->listAccounts( $this->mAttachedLocalAccounts, $showUnmergeCheckboxes ) . |
718 | $this->listAccounts( $this->mUnattachedLocalAccounts, $showUnmergeCheckboxes ) |
719 | ); |
720 | |
721 | return Html::rawElement( |
722 | 'table', |
723 | [ 'class' => 'wikitable sortable mw-centralauth-wikislist' ], |
724 | $header . $body |
725 | ); |
726 | } |
727 | |
728 | /** |
729 | * @param array[] $list The result of {@link CentralAuthUser::queryAttached} or |
730 | * {@link CentralAuthUser::queryUnattached()} |
731 | * @return string The HTML body table rows |
732 | */ |
733 | private function listAccounts( array $list, bool $showUnmergeCheckboxes ): string { |
734 | ksort( $list ); |
735 | return implode( "\n", array_map( function ( $row ) use ( $showUnmergeCheckboxes ) { |
736 | return $this->listWikiItem( $row, $showUnmergeCheckboxes ); |
737 | }, $list ) ); |
738 | } |
739 | |
740 | /** |
741 | * @param array $row The an item from an array returned by either {@link CentralAuthUser::queryAttached} or |
742 | * {@link CentralAuthUser::queryUnattached()} |
743 | * @return string The HTML row that represents the provided array |
744 | */ |
745 | private function listWikiItem( array $row, bool $showUnmergeCheckboxes ): string { |
746 | $html = Xml::openElement( 'tr' ); |
747 | |
748 | if ( $showUnmergeCheckboxes ) { |
749 | if ( !empty( $row['attachedMethod'] ) ) { |
750 | $html .= Html::rawElement( 'td', [], $this->adminCheck( $row['wiki'] ) ); |
751 | } else { |
752 | // Account is unattached, don't show checkbox to detach |
753 | $html .= Xml::element( 'td' ); |
754 | } |
755 | } |
756 | |
757 | $html .= Html::rawElement( 'td', [], $this->foreignUserLink( $row['wiki'] ) ); |
758 | |
759 | $attachedTimestamp = $row['attachedTimestamp'] ?? ''; |
760 | |
761 | $html .= $this->getAttachedTimestampField( $attachedTimestamp ); |
762 | |
763 | if ( empty( $row['attachedMethod'] ) ) { |
764 | $attachedMethod = $this->msg( 'centralauth-admin-unattached' )->parse(); |
765 | } else { |
766 | $attachedMethod = $this->formatMergeMethod( $row['attachedMethod'] ); |
767 | } |
768 | $html .= Html::rawElement( 'td', [ 'class' => 'mw-centralauth-wikislist-method' ], $attachedMethod ); |
769 | |
770 | $html .= Html::rawElement( 'td', [], $this->formatBlockStatus( $row ) ) . |
771 | Html::rawElement( |
772 | 'td', [ 'class' => 'mw-centralauth-wikislist-editcount' ], $this->formatEditcount( $row ) |
773 | ) . |
774 | Html::rawElement( 'td', [], $this->formatGroups( $row ) ) . |
775 | Xml::closeElement( 'tr' ); |
776 | |
777 | return $html; |
778 | } |
779 | |
780 | /** |
781 | * @param string|null $attachedTimestamp |
782 | * |
783 | * @return string |
784 | */ |
785 | private function getAttachedTimestampField( $attachedTimestamp ) { |
786 | if ( !$attachedTimestamp ) { |
787 | $html = Xml::openElement( 'td', [ 'data-sort-value' => '0' ] ) . |
788 | $this->msg( 'centralauth-admin-unattached' )->parse(); |
789 | } else { |
790 | $html = Xml::openElement( 'td', |
791 | [ 'data-sort-value' => $attachedTimestamp ] ) . |
792 | // visible date and time in users preference |
793 | htmlspecialchars( $this->getLanguage()->timeanddate( $attachedTimestamp, true ) ); |
794 | } |
795 | |
796 | $html .= Xml::closeElement( 'td' ); |
797 | return $html; |
798 | } |
799 | |
800 | /** |
801 | * @param string $method |
802 | * @return string |
803 | * @see CentralAuthUser::attach() |
804 | */ |
805 | private function formatMergeMethod( $method ) { |
806 | // Give grep a chance to find the usages: |
807 | // centralauth-merge-method-primary, centralauth-merge-method-empty, |
808 | // centralauth-merge-method-mail, centralauth-merge-method-password, |
809 | // centralauth-merge-method-admin, centralauth-merge-method-new, |
810 | // centralauth-merge-method-login |
811 | $brief = $this->msg( "centralauth-merge-method-{$method}" )->text(); |
812 | $html = |
813 | Html::element( |
814 | 'img', [ |
815 | 'src' => $this->getConfig()->get( MainConfigNames::ExtensionAssetsPath ) |
816 | . "/CentralAuth/images/icons/merged-{$method}.png", |
817 | 'alt' => $brief, |
818 | 'title' => $brief, |
819 | ] |
820 | ) |
821 | . Html::element( |
822 | 'span', [ |
823 | 'class' => 'merge-method-help', |
824 | 'title' => $brief, |
825 | 'data-centralauth-mergemethod' => $method |
826 | ], |
827 | $this->msg( 'centralauth-merge-method-questionmark' )->text() |
828 | ); |
829 | |
830 | return $html; |
831 | } |
832 | |
833 | /** |
834 | * @param array $row |
835 | * @return string |
836 | */ |
837 | private function formatBlockStatus( $row ) { |
838 | $additionalHtml = ''; |
839 | if ( isset( $row['blocked'] ) && $row['blocked'] ) { |
840 | $optionMessage = $this->formatBlockParams( $row ); |
841 | if ( $row['block-expiry'] == 'infinity' ) { |
842 | $text = $this->msg( 'centralauth-admin-blocked2-indef' )->text(); |
843 | } else { |
844 | $expiry = $this->getLanguage()->timeanddate( $row['block-expiry'], true ); |
845 | $expiryd = $this->getLanguage()->date( $row['block-expiry'], true ); |
846 | $expiryt = $this->getLanguage()->time( $row['block-expiry'], true ); |
847 | |
848 | $text = $this->msg( 'centralauth-admin-blocked2', $expiry, $expiryd, $expiryt ) |
849 | ->text(); |
850 | } |
851 | |
852 | if ( $row['block-reason'] ) { |
853 | $reason = Sanitizer::escapeHtmlAllowEntities( $row['block-reason'] ); |
854 | $reason = $this->commentFormatter->formatLinks( |
855 | $reason, |
856 | null, |
857 | false, |
858 | $row['wiki'] |
859 | ); |
860 | |
861 | $msg = $this->msg( 'centralauth-admin-blocked-reason' ); |
862 | $msg->rawParams( '<span class="plainlinks">' . $reason . '</span>' ); |
863 | |
864 | $additionalHtml .= Html::rawElement( 'br' ) . $msg->parse(); |
865 | } |
866 | |
867 | $additionalHtml .= ' ' . $optionMessage; |
868 | |
869 | } else { |
870 | $text = $this->msg( 'centralauth-admin-notblocked' )->text(); |
871 | } |
872 | |
873 | return self::foreignLink( |
874 | $row['wiki'], |
875 | 'Special:Log/block', |
876 | $text, |
877 | $this->msg( 'centralauth-admin-blocklog' )->text(), |
878 | 'page=User:' . urlencode( $this->mUserName ) |
879 | ) . $additionalHtml; |
880 | } |
881 | |
882 | /** |
883 | * Format a block's parameters. |
884 | * |
885 | * @see BlockListPager::formatValue() |
886 | * |
887 | * @param array $row |
888 | * @return string |
889 | */ |
890 | private function formatBlockParams( $row ) { |
891 | global $wgConf; |
892 | |
893 | // Ensure all the data is loaded before trying to use. |
894 | $wgConf->loadFullData(); |
895 | |
896 | $properties = []; |
897 | |
898 | if ( $row['block-sitewide'] ) { |
899 | $properties[] = $this->msg( 'blocklist-editing-sitewide' )->escaped(); |
900 | } |
901 | |
902 | if ( !$row['block-sitewide'] && $row['block-restrictions'] ) { |
903 | $list = $this->getRestrictionListHTML( $row ); |
904 | if ( $list ) { |
905 | $properties[] = $this->msg( 'blocklist-editing' )->escaped() . $list; |
906 | } |
907 | } |
908 | |
909 | $options = [ |
910 | 'anononly' => 'anononlyblock', |
911 | 'nocreate' => 'createaccountblock', |
912 | 'noautoblock' => 'noautoblockblock', |
913 | 'noemail' => 'emailblock', |
914 | 'nousertalk' => 'blocklist-nousertalk', |
915 | ]; |
916 | foreach ( $options as $option => $msg ) { |
917 | if ( $row['block-' . $option] ) { |
918 | $properties[] = $this->msg( $msg )->escaped(); |
919 | } |
920 | } |
921 | |
922 | if ( !$properties ) { |
923 | return ''; |
924 | } |
925 | |
926 | return Html::rawElement( |
927 | 'ul', |
928 | [], |
929 | implode( '', array_map( static function ( $prop ) { |
930 | return Html::rawElement( |
931 | 'li', |
932 | [], |
933 | $prop |
934 | ); |
935 | }, $properties ) ) |
936 | ); |
937 | } |
938 | |
939 | /** |
940 | * @see BlockListPager::getRestrictionListHTML() |
941 | * |
942 | * @param array $row |
943 | * |
944 | * @return string |
945 | */ |
946 | private function getRestrictionListHTML( array $row ) { |
947 | $count = array_reduce( $row['block-restrictions'], static function ( $carry, $restriction ) { |
948 | $carry[$restriction->getType()] += 1; |
949 | return $carry; |
950 | }, [ |
951 | PageRestriction::TYPE => 0, |
952 | NamespaceRestriction::TYPE => 0, |
953 | ActionRestriction::TYPE => 0, |
954 | ] ); |
955 | |
956 | $restrictions = []; |
957 | foreach ( $count as $type => $value ) { |
958 | if ( $value === 0 ) { |
959 | continue; |
960 | } |
961 | |
962 | $restrictions[] = Html::rawElement( |
963 | 'li', |
964 | [], |
965 | self::foreignLink( |
966 | $row['wiki'], |
967 | 'Special:BlockList/' . $row['name'], |
968 | $this->msg( 'centralauth-block-editing-' . $type, $value )->text() |
969 | ) |
970 | ); |
971 | } |
972 | |
973 | if ( count( $restrictions ) === 0 ) { |
974 | return ''; |
975 | } |
976 | |
977 | return Html::rawElement( |
978 | 'ul', |
979 | [], |
980 | implode( '', $restrictions ) |
981 | ); |
982 | } |
983 | |
984 | /** |
985 | * @param array $row |
986 | * @return string |
987 | * @throws Exception |
988 | */ |
989 | private function formatEditcount( $row ) { |
990 | $wiki = WikiMap::getWiki( $row['wiki'] ); |
991 | if ( !$wiki ) { |
992 | throw new InvalidArgumentException( "Invalid wiki: {$row['wiki']}" ); |
993 | } |
994 | $wikiname = $wiki->getDisplayName(); |
995 | $editCount = $this->getLanguage()->formatNum( intval( $row['editCount'] ) ); |
996 | |
997 | return self::foreignLink( |
998 | $row['wiki'], |
999 | 'Special:Contributions/' . $this->mUserName, |
1000 | $editCount, |
1001 | $this->msg( 'centralauth-foreign-contributions' ) |
1002 | ->params( $editCount, $wikiname )->text() |
1003 | ); |
1004 | } |
1005 | |
1006 | /** |
1007 | * @param array $row |
1008 | * @return string |
1009 | */ |
1010 | private function formatGroups( $row ) { |
1011 | if ( !count( $row['groupMemberships'] ) ) { |
1012 | return ''; |
1013 | } |
1014 | |
1015 | // We place temporary groups before non-expiring groups in the list. |
1016 | // This is to avoid the ambiguity of something like |
1017 | // "sysop, bureaucrat (temporary)" -- users might wonder whether the |
1018 | // "temporary" indication applies to both groups, or just the last one |
1019 | $listTemporary = []; |
1020 | $list = []; |
1021 | /** @var UserGroupMembership $ugm */ |
1022 | foreach ( $row['groupMemberships'] as $group => $ugm ) { |
1023 | if ( $ugm->getExpiry() ) { |
1024 | $listTemporary[] = $this->msg( 'centralauth-admin-group-temporary', |
1025 | wfEscapeWikitext( $group ) )->parse(); |
1026 | } else { |
1027 | $list[] = htmlspecialchars( $group ); |
1028 | } |
1029 | } |
1030 | return $this->getLanguage()->commaList( array_merge( $listTemporary, $list ) ); |
1031 | } |
1032 | |
1033 | /** |
1034 | * @param string|WikiReference $wikiID |
1035 | * @param string $title |
1036 | * @param string $text not HTML escaped |
1037 | * @param string $hint |
1038 | * @param string $params |
1039 | * @return string |
1040 | * @throws Exception |
1041 | */ |
1042 | public static function foreignLink( $wikiID, $title, $text, $hint = '', $params = '' ) { |
1043 | if ( $wikiID instanceof WikiReference ) { |
1044 | $wiki = $wikiID; |
1045 | } else { |
1046 | $wiki = WikiMap::getWiki( $wikiID ); |
1047 | if ( !$wiki ) { |
1048 | throw new InvalidArgumentException( "Invalid wiki: $wikiID" ); |
1049 | } |
1050 | } |
1051 | |
1052 | $url = $wiki->getFullUrl( $title ); |
1053 | if ( $params ) { |
1054 | $url .= '?' . $params; |
1055 | } |
1056 | return Xml::element( 'a', |
1057 | [ |
1058 | 'href' => $url, |
1059 | 'title' => $hint, |
1060 | ], |
1061 | $text ); |
1062 | } |
1063 | |
1064 | /** |
1065 | * @param string $wikiID |
1066 | * @return string |
1067 | * @throws Exception |
1068 | */ |
1069 | private function foreignUserLink( $wikiID ) { |
1070 | $wiki = WikiMap::getWiki( $wikiID ); |
1071 | if ( !$wiki ) { |
1072 | throw new InvalidArgumentException( "Invalid wiki: $wikiID" ); |
1073 | } |
1074 | |
1075 | $wikiname = $wiki->getDisplayName(); |
1076 | return self::foreignLink( |
1077 | $wiki, |
1078 | $this->namespaceInfo->getCanonicalName( NS_USER ) . ':' . $this->mUserName, |
1079 | $wikiname, |
1080 | $this->msg( 'centralauth-foreign-link', $this->mUserName, $wikiname )->text() |
1081 | ); |
1082 | } |
1083 | |
1084 | /** |
1085 | * @param string $wikiID |
1086 | * @return string |
1087 | */ |
1088 | private function adminCheck( $wikiID ) { |
1089 | return Xml::check( 'wpWikis[]', false, [ 'value' => $wikiID ] ); |
1090 | } |
1091 | |
1092 | /** |
1093 | * Generates a form for managing deleting a global account which contains a description, a reason field |
1094 | * and destructive submit button. |
1095 | */ |
1096 | private function showDeleteGlobalAccountForm() { |
1097 | $formDescriptor = [ |
1098 | 'info' => [ |
1099 | 'type' => 'info', |
1100 | 'raw' => true, |
1101 | 'default' => $this->msg( 'centralauth-admin-delete-description' )->parseAsBlock(), |
1102 | 'cssclass' => 'mw-centralauth-admin-delete-intro', |
1103 | ], |
1104 | 'reason' => [ |
1105 | 'type' => 'text', |
1106 | 'label-message' => 'centralauth-admin-reason', |
1107 | 'id' => "delete-reason", |
1108 | 'name' => 'reason', |
1109 | ], |
1110 | 'Method' => [ |
1111 | 'type' => 'hidden', |
1112 | 'default' => 'delete', |
1113 | ], |
1114 | 'submit' => [ |
1115 | 'type' => 'submit', |
1116 | 'buttonlabel-message' => 'centralauth-admin-delete-button', |
1117 | 'flags' => [ 'progressive', 'destructive' ] |
1118 | ], |
1119 | ]; |
1120 | |
1121 | $context = new DerivativeContext( $this->getContext() ); |
1122 | $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $context ); |
1123 | $htmlForm |
1124 | ->setAction( $this->getPageTitle()->getFullURL( [ 'target' => $this->mUserName ] ) ) |
1125 | ->suppressDefaultSubmit() |
1126 | ->setWrapperLegendMsg( 'centralauth-admin-delete-title' ) |
1127 | ->setId( 'mw-centralauth-delete' ) |
1128 | ->prepareForm() |
1129 | ->displayForm( false ); |
1130 | } |
1131 | |
1132 | private function showStatusForm() { |
1133 | // Allows locking, hiding, locking and hiding. |
1134 | $formDescriptor = [ |
1135 | 'intro' => [ |
1136 | 'type' => 'info', |
1137 | 'raw' => true, |
1138 | 'default' => $this->msg( 'centralauth-admin-status-intro' )->parseAsBlock(), |
1139 | 'cssclass' => 'mw-centralauth-admin-status-intro', |
1140 | 'section' => 'intro', |
1141 | ], |
1142 | 'StatusLocked' => [ |
1143 | 'type' => 'radio', |
1144 | 'label-message' => 'centralauth-admin-status-locked', |
1145 | 'options-messages' => [ |
1146 | 'centralauth-admin-status-locked-no' => 0, |
1147 | 'centralauth-admin-status-locked-yes' => 1, |
1148 | ], |
1149 | 'default' => (int)$this->mGlobalUser->isLocked(), |
1150 | 'id' => 'mw-centralauth-admin-status-locked', |
1151 | 'section' => 'lockedhidden', |
1152 | ], |
1153 | 'StatusHidden' => [ |
1154 | 'type' => 'radio', |
1155 | 'label-message' => 'centralauth-admin-status-hidden', |
1156 | 'options-messages' => [ |
1157 | 'centralauth-admin-status-hidden-no' => CentralAuthUser::HIDDEN_LEVEL_NONE, |
1158 | ], |
1159 | 'default' => $this->mGlobalUser->getHiddenLevelInt(), |
1160 | 'id' => 'mw-centralauth-admin-status-hidden', |
1161 | 'section' => 'lockedhidden', |
1162 | ], |
1163 | 'Reason' => [ |
1164 | 'type' => 'selectandother', |
1165 | 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT, |
1166 | 'label-message' => 'centralauth-admin-reason', |
1167 | 'options-message' => 'centralauth-admin-status-reasons', |
1168 | 'other-message' => 'centralauth-admin-reason-other-select', |
1169 | 'id' => 'mw-centralauth-admin-reason', |
1170 | ], |
1171 | 'Method' => [ |
1172 | 'type' => 'hidden', |
1173 | 'default' => 'set-status', |
1174 | ], |
1175 | 'UserState' => [ |
1176 | 'type' => 'hidden', |
1177 | 'default' => $this->mGlobalUser->getStateHash( false ), |
1178 | ], |
1179 | 'submit' => [ |
1180 | 'type' => 'submit', |
1181 | 'buttonlabel-message' => 'centralauth-admin-status-submit', |
1182 | 'flags' => [ 'progressive' ], |
1183 | ], |
1184 | ]; |
1185 | |
1186 | if ( $this->mCanSuppress ) { |
1187 | $formDescriptor['StatusHidden']['options-messages'] += [ |
1188 | 'centralauth-admin-status-hidden-list' => CentralAuthUser::HIDDEN_LEVEL_LISTS, |
1189 | 'centralauth-admin-status-hidden-oversight' => CentralAuthUser::HIDDEN_LEVEL_SUPPRESSED, |
1190 | ]; |
1191 | } |
1192 | |
1193 | $context = new DerivativeContext( $this->getContext() ); |
1194 | $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $context ); |
1195 | $htmlForm |
1196 | ->setAction( $this->getPageTitle()->getFullURL( [ 'target' => $this->mUserName ] ) ) |
1197 | ->suppressDefaultSubmit() |
1198 | ->setId( 'mw-centralauth-admin-status' ) |
1199 | ->setWrapperLegendMsg( 'centralauth-admin-status' ) |
1200 | ->prepareForm() |
1201 | ->displayForm( false ); |
1202 | } |
1203 | |
1204 | /** |
1205 | * Gets a framed and padded fieldset that contains the given HTML. |
1206 | * |
1207 | * Used over HTMLForm as we need to avoid adding hidden fields like "wpEditToken" and form elements to parts of |
1208 | * the page that are not forms. |
1209 | * |
1210 | * @param string $html The HTML to be wrapped with the fieldset |
1211 | * @param string|MessageSpecifier $fieldsetLegendMsg The message to use as the fieldset legend |
1212 | * @param string|null $fieldsetId The ID for the fieldset, or null for no ID |
1213 | * @return string HTML |
1214 | */ |
1215 | private function getFramedFieldsetLayout( string $html, $fieldsetLegendMsg, ?string $fieldsetId = null ): string { |
1216 | $fieldset = new FieldsetLayout( [ |
1217 | 'label' => $this->msg( $fieldsetLegendMsg )->text(), |
1218 | 'items' => [ |
1219 | new Widget( [ |
1220 | 'content' => new HtmlSnippet( $html ), |
1221 | ] ), |
1222 | ], |
1223 | 'id' => $fieldsetId, |
1224 | ] ); |
1225 | return new PanelLayout( [ |
1226 | 'classes' => [ 'mw-htmlform-ooui-wrapper' ], |
1227 | 'expanded' => false, |
1228 | 'padded' => true, |
1229 | 'framed' => true, |
1230 | 'content' => $fieldset, |
1231 | ] ); |
1232 | } |
1233 | |
1234 | private function showLogExtract() { |
1235 | $user = $this->mGlobalUser->getName(); |
1236 | $title = Title::newFromText( $this->namespaceInfo->getCanonicalName( NS_USER ) . ":{$user}@global" ); |
1237 | if ( !$title ) { |
1238 | // Don't fatal even if a Title couldn't be generated |
1239 | // because we've invalid usernames too :/ |
1240 | return; |
1241 | } |
1242 | $logTypes = [ 'globalauth' ]; |
1243 | if ( $this->mCanSuppress ) { |
1244 | $logTypes[] = 'suppress'; |
1245 | } |
1246 | $html = ''; |
1247 | $numRows = LogEventsList::showLogExtract( |
1248 | $html, |
1249 | $logTypes, |
1250 | $title->getPrefixedText(), |
1251 | '', |
1252 | [ 'showIfEmpty' => true ] |
1253 | ); |
1254 | |
1255 | if ( $numRows ) { |
1256 | $this->getOutput()->addHTML( $this->getFramedFieldsetLayout( $html, 'centralauth-admin-logsnippet' ) ); |
1257 | |
1258 | return; |
1259 | } |
1260 | |
1261 | if ( $this->mGlobalUser->isLocked() ) { |
1262 | $logOtherWikiMsg = $this |
1263 | ->msg( 'centralauth-admin-log-otherwiki' ) |
1264 | ->params( $this->mGlobalUser->getName() ); |
1265 | |
1266 | if ( !$logOtherWikiMsg->isDisabled() ) { |
1267 | $this->getOutput()->addHTML( |
1268 | Html::warningBox( |
1269 | $logOtherWikiMsg->parse(), |
1270 | 'centralauth-admin-log-otherwiki' |
1271 | ) |
1272 | ); |
1273 | } |
1274 | } |
1275 | } |
1276 | |
1277 | /** |
1278 | * @return int |
1279 | */ |
1280 | private function evaluateTotalEditcount() { |
1281 | $total = 0; |
1282 | foreach ( $this->mAttachedLocalAccounts as $acc ) { |
1283 | $total += $acc['editCount']; |
1284 | } |
1285 | return $total; |
1286 | } |
1287 | |
1288 | /** |
1289 | * @return array[] |
1290 | */ |
1291 | private function getMergeMethodDescriptions() { |
1292 | // Give grep a chance to find the usages: |
1293 | // centralauth-merge-method-primary, centralauth-merge-method-new, |
1294 | // centralauth-merge-method-empty, centralauth-merge-method-password, |
1295 | // centralauth-merge-method-mail, centralauth-merge-method-admin, |
1296 | // centralauth-merge-method-login |
1297 | // Give grep a chance to find the usages: |
1298 | // centralauth-merge-method-primary-desc, centralauth-merge-method-new-desc, |
1299 | // centralauth-merge-method-empty-desc, centralauth-merge-method-password-desc, |
1300 | // centralauth-merge-method-mail-desc, centralauth-merge-method-admin-desc, |
1301 | // centralauth-merge-method-login-desc |
1302 | $mergeMethodDescriptions = []; |
1303 | foreach ( [ 'primary', 'new', 'empty', 'password', 'mail', 'admin', 'login' ] as $method ) { |
1304 | $mergeMethodDescriptions[$method] = [ |
1305 | 'short' => $this->getLanguage()->ucfirst( |
1306 | $this->msg( "centralauth-merge-method-{$method}" )->escaped() |
1307 | ), |
1308 | 'desc' => $this->msg( "centralauth-merge-method-{$method}-desc" )->escaped() |
1309 | ]; |
1310 | } |
1311 | return $mergeMethodDescriptions; |
1312 | } |
1313 | |
1314 | /** |
1315 | * Return an array of subpages beginning with $search that this special page will accept. |
1316 | * |
1317 | * @param string $search Prefix to search for |
1318 | * @param int $limit Maximum number of results to return (usually 10) |
1319 | * @param int $offset Number of results to skip (usually 0) |
1320 | * @return string[] Matching subpages |
1321 | */ |
1322 | public function prefixSearchSubpages( $search, $limit, $offset ) { |
1323 | $search = $this->userNameUtils->getCanonical( $search ); |
1324 | if ( !$search ) { |
1325 | // No prefix suggestion for invalid user |
1326 | return []; |
1327 | } |
1328 | |
1329 | $dbr = $this->databaseManager->getCentralReplicaDB(); |
1330 | |
1331 | // Autocomplete subpage as user list - non-hidden users to allow caching |
1332 | return $dbr->newSelectQueryBuilder() |
1333 | ->select( 'gu_name' ) |
1334 | ->from( 'globaluser' ) |
1335 | ->where( [ |
1336 | $dbr->expr( 'gu_name', IExpression::LIKE, new LikeValue( $search, $dbr->anyString() ) ), |
1337 | 'gu_hidden_level' => CentralAuthUser::HIDDEN_LEVEL_NONE, |
1338 | ] ) |
1339 | ->orderBy( 'gu_name' ) |
1340 | ->limit( $limit ) |
1341 | ->offset( $offset ) |
1342 | ->caller( __METHOD__ ) |
1343 | ->fetchFieldValues(); |
1344 | } |
1345 | |
1346 | /** @inheritDoc */ |
1347 | protected function getGroupName() { |
1348 | return 'users'; |
1349 | } |
1350 | } |