Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
5.29% |
19 / 359 |
|
3.23% |
1 / 31 |
CRAP | |
0.00% |
0 / 1 |
SpecialMergeAccount | |
5.29% |
19 / 359 |
|
3.23% |
1 / 31 |
5113.57 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
27.78% |
15 / 54 |
|
0.00% |
0 / 1 |
87.84 | |||
showFormForExistingUsers | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
initSession | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getWorkingPasswords | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
6 | |||
addWorkingPassword | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
clearWorkingPasswords | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
xorString | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
doDryRunMerge | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
56 | |||
doInitialMerge | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
20 | |||
doCleanupMerge | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
42 | |||
doAttachMerge | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
30 | |||
showWelcomeForm | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
showCleanupForm | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
showAttachForm | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
showStatus | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
20 | |||
listAttached | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
listUnattached | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
listWikis | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
formatList | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
listWikiItem | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
foreignUserLink | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
actionForm | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 | |||
passwordForm | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
2 | |||
step1PasswordForm | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
step2PasswordForm | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
step3ActionForm | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
6 | |||
attachActionForm | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
dryRunError | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\CentralAuth\Special; |
4 | |
5 | use ErrorPageError; |
6 | use Exception; |
7 | use InvalidArgumentException; |
8 | use MediaWiki\Extension\CentralAuth\CentralAuthDatabaseManager; |
9 | use MediaWiki\Extension\CentralAuth\Config\CAMainConfigNames; |
10 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
11 | use MediaWiki\Html\Html; |
12 | use MediaWiki\SpecialPage\SpecialPage; |
13 | use MediaWiki\Title\NamespaceInfo; |
14 | use MediaWiki\User\UserFactory; |
15 | use MediaWiki\WikiMap\WikiMap; |
16 | use MediaWiki\Xml\Xml; |
17 | use MWCryptRand; |
18 | use RuntimeException; |
19 | use Wikimedia\AtEase\AtEase; |
20 | |
21 | class SpecialMergeAccount extends SpecialPage { |
22 | |
23 | /** @var string */ |
24 | protected $mUserName; |
25 | /** @var bool */ |
26 | protected $mAttemptMerge; |
27 | /** @var string */ |
28 | protected $mMergeAction; |
29 | /** @var string */ |
30 | protected $mPassword; |
31 | /** @var string[] */ |
32 | protected $mWikiIDs; |
33 | /** @var string */ |
34 | protected $mSessionToken; |
35 | /** @var string */ |
36 | protected $mSessionKey; |
37 | |
38 | private NamespaceInfo $namespaceInfo; |
39 | private UserFactory $userFactory; |
40 | private CentralAuthDatabaseManager $databaseManager; |
41 | |
42 | public function __construct( |
43 | NamespaceInfo $namespaceInfo, |
44 | UserFactory $userFactory, |
45 | CentralAuthDatabaseManager $databaseManager |
46 | ) { |
47 | parent::__construct( 'MergeAccount', 'centralauth-merge' ); |
48 | $this->namespaceInfo = $namespaceInfo; |
49 | $this->userFactory = $userFactory; |
50 | $this->databaseManager = $databaseManager; |
51 | } |
52 | |
53 | public function doesWrites() { |
54 | return true; |
55 | } |
56 | |
57 | /** @inheritDoc */ |
58 | public function execute( $subpage ) { |
59 | $this->setHeaders(); |
60 | $this->addHelpLink( 'Extension:CentralAuth' ); |
61 | |
62 | if ( $subpage !== null && preg_match( "/^[0-9a-zA-Z]{32}$/", $subpage ) ) { |
63 | $user = $this->userFactory->newFromConfirmationCode( $subpage ); |
64 | |
65 | if ( is_object( $user ) ) { |
66 | $user->confirmEmail(); |
67 | $user->saveSettings(); |
68 | $this->getOutput()->addWikiMsg( 'confirmemail_success' ); |
69 | } else { |
70 | $this->getOutput()->addWikiMsg( 'confirmemail_invalid' ); |
71 | // return; // Let's be greedy and still show them MergeAccount |
72 | } |
73 | } |
74 | |
75 | if ( !$this->userCanExecute( $this->getUser() ) ) { |
76 | $this->getOutput()->addWikiMsg( 'centralauth-merge-denied' ); |
77 | $this->getOutput()->addWikiMsg( 'centralauth-readmore-text' ); |
78 | return; |
79 | } |
80 | |
81 | if ( !$this->getUser()->isNamed() ) { |
82 | $loginpage = SpecialPage::getTitleFor( 'Userlogin' ); |
83 | $loginurl = $loginpage->getFullUrl( |
84 | [ 'returnto' => $this->getPageTitle()->getPrefixedText() ] |
85 | ); |
86 | $this->getOutput()->addWikiMsg( 'centralauth-merge-notlogged', $loginurl ); |
87 | $this->getOutput()->addWikiMsg( 'centralauth-readmore-text' ); |
88 | |
89 | return; |
90 | } |
91 | |
92 | $this->databaseManager->assertNotReadOnly(); |
93 | $request = $this->getRequest(); |
94 | |
95 | $this->mUserName = $this->getUser()->getName(); |
96 | |
97 | $this->mAttemptMerge = $request->wasPosted(); |
98 | |
99 | $this->mMergeAction = $request->getVal( 'wpMergeAction' ); |
100 | $this->mPassword = $request->getVal( 'wpPassword' ); |
101 | $this->mWikiIDs = $request->getArray( 'wpWikis' ); |
102 | $this->mSessionToken = $request->getVal( 'wpMergeSessionToken' ); |
103 | $this->mSessionKey = pack( "H*", $request->getVal( 'wpMergeSessionKey' ) ); |
104 | |
105 | // Possible demo states |
106 | |
107 | // success, all accounts merged |
108 | // successful login, some accounts merged, others left |
109 | // successful login, others left |
110 | // not account owner, others left |
111 | |
112 | // is owner / is not owner |
113 | // did / did not merge some accounts |
114 | // do / don't have more accounts to merge |
115 | |
116 | if ( $this->mAttemptMerge ) { |
117 | // First check the edit token |
118 | if ( !$this->getUser()->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) { |
119 | throw new ErrorPageError( 'sessionfailure-title', 'sessionfailure' ); |
120 | } |
121 | switch ( $this->mMergeAction ) { |
122 | case "dryrun": |
123 | $this->doDryRunMerge(); |
124 | break; |
125 | case "initial": |
126 | $this->doInitialMerge(); |
127 | break; |
128 | case "cleanup": |
129 | $this->doCleanupMerge(); |
130 | break; |
131 | case "attach": |
132 | $this->doAttachMerge(); |
133 | break; |
134 | default: |
135 | throw new InvalidArgumentException( |
136 | 'Invalid merge action ' . $this->mMergeAction . ' given' |
137 | ); |
138 | } |
139 | return; |
140 | } |
141 | |
142 | $globalUser = CentralAuthUser::getInstanceByName( $this->mUserName ); |
143 | if ( $globalUser->exists() ) { |
144 | $this->showFormForExistingUsers( $globalUser ); |
145 | } else { |
146 | $this->showWelcomeForm(); |
147 | } |
148 | } |
149 | |
150 | /** |
151 | * Pick which form to show for a user that already exists |
152 | * |
153 | * @param CentralAuthUser $globalUser |
154 | */ |
155 | private function showFormForExistingUsers( CentralAuthUser $globalUser ) { |
156 | if ( $globalUser->isAttached() ) { |
157 | $this->showCleanupForm(); |
158 | } else { |
159 | $this->showAttachForm(); |
160 | } |
161 | } |
162 | |
163 | /** |
164 | * To pass potentially multiple passwords from one form submission |
165 | * to another while previewing the merge behavior, we can store them |
166 | * in the server-side session information. |
167 | * |
168 | * We'd rather not have plaintext passwords floating about on disk |
169 | * or memcached, so the session store is obfuscated with simple XOR |
170 | * encryption. The key is passed in the form instead of the session |
171 | * data, so they won't be found floating in the same place. |
172 | */ |
173 | private function initSession() { |
174 | $this->mSessionToken = MWCryptRand::generateHex( 32 ); |
175 | $this->mSessionKey = random_bytes( 128 ); |
176 | } |
177 | |
178 | /** |
179 | * @return string[] |
180 | */ |
181 | private function getWorkingPasswords() { |
182 | AtEase::suppressWarnings(); |
183 | $data = $this->getRequest()->getSessionData( 'wsCentralAuthMigration' ); |
184 | $passwords = unserialize( |
185 | gzinflate( |
186 | $this->xorString( |
187 | $data[$this->mSessionToken], |
188 | $this->mSessionKey |
189 | ) |
190 | ) |
191 | ); |
192 | AtEase::restoreWarnings(); |
193 | if ( is_array( $passwords ) ) { |
194 | return $passwords; |
195 | } |
196 | return []; |
197 | } |
198 | |
199 | /** |
200 | * @param string $password |
201 | */ |
202 | private function addWorkingPassword( $password ) { |
203 | $passwords = $this->getWorkingPasswords(); |
204 | if ( !in_array( $password, $passwords ) ) { |
205 | $passwords[] = $password; |
206 | } |
207 | |
208 | // Lightly obfuscate the passwords while we're storing them, |
209 | // just to make us feel better about them floating around. |
210 | $request = $this->getRequest(); |
211 | $data = $request->getSessionData( 'wsCentralAuthMigration' ); |
212 | $data[$this->mSessionToken] = |
213 | $this->xorString( |
214 | gzdeflate( |
215 | serialize( |
216 | $passwords ) ), |
217 | $this->mSessionKey ); |
218 | $request->setSessionData( 'wsCentralAuthMigration', $data ); |
219 | } |
220 | |
221 | private function clearWorkingPasswords() { |
222 | $request = $this->getRequest(); |
223 | $data = $request->getSessionData( 'wsCentralAuthMigration' ); |
224 | unset( $data[$this->mSessionToken] ); |
225 | $request->setSessionData( 'wsCentralAuthMigration', $data ); |
226 | } |
227 | |
228 | /** |
229 | * @param string $text |
230 | * @param string $key |
231 | * @return string |
232 | */ |
233 | private function xorString( $text, $key ) { |
234 | if ( $key !== '' ) { |
235 | $textLen = strlen( $text ); |
236 | $keyLen = strlen( $key ); |
237 | for ( $i = 0; $i < $textLen; $i++ ) { |
238 | $text[$i] = chr( 0xff & ( ord( $text[$i] ) ^ ord( $key[$i % $keyLen] ) ) ); |
239 | } |
240 | } |
241 | return $text; |
242 | } |
243 | |
244 | private function doDryRunMerge() { |
245 | $globalUser = CentralAuthUser::getPrimaryInstance( $this->getUser() ); |
246 | |
247 | if ( $globalUser->exists() ) { |
248 | // Already exists - race condition |
249 | $this->showFormForExistingUsers( $globalUser ); |
250 | return; |
251 | } |
252 | |
253 | if ( $this->getConfig()->get( CAMainConfigNames::CentralAuthDryRun ) ) { |
254 | $this->getOutput()->addHTML( |
255 | Html::successBox( |
256 | $this->msg( 'centralauth-notice-dryrun' )->parseAsBlock() |
257 | ) |
258 | ); |
259 | } |
260 | |
261 | $password = $this->getRequest()->getVal( 'wpPassword' ); |
262 | $this->addWorkingPassword( $password ); |
263 | $passwords = $this->getWorkingPasswords(); |
264 | |
265 | $home = false; |
266 | $attached = []; |
267 | $unattached = []; |
268 | $methods = []; |
269 | $status = $globalUser->migrationDryRun( |
270 | $passwords, $home, $attached, $unattached, $methods |
271 | ); |
272 | |
273 | if ( $status->isGood() ) { |
274 | // This is the global account or matched it |
275 | if ( count( $unattached ) == 0 ) { |
276 | // Everything matched -- very convenient! |
277 | $this->getOutput()->addWikiMsg( 'centralauth-merge-dryrun-complete' ); |
278 | } else { |
279 | $this->getOutput()->addWikiMsg( 'centralauth-merge-dryrun-incomplete' ); |
280 | } |
281 | |
282 | if ( count( $unattached ) > 0 ) { |
283 | $this->getOutput()->addHTML( $this->step2PasswordForm( $unattached ) ); |
284 | $this->getOutput()->addWikiMsg( 'centralauth-merge-dryrun-or' ); |
285 | } |
286 | |
287 | $subAttached = array_diff( $attached, [ $home ] ); |
288 | $this->getOutput()->addHTML( $this->step3ActionForm( $home, $subAttached, $methods ) ); |
289 | } else { |
290 | // Show error message from status |
291 | $out = $this->getOutput(); |
292 | $out->addHTML( |
293 | Html::errorBox( |
294 | $out->parseAsInterface( $status->getWikiText() ) |
295 | ) |
296 | ); |
297 | |
298 | // Show wiki list if required |
299 | if ( $status->hasMessage( 'centralauth-merge-home-password' ) ) { |
300 | $out = Html::rawElement( 'h2', [], |
301 | $this->msg( 'centralauth-list-home-title' )->escaped() ); |
302 | $out .= $this->msg( 'centralauth-list-home-dryrun' )->parseAsBlock(); |
303 | $out .= $this->listAttached( [ $home ], [ $home => 'primary' ] ); |
304 | $this->getOutput()->addHTML( $out ); |
305 | } |
306 | |
307 | // Show password box |
308 | $this->getOutput()->addHTML( $this->step1PasswordForm() ); |
309 | } |
310 | } |
311 | |
312 | private function doInitialMerge() { |
313 | $globalUser = CentralAuthUser::getPrimaryInstance( $this->getUser() ); |
314 | |
315 | if ( $this->getConfig()->get( CAMainConfigNames::CentralAuthDryRun ) ) { |
316 | $this->dryRunError(); |
317 | return; |
318 | } |
319 | |
320 | if ( $globalUser->exists() ) { |
321 | // Already exists - race condition |
322 | $this->showFormForExistingUsers( $globalUser ); |
323 | return; |
324 | } |
325 | |
326 | $passwords = $this->getWorkingPasswords(); |
327 | if ( !$passwords ) { |
328 | throw new RuntimeException( "Submission error -- invalid input" ); |
329 | } |
330 | |
331 | $globalUser->storeAndMigrate( |
332 | $passwords, |
333 | true, |
334 | false, |
335 | true |
336 | ); |
337 | $this->clearWorkingPasswords(); |
338 | |
339 | $this->showCleanupForm(); |
340 | } |
341 | |
342 | private function doCleanupMerge() { |
343 | $globalUser = CentralAuthUser::getPrimaryInstance( $this->getUser() ); |
344 | |
345 | if ( !$globalUser->exists() ) { |
346 | throw new RuntimeException( "User doesn't exist -- race condition?" ); |
347 | } |
348 | |
349 | if ( !$globalUser->isAttached() ) { |
350 | throw new RuntimeException( "Can't cleanup merge if not already attached." ); |
351 | } |
352 | |
353 | if ( $this->getConfig()->get( CAMainConfigNames::CentralAuthDryRun ) ) { |
354 | $this->dryRunError(); |
355 | return; |
356 | } |
357 | $password = $this->getRequest()->getText( 'wpPassword' ); |
358 | |
359 | $attached = []; |
360 | $unattached = []; |
361 | $ok = $globalUser->attemptPasswordMigration( $password, $attached, $unattached ); |
362 | $this->clearWorkingPasswords(); |
363 | |
364 | if ( !$ok ) { |
365 | if ( !$attached ) { |
366 | $this->getOutput()->addWikiMsg( 'centralauth-finish-noconfirms' ); |
367 | } else { |
368 | $this->getOutput()->addWikiMsg( 'centralauth-finish-incomplete' ); |
369 | } |
370 | } |
371 | $this->showCleanupForm(); |
372 | } |
373 | |
374 | private function doAttachMerge() { |
375 | $globalUser = CentralAuthUser::getPrimaryInstance( $this->getUser() ); |
376 | |
377 | if ( !$globalUser->exists() ) { |
378 | throw new RuntimeException( "User doesn't exist -- race condition?" ); |
379 | } |
380 | |
381 | if ( $globalUser->isAttached() ) { |
382 | // Already attached - race condition |
383 | $this->showCleanupForm(); |
384 | return; |
385 | } |
386 | |
387 | if ( $this->getConfig()->get( CAMainConfigNames::CentralAuthDryRun ) ) { |
388 | $this->dryRunError(); |
389 | return; |
390 | } |
391 | $password = $this->getRequest()->getText( 'wpPassword' ); |
392 | if ( $globalUser->authenticate( $password ) == [ CentralAuthUser::AUTHENTICATE_OK ] ) { |
393 | $globalUser->attach( WikiMap::getCurrentWikiId(), 'password' ); |
394 | $this->getOutput()->addWikiMsg( 'centralauth-attach-success' ); |
395 | $this->showCleanupForm(); |
396 | } else { |
397 | $this->getOutput()->addHTML( |
398 | Html::errorBox( |
399 | $this->msg( 'wrongpassword' )->escaped() |
400 | ) . $this->attachActionForm() |
401 | ); |
402 | } |
403 | } |
404 | |
405 | private function showWelcomeForm() { |
406 | if ( $this->getConfig()->get( CAMainConfigNames::CentralAuthDryRun ) ) { |
407 | $this->getOutput()->addHTML( |
408 | Html::successBox( |
409 | $this->msg( 'centralauth-notice-dryrun' )->parseAsBlock() |
410 | ) |
411 | ); |
412 | } |
413 | |
414 | $this->getOutput()->addWikiMsg( 'centralauth-merge-welcome' ); |
415 | $this->getOutput()->addWikiMsg( 'centralauth-readmore-text' ); |
416 | |
417 | $this->initSession(); |
418 | $this->getOutput()->addHTML( |
419 | $this->passwordForm( |
420 | 'dryrun', |
421 | $this->msg( 'centralauth-merge-step1-title' )->text(), |
422 | $this->msg( 'centralauth-merge-step1-detail' )->escaped(), |
423 | $this->msg( 'centralauth-merge-step1-submit' )->text() ) |
424 | ); |
425 | } |
426 | |
427 | private function showCleanupForm() { |
428 | $globalUser = CentralAuthUser::getInstance( $this->getUser() ); |
429 | |
430 | $merged = $globalUser->listAttached(); |
431 | $remainder = $globalUser->listUnattached(); |
432 | $this->showStatus( $merged, $remainder ); |
433 | } |
434 | |
435 | private function showAttachForm() { |
436 | $globalUser = CentralAuthUser::getInstance( $this->getUser() ); |
437 | $merged = $globalUser->listAttached(); |
438 | $this->getOutput()->addWikiMsg( 'centralauth-attach-list-attached', $this->mUserName ); |
439 | $this->getOutput()->addHTML( $this->listAttached( $merged ) ); |
440 | $this->getOutput()->addHTML( $this->attachActionForm() ); |
441 | } |
442 | |
443 | /** |
444 | * @param string[] $merged |
445 | * @param string[] $remainder |
446 | */ |
447 | private function showStatus( $merged, $remainder ) { |
448 | $remainderCount = count( $remainder ); |
449 | if ( $remainderCount > 0 ) { |
450 | $this->getOutput()->setPageTitleMsg( $this->msg( 'centralauth-incomplete' ) ); |
451 | $this->getOutput()->addWikiMsg( 'centralauth-incomplete-text' ); |
452 | } else { |
453 | $this->getOutput()->setPageTitleMsg( $this->msg( 'centralauth-complete' ) ); |
454 | $this->getOutput()->addWikiMsg( 'centralauth-complete-text' ); |
455 | } |
456 | $this->getOutput()->addWikiMsg( 'centralauth-readmore-text' ); |
457 | |
458 | if ( $merged ) { |
459 | $this->getOutput()->addHTML( Xml::element( 'hr' ) ); |
460 | $this->getOutput()->addWikiMsg( |
461 | 'centralauth-list-attached', |
462 | $this->mUserName, |
463 | count( $merged ) |
464 | ); |
465 | $this->getOutput()->addHTML( $this->listAttached( $merged ) ); |
466 | } |
467 | |
468 | if ( $remainder ) { |
469 | $this->getOutput()->addHTML( Xml::element( 'hr' ) ); |
470 | $this->getOutput()->addWikiMsg( |
471 | 'centralauth-list-unattached', |
472 | $this->mUserName, |
473 | $remainderCount |
474 | ); |
475 | $this->getOutput()->addHTML( $this->listUnattached( $remainder ) ); |
476 | |
477 | // Try the password form! |
478 | $this->getOutput()->addHTML( $this->passwordForm( |
479 | 'cleanup', |
480 | $this->msg( 'centralauth-finish-title' )->text(), |
481 | $this->msg( 'centralauth-finish-text' )->parseAsBlock(), |
482 | $this->msg( 'centralauth-finish-login' )->text() ) ); |
483 | } |
484 | } |
485 | |
486 | /** |
487 | * @param string[] $wikiList |
488 | * @param string[] $methods |
489 | * @return string |
490 | */ |
491 | private function listAttached( $wikiList, $methods = [] ) { |
492 | return $this->listWikis( $wikiList, $methods ); |
493 | } |
494 | |
495 | /** |
496 | * @param string[] $wikiList |
497 | * @return string |
498 | */ |
499 | private function listUnattached( $wikiList ) { |
500 | return $this->listWikis( $wikiList ); |
501 | } |
502 | |
503 | /** |
504 | * @param string[] $list |
505 | * @param string[] $methods |
506 | * @return string |
507 | */ |
508 | private function listWikis( $list, $methods = [] ) { |
509 | asort( $list ); |
510 | return $this->formatList( $list, $methods, [ $this, 'listWikiItem' ] ); |
511 | } |
512 | |
513 | /** |
514 | * @param string[] $items |
515 | * @param string[] $methods |
516 | * @param callable $callback |
517 | * @return string |
518 | */ |
519 | private function formatList( $items, $methods, $callback ) { |
520 | if ( !$items ) { |
521 | return ''; |
522 | } |
523 | |
524 | $itemMethods = []; |
525 | foreach ( $items as $item ) { |
526 | $itemMethods[] = $methods[$item] ?? ''; |
527 | } |
528 | |
529 | $html = Xml::openElement( 'ul' ) . "\n"; |
530 | $list = array_map( $callback, $items, $itemMethods ); |
531 | foreach ( $list as $item ) { |
532 | $html .= Html::rawElement( 'li', [], $item ) . "\n"; |
533 | } |
534 | $html .= Xml::closeElement( 'ul' ) . "\n"; |
535 | |
536 | return $html; |
537 | } |
538 | |
539 | /** |
540 | * @param string $wikiID |
541 | * @param string $method |
542 | * @return string |
543 | */ |
544 | private function listWikiItem( $wikiID, $method ) { |
545 | $return = $this->foreignUserLink( $wikiID ); |
546 | if ( $method ) { |
547 | // Give grep a chance to find the usages: |
548 | // centralauth-merge-method-primary, centralauth-merge-method-empty, |
549 | // centralauth-merge-method-mail, centralauth-merge-method-password, |
550 | // centralauth-merge-method-admin, centralauth-merge-method-new, |
551 | // centralauth-merge-method-login, |
552 | $return .= $this->msg( 'word-separator' )->escaped(); |
553 | $return .= $this->msg( 'parentheses', |
554 | $this->msg( 'centralauth-merge-method-' . $method )->text() |
555 | )->escaped(); |
556 | } |
557 | return $return; |
558 | } |
559 | |
560 | /** |
561 | * @param string $wikiID |
562 | * @return string |
563 | * @throws Exception |
564 | */ |
565 | private function foreignUserLink( $wikiID ) { |
566 | $wiki = WikiMap::getWiki( $wikiID ); |
567 | if ( !$wiki ) { |
568 | throw new InvalidArgumentException( "Invalid wiki: $wikiID" ); |
569 | } |
570 | |
571 | $wikiname = $wiki->getDisplayName(); |
572 | return SpecialCentralAuth::foreignLink( |
573 | $wiki, |
574 | $this->namespaceInfo->getCanonicalName( NS_USER ) . ':' . $this->mUserName, |
575 | $wikiname, |
576 | $this->msg( 'centralauth-foreign-link', $this->mUserName, $wikiname )->text() |
577 | ); |
578 | } |
579 | |
580 | /** |
581 | * @param string $action Value for wpMergeAction hidden form field |
582 | * @param string $title Header for form (Plain text. Will be escaped by this method) |
583 | * @param string $text Raw html contents of form |
584 | * @return string HTML of form |
585 | */ |
586 | private function actionForm( $action, $title, $text ) { |
587 | return Xml::openElement( 'div', [ 'id' => "userloginForm" ] ) . |
588 | Xml::openElement( 'form', |
589 | [ |
590 | 'method' => 'post', |
591 | 'action' => $this->getPageTitle()->getLocalUrl( 'action=submit' ) ] ) . |
592 | Xml::element( 'h2', [], $title ) . |
593 | Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ) . |
594 | Html::hidden( 'wpMergeAction', $action ) . |
595 | Html::hidden( 'wpMergeSessionToken', $this->mSessionToken ) . |
596 | Html::hidden( 'wpMergeSessionKey', bin2hex( $this->mSessionKey ) ) . |
597 | |
598 | $text . |
599 | |
600 | Xml::closeElement( 'form' ) . |
601 | Xml::element( 'br', [ 'clear' => 'all' ] ) . |
602 | Xml::closeElement( 'div' ); |
603 | } |
604 | |
605 | /** |
606 | * @param string $action wpMergeAction form value |
607 | * @param string $title Header of form (Not html escaped) |
608 | * @param string $text Raw html contents of form |
609 | * @param string $submit Text of submit button (Not html escaped) |
610 | * @return string HTML of form. |
611 | */ |
612 | private function passwordForm( $action, $title, $text, $submit ) { |
613 | $table = Html::rawElement( 'table', [], |
614 | Html::rawElement( 'tr', [], |
615 | Html::rawElement( 'td', [], |
616 | Xml::label( |
617 | $this->msg( 'centralauth-finish-password' )->text(), |
618 | 'wpPassword1' |
619 | ) |
620 | ) . |
621 | Html::rawElement( 'td', [], |
622 | Xml::input( |
623 | 'wpPassword', 20, '', |
624 | [ 'type' => 'password', 'id' => 'wpPassword1' ] ) |
625 | ) |
626 | ) . |
627 | Html::rawElement( 'tr', [], |
628 | Html::rawElement( 'td' ) . |
629 | Html::rawElement( 'td', [], |
630 | Xml::submitButton( $submit, [ 'name' => 'wpLogin' ] ) |
631 | ) |
632 | ) |
633 | ); |
634 | return $this->actionForm( $action, $title, $text . $table ); |
635 | } |
636 | |
637 | /** |
638 | * @return string |
639 | */ |
640 | private function step1PasswordForm() { |
641 | return $this->passwordForm( |
642 | 'dryrun', |
643 | $this->msg( 'centralauth-merge-step1-title' )->text(), |
644 | $this->msg( 'centralauth-merge-step1-detail' )->escaped(), |
645 | $this->msg( 'centralauth-merge-step1-submit' )->text() ); |
646 | } |
647 | |
648 | /** |
649 | * @param string[] $unattached |
650 | * @return string |
651 | */ |
652 | private function step2PasswordForm( $unattached ) { |
653 | return $this->passwordForm( |
654 | 'dryrun', |
655 | $this->msg( 'centralauth-merge-step2-title' )->text(), |
656 | $this->msg( 'centralauth-merge-step2-detail', |
657 | $this->getUser()->getName() )->parseAsBlock() . |
658 | $this->listUnattached( $unattached ), |
659 | $this->msg( 'centralauth-merge-step2-submit' )->text() ); |
660 | } |
661 | |
662 | /** |
663 | * @param string $home |
664 | * @param string[] $attached |
665 | * @param string[] $methods |
666 | * @return string |
667 | */ |
668 | private function step3ActionForm( $home, $attached, $methods ) { |
669 | $html = $this->msg( 'centralauth-merge-step3-detail', |
670 | $this->getUser()->getName() )->parseAsBlock() . |
671 | Html::rawElement( 'h3', [], |
672 | $this->msg( 'centralauth-list-home-title' )->escaped() |
673 | ) . $this->msg( 'centralauth-list-home-dryrun' )->parseAsBlock() . |
674 | $this->listAttached( [ $home ], $methods ); |
675 | |
676 | if ( count( $attached ) ) { |
677 | $html .= Html::rawElement( 'h3', [], |
678 | $this->msg( 'centralauth-list-attached-title' )->escaped() |
679 | ) . $this->msg( 'centralauth-list-attached-dryrun', |
680 | $this->getUser()->getName(), |
681 | count( $attached ) )->parseAsBlock(); |
682 | } |
683 | |
684 | $html .= $this->listAttached( $attached, $methods ) . |
685 | Html::rawElement( 'p', [], |
686 | Xml::submitButton( $this->msg( 'centralauth-merge-step3-submit' )->text(), |
687 | [ 'name' => 'wpLogin' ] ) |
688 | ); |
689 | |
690 | return $this->actionForm( |
691 | 'initial', |
692 | $this->msg( 'centralauth-merge-step3-title' )->text(), |
693 | $html |
694 | ); |
695 | } |
696 | |
697 | /** |
698 | * @return string |
699 | */ |
700 | private function attachActionForm() { |
701 | return $this->passwordForm( |
702 | 'attach', |
703 | $this->msg( 'centralauth-attach-title' )->text(), |
704 | $this->msg( 'centralauth-attach-text' )->escaped(), |
705 | $this->msg( 'centralauth-attach-submit' )->text() ); |
706 | } |
707 | |
708 | private function dryRunError() { |
709 | $this->getOutput()->addWikiMsg( 'centralauth-disabled-dryrun' ); |
710 | } |
711 | |
712 | /** @inheritDoc */ |
713 | protected function getGroupName() { |
714 | return 'login'; |
715 | } |
716 | } |