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