Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 727 |
|
0.00% |
0 / 26 |
CRAP | |
0.00% |
0 / 1 |
LoginSignupSpecialPage | |
0.00% |
0 / 726 |
|
0.00% |
0 / 26 |
44732 | |
0.00% |
0 / 1 |
isSignup | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
successfulAction | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
logAuthResult | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
setRequest | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
loadRequestParameters | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
42 | |||
load | |
0.00% |
0 / 41 |
|
0.00% |
0 / 1 |
240 | |||
getPreservedParams | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
42 | |||
beforeExecute | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 98 |
|
0.00% |
0 / 1 |
1640 | |||
canBypassForm | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
72 | |||
showSuccessPage | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
showReturnToPage | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setSessionUserForCurrentRequest | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
mainLoginForm | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
156 | |||
getPageHtml | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
132 | |||
getBenefitsContainerHtml | |
0.00% |
0 / 62 |
|
0.00% |
0 / 1 |
42 | |||
getAuthForm | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
110 | |||
onAuthChangeFormFields | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
showExtraInformation | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getFieldDefinitions | |
0.00% |
0 / 277 |
|
0.00% |
0 / 1 |
2862 | |||
hasSessionCookie | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getReturnToQueryStringFragment | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
showCreateAccountLink | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getTokenName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
makeLanguageSelector | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
makeLanguageSelectorLink | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
20 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
postProcessFormDescriptor | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
156 | |||
getNoticeHtml | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Holds shared logic for login and account creation pages. |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | * @ingroup SpecialPage |
22 | */ |
23 | |
24 | namespace MediaWiki\SpecialPage; |
25 | |
26 | use ErrorPageError; |
27 | use Exception; |
28 | use FatalError; |
29 | use LogicException; |
30 | use LoginHelper; |
31 | use MediaWiki\Auth\AuthenticationRequest; |
32 | use MediaWiki\Auth\AuthenticationResponse; |
33 | use MediaWiki\Auth\AuthManager; |
34 | use MediaWiki\Auth\PasswordAuthenticationRequest; |
35 | use MediaWiki\Context\DerivativeContext; |
36 | use MediaWiki\Context\RequestContext; |
37 | use MediaWiki\Html\Html; |
38 | use MediaWiki\HTMLForm\HTMLForm; |
39 | use MediaWiki\Language\RawMessage; |
40 | use MediaWiki\Logger\LoggerFactory; |
41 | use MediaWiki\MainConfigNames; |
42 | use MediaWiki\MediaWikiServices; |
43 | use MediaWiki\Message\Message; |
44 | use MediaWiki\Parser\Sanitizer; |
45 | use MediaWiki\Session\SessionManager; |
46 | use MediaWiki\Status\Status; |
47 | use MediaWiki\StubObject\StubGlobalUser; |
48 | use MediaWiki\Title\Title; |
49 | use MediaWiki\User\User; |
50 | use PermissionsError; |
51 | use ReadOnlyError; |
52 | use Skin; |
53 | use StatusValue; |
54 | use Wikimedia\ScopedCallback; |
55 | |
56 | /** |
57 | * Holds shared logic for login and account creation pages. |
58 | * |
59 | * @ingroup SpecialPage |
60 | */ |
61 | abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage { |
62 | protected $mReturnTo; |
63 | protected $mPosted; |
64 | protected $mAction; |
65 | protected $mLanguage; |
66 | protected $mVariant; |
67 | protected $mReturnToQuery; |
68 | protected $mToken; |
69 | protected $mStickHTTPS; |
70 | protected $mFromHTTP; |
71 | protected $mEntryError = ''; |
72 | protected $mEntryErrorType = 'error'; |
73 | |
74 | protected $mLoaded = false; |
75 | protected $mLoadedRequest = false; |
76 | protected $mSecureLoginUrl; |
77 | private $reasonValidatorResult = null; |
78 | |
79 | /** @var string */ |
80 | protected $securityLevel; |
81 | |
82 | /** @var bool True if the user if creating an account for someone else. Flag used for internal |
83 | * communication, only set at the very end. |
84 | */ |
85 | protected $proxyAccountCreation; |
86 | /** @var User FIXME another flag for passing data. */ |
87 | protected $targetUser; |
88 | |
89 | /** @var HTMLForm|null */ |
90 | protected $authForm; |
91 | |
92 | abstract protected function isSignup(); |
93 | |
94 | /** |
95 | * @param bool $direct True if the action was successful just now; false if that happened |
96 | * pre-redirection (so this handler was called already) |
97 | * @param StatusValue|null $extraMessages |
98 | * @return void |
99 | */ |
100 | abstract protected function successfulAction( $direct = false, $extraMessages = null ); |
101 | |
102 | /** |
103 | * Logs to the authmanager-stats channel. |
104 | * @param bool $success |
105 | * @param string|null $status Error message key |
106 | */ |
107 | abstract protected function logAuthResult( $success, $status = null ); |
108 | |
109 | protected function setRequest( array $data, $wasPosted = null ) { |
110 | parent::setRequest( $data, $wasPosted ); |
111 | $this->mLoadedRequest = false; |
112 | } |
113 | |
114 | /** |
115 | * Load basic request parameters for this Special page. |
116 | */ |
117 | private function loadRequestParameters() { |
118 | if ( $this->mLoadedRequest ) { |
119 | return; |
120 | } |
121 | $this->mLoadedRequest = true; |
122 | $request = $this->getRequest(); |
123 | |
124 | $this->mPosted = $request->wasPosted(); |
125 | $this->mAction = $request->getRawVal( 'action' ); |
126 | $this->mFromHTTP = $request->getBool( 'fromhttp', false ) |
127 | || $request->getBool( 'wpFromhttp', false ); |
128 | $this->mStickHTTPS = $this->getConfig()->get( MainConfigNames::ForceHTTPS ) |
129 | || ( !$this->mFromHTTP && $request->getProtocol() === 'https' ) |
130 | || $request->getBool( 'wpForceHttps', false ); |
131 | $this->mLanguage = $request->getText( 'uselang' ); |
132 | $this->mVariant = $request->getText( 'variant' ); |
133 | $this->mReturnTo = $request->getVal( 'returnto', '' ); |
134 | $this->mReturnToQuery = $request->getVal( 'returntoquery', '' ); |
135 | } |
136 | |
137 | /** |
138 | * Load data from request. |
139 | * @internal |
140 | * @param string $subPage Subpage of Special:Userlogin |
141 | */ |
142 | protected function load( $subPage ) { |
143 | $this->loadRequestParameters(); |
144 | if ( $this->mLoaded ) { |
145 | return; |
146 | } |
147 | $this->mLoaded = true; |
148 | $request = $this->getRequest(); |
149 | |
150 | $securityLevel = $this->getRequest()->getText( 'force' ); |
151 | if ( |
152 | $securityLevel && |
153 | MediaWikiServices::getInstance()->getAuthManager()->securitySensitiveOperationStatus( |
154 | $securityLevel ) === AuthManager::SEC_REAUTH |
155 | ) { |
156 | $this->securityLevel = $securityLevel; |
157 | } |
158 | |
159 | $this->loadAuth( $subPage ); |
160 | |
161 | $this->mToken = $request->getVal( $this->getTokenName() ); |
162 | |
163 | // Show an error or warning passed on from a previous page |
164 | $entryError = $this->msg( $request->getVal( 'error', '' ) ); |
165 | $entryWarning = $this->msg( $request->getVal( 'warning', '' ) ); |
166 | // bc: provide login link as a parameter for messages where the translation |
167 | // was not updated |
168 | $loginreqlink = $this->getLinkRenderer()->makeKnownLink( |
169 | $this->getPageTitle(), |
170 | $this->msg( 'loginreqlink' )->text(), |
171 | [], |
172 | [ |
173 | 'returnto' => $this->mReturnTo, |
174 | 'returntoquery' => $this->mReturnToQuery, |
175 | 'uselang' => $this->mLanguage ?: null, |
176 | 'variant' => $this->mVariant ?: null, |
177 | 'fromhttp' => $this->getConfig()->get( MainConfigNames::SecureLogin ) && |
178 | $this->mFromHTTP ? '1' : null, |
179 | ] |
180 | ); |
181 | |
182 | // Only show valid error or warning messages. |
183 | if ( $entryError->exists() |
184 | && in_array( $entryError->getKey(), LoginHelper::getValidErrorMessages(), true ) |
185 | ) { |
186 | $this->mEntryErrorType = 'error'; |
187 | $this->mEntryError = $entryError->rawParams( $loginreqlink )->parse(); |
188 | |
189 | } elseif ( $entryWarning->exists() |
190 | && in_array( $entryWarning->getKey(), LoginHelper::getValidErrorMessages(), true ) |
191 | ) { |
192 | $this->mEntryErrorType = 'warning'; |
193 | $this->mEntryError = $entryWarning->rawParams( $loginreqlink )->parse(); |
194 | } |
195 | |
196 | # 1. When switching accounts, it sucks to get automatically logged out |
197 | # 2. Do not return to PasswordReset after a successful password change |
198 | # but goto Wiki start page (Main_Page) instead ( T35997 ) |
199 | $returnToTitle = Title::newFromText( $this->mReturnTo ); |
200 | if ( is_object( $returnToTitle ) |
201 | && ( $returnToTitle->isSpecial( 'Userlogout' ) |
202 | || $returnToTitle->isSpecial( 'PasswordReset' ) ) |
203 | ) { |
204 | $this->mReturnTo = ''; |
205 | $this->mReturnToQuery = ''; |
206 | } |
207 | } |
208 | |
209 | protected function getPreservedParams( $withToken = false ) { |
210 | $params = parent::getPreservedParams( $withToken ); |
211 | $params += [ |
212 | 'returnto' => $this->mReturnTo ?: null, |
213 | 'returntoquery' => $this->mReturnToQuery ?: null, |
214 | ]; |
215 | if ( $this->getConfig()->get( MainConfigNames::SecureLogin ) && !$this->isSignup() ) { |
216 | $params['fromhttp'] = $this->mFromHTTP ? '1' : null; |
217 | } |
218 | return $params; |
219 | } |
220 | |
221 | protected function beforeExecute( $subPage ) { |
222 | // finish initializing the class before processing the request - T135924 |
223 | $this->loadRequestParameters(); |
224 | return parent::beforeExecute( $subPage ); |
225 | } |
226 | |
227 | /** |
228 | * @param string|null $subPage |
229 | * @suppress PhanTypeObjectUnsetDeclaredProperty |
230 | */ |
231 | public function execute( $subPage ) { |
232 | if ( $this->mPosted ) { |
233 | $time = microtime( true ); |
234 | $profilingScope = new ScopedCallback( function () use ( $time ) { |
235 | $time = microtime( true ) - $time; |
236 | $stats = MediaWikiServices::getInstance()->getStatsFactory(); |
237 | $stats->getTiming( 'auth_specialpage_executeTiming_seconds' ) |
238 | ->setLabel( 'action', $this->authAction ) |
239 | ->copyToStatsdAt( "timing.login.ui.{$this->authAction}" ) |
240 | ->observe( $time * 1000 ); |
241 | } ); |
242 | } |
243 | |
244 | $authManager = MediaWikiServices::getInstance()->getAuthManager(); |
245 | $session = SessionManager::getGlobalSession(); |
246 | |
247 | // Session data is used for various things in the authentication process, so we must make |
248 | // sure a session cookie or some equivalent mechanism is set. |
249 | $session->persist(); |
250 | // Explicitly disable cache to ensure cookie blocks may be set (T152462). |
251 | // (Technically redundant with sessions persisting from this page.) |
252 | $this->getOutput()->disableClientCache(); |
253 | |
254 | $this->load( $subPage ); |
255 | $this->setHeaders(); |
256 | $this->checkPermissions(); |
257 | |
258 | // Make sure the system configuration allows log in / sign up |
259 | if ( !$this->isSignup() && !$authManager->canAuthenticateNow() ) { |
260 | if ( !$session->canSetUser() ) { |
261 | throw new ErrorPageError( 'cannotloginnow-title', 'cannotloginnow-text', [ |
262 | $session->getProvider()->describe( $this->getLanguage() ) |
263 | ] ); |
264 | } |
265 | throw new ErrorPageError( 'cannotlogin-title', 'cannotlogin-text' ); |
266 | } elseif ( $this->isSignup() && !$authManager->canCreateAccounts() ) { |
267 | throw new ErrorPageError( 'cannotcreateaccount-title', 'cannotcreateaccount-text' ); |
268 | } |
269 | |
270 | /* |
271 | * In the case where the user is already logged in, and was redirected to |
272 | * the login form from a page that requires login, do not show the login |
273 | * page. The use case scenario for this is when a user opens a large number |
274 | * of tabs, is redirected to the login page on all of them, and then logs |
275 | * in on one, expecting all the others to work properly. |
276 | * |
277 | * However, do show the form if it was visited intentionally (no 'returnto' |
278 | * is present). People who often switch between several accounts have grown |
279 | * accustomed to this behavior. |
280 | * |
281 | * For temporary users, the form is always shown, since the UI presents |
282 | * temporary users as not logged in and offers to discard their temporary |
283 | * account by logging in. |
284 | * |
285 | * Also make an exception when force=<level> is set in the URL, which means the user must |
286 | * reauthenticate for security reasons. |
287 | */ |
288 | if ( !$this->isSignup() && !$this->mPosted && !$this->securityLevel && |
289 | ( $this->mReturnTo !== '' || $this->mReturnToQuery !== '' ) && |
290 | !$this->getUser()->isTemp() && $this->getUser()->isRegistered() |
291 | ) { |
292 | $this->successfulAction(); |
293 | return; |
294 | } |
295 | |
296 | // If logging in and not on HTTPS, either redirect to it or offer a link. |
297 | if ( $this->getRequest()->getProtocol() !== 'https' ) { |
298 | $title = $this->getFullTitle(); |
299 | $query = $this->getPreservedParams( false ) + [ |
300 | 'title' => null, |
301 | ( $this->mEntryErrorType === 'error' ? 'error' |
302 | : 'warning' ) => $this->mEntryError, |
303 | ] + $this->getRequest()->getQueryValues(); |
304 | $url = $title->getFullURL( $query, false, PROTO_HTTPS ); |
305 | if ( $this->getConfig()->get( MainConfigNames::SecureLogin ) && !$this->mFromHTTP ) { |
306 | // Avoid infinite redirect |
307 | $url = wfAppendQuery( $url, 'fromhttp=1' ); |
308 | $this->getOutput()->redirect( $url ); |
309 | // Since we only do this redir to change proto, always vary |
310 | $this->getOutput()->addVaryHeader( 'X-Forwarded-Proto' ); |
311 | |
312 | return; |
313 | } else { |
314 | // A wiki without HTTPS login support should set $wgServer to |
315 | // http://somehost, in which case the secure URL generated |
316 | // above won't actually start with https:// |
317 | if ( str_starts_with( $url, 'https://' ) ) { |
318 | $this->mSecureLoginUrl = $url; |
319 | } |
320 | } |
321 | } |
322 | |
323 | if ( !$this->isActionAllowed( $this->authAction ) ) { |
324 | // FIXME how do we explain this to the user? can we handle session loss better? |
325 | // messages used: authpage-cannot-login, authpage-cannot-login-continue, |
326 | // authpage-cannot-create, authpage-cannot-create-continue |
327 | $this->mainLoginForm( [], 'authpage-cannot-' . $this->authAction ); |
328 | return; |
329 | } |
330 | |
331 | if ( $this->canBypassForm( $button_name ) ) { |
332 | $this->setRequest( [], true ); |
333 | $this->getRequest()->setVal( $this->getTokenName(), $this->getToken() ); |
334 | if ( $button_name ) { |
335 | $this->getRequest()->setVal( $button_name, true ); |
336 | } |
337 | } |
338 | |
339 | $status = $this->trySubmit(); |
340 | |
341 | if ( !$status || !$status->isGood() ) { |
342 | $this->mainLoginForm( $this->authRequests, $status ? $status->getMessage() : '', 'error' ); |
343 | return; |
344 | } |
345 | |
346 | /** @var AuthenticationResponse $response */ |
347 | $response = $status->getValue(); |
348 | |
349 | $returnToUrl = $this->getPageTitle( 'return' ) |
350 | ->getFullURL( $this->getPreservedParams( true ), false, PROTO_HTTPS ); |
351 | switch ( $response->status ) { |
352 | case AuthenticationResponse::PASS: |
353 | $this->logAuthResult( true ); |
354 | $this->proxyAccountCreation = $this->isSignup() && $this->getUser()->isNamed(); |
355 | $this->targetUser = User::newFromName( $response->username ); |
356 | |
357 | if ( |
358 | !$this->proxyAccountCreation |
359 | && $response->loginRequest |
360 | && $authManager->canAuthenticateNow() |
361 | ) { |
362 | // successful registration; log the user in instantly |
363 | $response2 = $authManager->beginAuthentication( [ $response->loginRequest ], |
364 | $returnToUrl ); |
365 | if ( $response2->status !== AuthenticationResponse::PASS ) { |
366 | LoggerFactory::getInstance( 'login' ) |
367 | ->error( 'Could not log in after account creation' ); |
368 | $this->successfulAction( true, Status::newFatal( 'createacct-loginerror' ) ); |
369 | break; |
370 | } |
371 | } |
372 | |
373 | if ( !$this->proxyAccountCreation ) { |
374 | // Ensure that the context user is the same as the session user. |
375 | $this->setSessionUserForCurrentRequest(); |
376 | } |
377 | |
378 | $this->successfulAction( true ); |
379 | break; |
380 | case AuthenticationResponse::FAIL: |
381 | // fall through |
382 | case AuthenticationResponse::RESTART: |
383 | unset( $this->authForm ); |
384 | if ( $response->status === AuthenticationResponse::FAIL ) { |
385 | $action = $this->getDefaultAction( $subPage ); |
386 | $messageType = 'error'; |
387 | } else { |
388 | $action = $this->getContinueAction( $this->authAction ); |
389 | $messageType = 'warning'; |
390 | } |
391 | $this->logAuthResult( false, $response->message ? $response->message->getKey() : '-' ); |
392 | $this->loadAuth( $subPage, $action, true ); |
393 | $this->mainLoginForm( $this->authRequests, $response->message, $messageType ); |
394 | break; |
395 | case AuthenticationResponse::REDIRECT: |
396 | unset( $this->authForm ); |
397 | $this->getOutput()->redirect( $response->redirectTarget ); |
398 | break; |
399 | case AuthenticationResponse::UI: |
400 | unset( $this->authForm ); |
401 | $this->authAction = $this->isSignup() ? AuthManager::ACTION_CREATE_CONTINUE |
402 | : AuthManager::ACTION_LOGIN_CONTINUE; |
403 | $this->authRequests = $response->neededRequests; |
404 | $this->mainLoginForm( $response->neededRequests, $response->message, $response->messageType ); |
405 | break; |
406 | default: |
407 | throw new LogicException( 'invalid AuthenticationResponse' ); |
408 | } |
409 | } |
410 | |
411 | /** |
412 | * Determine if the login form can be bypassed. This will be the case when no more than one |
413 | * button is present and no other user input fields that are not marked as 'skippable' are |
414 | * present. If the login form were not bypassed, the user would be presented with a |
415 | * superfluous page on which they must press the single button to proceed with login. |
416 | * Not only does this cause an additional mouse click and page load, it confuses users, |
417 | * especially since there are a help link and forgotten password link that are |
418 | * provided on the login page that do not apply to this situation. |
419 | * |
420 | * @param string|null &$button_name if the form has a single button, returns |
421 | * the name of the button; otherwise, returns null |
422 | * @return bool |
423 | */ |
424 | private function canBypassForm( &$button_name ) { |
425 | $button_name = null; |
426 | if ( $this->isContinued() ) { |
427 | return false; |
428 | } |
429 | $fields = AuthenticationRequest::mergeFieldInfo( $this->authRequests ); |
430 | foreach ( $fields as $fieldname => $field ) { |
431 | if ( !isset( $field['type'] ) ) { |
432 | return false; |
433 | } |
434 | if ( !empty( $field['skippable'] ) ) { |
435 | continue; |
436 | } |
437 | if ( $field['type'] === 'button' ) { |
438 | if ( $button_name !== null ) { |
439 | $button_name = null; |
440 | return false; |
441 | } else { |
442 | $button_name = $fieldname; |
443 | } |
444 | } elseif ( $field['type'] !== 'null' ) { |
445 | return false; |
446 | } |
447 | } |
448 | return true; |
449 | } |
450 | |
451 | /** |
452 | * Show the success page. |
453 | * |
454 | * @param string $type Condition of return to; see `executeReturnTo` |
455 | * @param string|Message $title Page's title |
456 | * @param string $msgname |
457 | * @param string $injected_html |
458 | * @param StatusValue|null $extraMessages |
459 | */ |
460 | protected function showSuccessPage( |
461 | $type, $title, $msgname, $injected_html, $extraMessages |
462 | ) { |
463 | $out = $this->getOutput(); |
464 | if ( is_string( $title ) ) { |
465 | wfDeprecated( __METHOD__ . ' with string title', '1.41' ); // T343849 |
466 | $title = ( new RawMessage( '$1' ) )->rawParams( $title ); |
467 | } |
468 | $out->setPageTitleMsg( $title ); |
469 | if ( $msgname ) { |
470 | $out->addWikiMsg( $msgname, wfEscapeWikiText( $this->getUser()->getName() ) ); |
471 | } |
472 | if ( $extraMessages ) { |
473 | $extraMessages = Status::wrap( $extraMessages ); |
474 | $out->addWikiTextAsInterface( |
475 | $extraMessages->getWikiText( false, false, $this->getLanguage() ) |
476 | ); |
477 | } |
478 | |
479 | $out->addHTML( $injected_html ); |
480 | |
481 | $helper = new LoginHelper( $this->getContext() ); |
482 | $helper->showReturnToPage( $type, $this->mReturnTo, $this->mReturnToQuery, $this->mStickHTTPS ); |
483 | } |
484 | |
485 | /** |
486 | * Add a "return to" link or redirect to it. |
487 | * Extensions can use this to reuse the "return to" logic after |
488 | * inject steps (such as redirection) into the login process. |
489 | * |
490 | * @param string $type One of the following: |
491 | * - error: display a return to link ignoring $wgRedirectOnLogin |
492 | * - signup: display a return to link using $wgRedirectOnLogin if needed |
493 | * - success: display a return to link using $wgRedirectOnLogin if needed |
494 | * - successredirect: send an HTTP redirect using $wgRedirectOnLogin if needed |
495 | * @param string $returnTo |
496 | * @param array|string $returnToQuery |
497 | * @param bool $stickHTTPS Keep redirect link on HTTPS |
498 | * @since 1.22 |
499 | */ |
500 | public function showReturnToPage( |
501 | $type, $returnTo = '', $returnToQuery = '', $stickHTTPS = false |
502 | ) { |
503 | $helper = new LoginHelper( $this->getContext() ); |
504 | $helper->showReturnToPage( $type, $returnTo, $returnToQuery, $stickHTTPS ); |
505 | } |
506 | |
507 | /** |
508 | * Replace some globals to make sure the fact that the user has just been logged in is |
509 | * reflected in the current request. |
510 | */ |
511 | protected function setSessionUserForCurrentRequest() { |
512 | global $wgLang; |
513 | |
514 | $context = RequestContext::getMain(); |
515 | $localContext = $this->getContext(); |
516 | if ( $context !== $localContext ) { |
517 | // remove AuthManagerSpecialPage context hack |
518 | $this->setContext( $context ); |
519 | } |
520 | |
521 | $user = $context->getRequest()->getSession()->getUser(); |
522 | |
523 | StubGlobalUser::setUser( $user ); |
524 | $context->setUser( $user ); |
525 | |
526 | $wgLang = $context->getLanguage(); |
527 | } |
528 | |
529 | /** |
530 | * @param AuthenticationRequest[] $requests A list of AuthorizationRequest objects, |
531 | * used to generate the form fields. An empty array means a fatal error |
532 | * (authentication cannot continue). |
533 | * @param string|Message $msg |
534 | * @param string $msgtype |
535 | * @throws ErrorPageError |
536 | * @throws Exception |
537 | * @throws FatalError |
538 | * @throws PermissionsError |
539 | * @throws ReadOnlyError |
540 | * @internal |
541 | */ |
542 | protected function mainLoginForm( array $requests, $msg = '', $msgtype = 'error' ) { |
543 | $user = $this->getUser(); |
544 | $out = $this->getOutput(); |
545 | |
546 | // FIXME how to handle empty $requests - restart, or no form, just an error message? |
547 | // no form would be better for no session type errors, restart is better when can* fails. |
548 | if ( !$requests ) { |
549 | $this->authAction = $this->getDefaultAction( $this->subPage ); |
550 | $this->authForm = null; |
551 | $requests = MediaWikiServices::getInstance()->getAuthManager() |
552 | ->getAuthenticationRequests( $this->authAction, $user ); |
553 | } |
554 | |
555 | // Generic styles and scripts for both login and signup form |
556 | $out->addModuleStyles( [ |
557 | 'mediawiki.special.userlogin.common.styles' |
558 | ] ); |
559 | if ( $this->isSignup() ) { |
560 | // XXX hack pending RL or JS parse() support for complex content messages T27349 |
561 | $out->addJsConfigVars( 'wgCreateacctImgcaptchaHelp', |
562 | $this->msg( 'createacct-imgcaptcha-help' )->parse() ); |
563 | |
564 | // Additional styles and scripts for signup form |
565 | $out->addModules( 'mediawiki.special.createaccount' ); |
566 | $out->addModuleStyles( [ |
567 | 'mediawiki.special.userlogin.signup.styles' |
568 | ] ); |
569 | } else { |
570 | // Additional styles for login form |
571 | $out->addModuleStyles( [ |
572 | 'mediawiki.special.userlogin.login.styles' |
573 | ] ); |
574 | } |
575 | $out->disallowUserJs(); // just in case... |
576 | |
577 | $form = $this->getAuthForm( $requests, $this->authAction ); |
578 | $form->prepareForm(); |
579 | |
580 | $submitStatus = Status::newGood(); |
581 | if ( $msg && $msgtype === 'warning' ) { |
582 | $submitStatus->warning( $msg ); |
583 | } elseif ( $msg && $msgtype === 'error' ) { |
584 | $submitStatus->fatal( $msg ); |
585 | } |
586 | |
587 | // warning header for non-standard workflows (e.g. security reauthentication) |
588 | if ( |
589 | !$this->isSignup() && |
590 | $this->getUser()->isRegistered() && |
591 | !$this->getUser()->isTemp() && |
592 | $this->authAction !== AuthManager::ACTION_LOGIN_CONTINUE |
593 | ) { |
594 | $reauthMessage = $this->securityLevel ? 'userlogin-reauth' : 'userlogin-loggedin'; |
595 | $submitStatus->warning( $reauthMessage, $this->getUser()->getName() ); |
596 | } |
597 | |
598 | $formHtml = $form->getHTML( $submitStatus ); |
599 | |
600 | $out->addHTML( $this->getPageHtml( $formHtml ) ); |
601 | } |
602 | |
603 | /** |
604 | * Add page elements which are outside the form. |
605 | * FIXME this should probably be a template, but use a sensible language (handlebars?) |
606 | * @param string $formHtml |
607 | * @return string |
608 | */ |
609 | protected function getPageHtml( $formHtml ) { |
610 | $loginPrompt = $this->isSignup() ? '' : Html::rawElement( 'div', |
611 | [ 'id' => 'userloginprompt' ], $this->msg( 'loginprompt' )->parseAsBlock() ); |
612 | $languageLinks = $this->getConfig()->get( MainConfigNames::LoginLanguageSelector ) |
613 | ? $this->makeLanguageSelector() : ''; |
614 | $signupStartMsg = $this->msg( 'signupstart' ); |
615 | $signupStart = ( $this->isSignup() && !$signupStartMsg->isDisabled() ) |
616 | ? Html::rawElement( 'div', [ 'id' => 'signupstart' ], $signupStartMsg->parseAsBlock() ) : ''; |
617 | if ( $languageLinks ) { |
618 | $languageLinks = Html::rawElement( 'div', [ 'id' => 'languagelinks' ], |
619 | Html::rawElement( 'p', [], $languageLinks ) |
620 | ); |
621 | } |
622 | if ( $this->getUser()->isTemp() ) { |
623 | $noticeHtml = $this->getNoticeHtml(); |
624 | } else { |
625 | $noticeHtml = ''; |
626 | } |
627 | $formBlock = Html::rawElement( 'div', [ 'id' => 'userloginForm' ], $formHtml ); |
628 | $formAndBenefits = $formBlock; |
629 | if ( $this->isSignup() && $this->showExtraInformation() ) { |
630 | $benefitsContainerHtml = null; |
631 | $info = [ |
632 | 'context' => $this->getContext(), |
633 | 'form' => $this->authForm, |
634 | ]; |
635 | $options = [ |
636 | 'beforeForm' => false, |
637 | ]; |
638 | $this->getHookRunner()->onSpecialCreateAccountBenefits( |
639 | $benefitsContainerHtml, $info, $options |
640 | ); |
641 | if ( $benefitsContainerHtml === null ) { |
642 | $benefitsContainerHtml = $this->getBenefitsContainerHtml(); |
643 | } |
644 | $formAndBenefits = $options['beforeForm'] |
645 | ? ( $benefitsContainerHtml . $formBlock ) |
646 | : ( $formBlock . $benefitsContainerHtml ); |
647 | } |
648 | |
649 | return Html::rawElement( 'div', [ 'class' => 'mw-ui-container' ], |
650 | $loginPrompt |
651 | . $languageLinks |
652 | . $signupStart |
653 | . $noticeHtml |
654 | . $formAndBenefits |
655 | ); |
656 | } |
657 | |
658 | /** |
659 | * The HTML to be shown in the "benefits to signing in / creating an account" section of the signup/login page. |
660 | * |
661 | * @unstable Experimental method added in 1.38. As noted in the comment from 2015 for getPageHtml, |
662 | * this should use a template. |
663 | * @return string |
664 | */ |
665 | protected function getBenefitsContainerHtml(): string { |
666 | $benefitsContainer = ''; |
667 | $this->getOutput()->addModuleStyles( [ 'oojs-ui.styles.icons-user' ] ); |
668 | if ( $this->isSignup() && $this->showExtraInformation() ) { |
669 | if ( !$this->getUser()->isTemp() ) { |
670 | // The following messages are used here: |
671 | // * createacct-benefit-icon1 createacct-benefit-head1 createacct-benefit-body1 |
672 | // * createacct-benefit-icon2 createacct-benefit-head2 createacct-benefit-body2 |
673 | // * createacct-benefit-icon3 createacct-benefit-head3 createacct-benefit-body3 |
674 | $benefitCount = 3; |
675 | $benefitList = ''; |
676 | for ( $benefitIdx = 1; $benefitIdx <= $benefitCount; $benefitIdx++ ) { |
677 | $headUnescaped = $this->msg( "createacct-benefit-head$benefitIdx" )->text(); |
678 | $iconClass = $this->msg( "createacct-benefit-icon$benefitIdx" )->text(); |
679 | $benefitList .= Html::rawElement( 'div', [ 'class' => "mw-number-text $iconClass" ], |
680 | Html::rawElement( 'h3', [], |
681 | $this->msg( "createacct-benefit-head$benefitIdx" )->escaped() |
682 | ) |
683 | . Html::rawElement( 'p', [], |
684 | $this->msg( "createacct-benefit-body$benefitIdx" )->params( $headUnescaped )->escaped() |
685 | ) |
686 | ); |
687 | } |
688 | $benefitsContainer = Html::rawElement( 'div', [ 'class' => 'mw-createacct-benefits-container' ], |
689 | Html::rawElement( 'h2', [], $this->msg( 'createacct-benefit-heading' )->escaped() ) |
690 | . Html::rawElement( 'div', [ 'class' => 'mw-createacct-benefits-list' ], $benefitList ) |
691 | ); |
692 | } else { |
693 | $benefitList = ''; |
694 | $this->getOutput()->addModuleStyles( |
695 | [ |
696 | 'oojs-ui.styles.icons-moderation', |
697 | 'oojs-ui.styles.icons-interactions', |
698 | ] |
699 | ); |
700 | $benefits = [ |
701 | [ |
702 | 'icon' => 'oo-ui-icon-unStar', |
703 | 'description' => $this->msg( "benefit-1-description" )->escaped() |
704 | ], |
705 | [ |
706 | 'icon' => 'oo-ui-icon-userContributions', |
707 | 'description' => $this->msg( "benefit-2-description" )->escaped() |
708 | ], |
709 | [ |
710 | 'icon' => 'oo-ui-icon-settings', |
711 | 'description' => $this->msg( "benefit-3-description" )->escaped() |
712 | ] |
713 | ]; |
714 | foreach ( $benefits as $benefit ) { |
715 | $benefitContent = Html::rawElement( 'div', [ 'class' => 'mw-benefit-item' ], |
716 | Html::rawElement( 'span', [ 'class' => $benefit[ 'icon' ] ] ) |
717 | . Html::rawElement( 'p', [], $benefit['description'] ) |
718 | ); |
719 | |
720 | $benefitList .= Html::rawElement( |
721 | 'div', [ 'class' => 'mw-benefit-item-wrapper' ], $benefitContent ); |
722 | } |
723 | |
724 | $benefitsListWrapper = Html::rawElement( |
725 | 'div', [ 'class' => 'mw-benefit-list-wrapper' ], $benefitList ); |
726 | |
727 | $headingSubheadingWrapper = Html::rawElement( 'div', [ 'class' => 'mw-heading-subheading-wrapper' ], |
728 | Html::rawElement( 'h2', [], $this->msg( 'createacct-benefit-heading-temp-user' )->escaped() ) |
729 | . Html::rawElement( 'p', [ 'class' => 'mw-benefit-subheading' ], $this->msg( |
730 | 'createacct-benefit-subheading-temp-user' )->escaped() ) |
731 | ); |
732 | |
733 | $benefitsContainer = Html::rawElement( |
734 | 'div', [ 'class' => 'mw-createacct-benefits-container' ], |
735 | $headingSubheadingWrapper |
736 | . $benefitsListWrapper |
737 | ); |
738 | } |
739 | } |
740 | return $benefitsContainer; |
741 | } |
742 | |
743 | /** |
744 | * Generates a form from the given request. |
745 | * @param AuthenticationRequest[] $requests |
746 | * @param string $action AuthManager action name |
747 | * @return HTMLForm |
748 | */ |
749 | protected function getAuthForm( array $requests, $action ) { |
750 | // FIXME merge this with parent |
751 | |
752 | if ( isset( $this->authForm ) ) { |
753 | return $this->authForm; |
754 | } |
755 | |
756 | $usingHTTPS = $this->getRequest()->getProtocol() === 'https'; |
757 | |
758 | // get basic form description from the auth logic |
759 | $fieldInfo = AuthenticationRequest::mergeFieldInfo( $requests ); |
760 | // this will call onAuthChangeFormFields() |
761 | $formDescriptor = $this->fieldInfoToFormDescriptor( $requests, $fieldInfo, $this->authAction ); |
762 | $this->postProcessFormDescriptor( $formDescriptor, $requests ); |
763 | |
764 | $context = $this->getContext(); |
765 | if ( $context->getRequest() !== $this->getRequest() ) { |
766 | // We have overridden the request, need to make sure the form uses that too. |
767 | $context = new DerivativeContext( $this->getContext() ); |
768 | $context->setRequest( $this->getRequest() ); |
769 | } |
770 | $form = HTMLForm::factory( 'codex', $formDescriptor, $context ); |
771 | |
772 | $form->addHiddenField( 'authAction', $this->authAction ); |
773 | if ( $this->mLanguage ) { |
774 | $form->addHiddenField( 'uselang', $this->mLanguage ); |
775 | } |
776 | if ( $this->mVariant ) { |
777 | $form->addHiddenField( 'variant', $this->mVariant ); |
778 | } |
779 | $form->addHiddenField( 'force', $this->securityLevel ); |
780 | $form->addHiddenField( $this->getTokenName(), $this->getToken()->toString() ); |
781 | $config = $this->getConfig(); |
782 | if ( $config->get( MainConfigNames::SecureLogin ) && |
783 | !$config->get( MainConfigNames::ForceHTTPS ) ) { |
784 | // If using HTTPS coming from HTTP, then the 'fromhttp' parameter must be preserved |
785 | if ( !$this->isSignup() ) { |
786 | $form->addHiddenField( 'wpForceHttps', (int)$this->mStickHTTPS ); |
787 | $form->addHiddenField( 'wpFromhttp', $usingHTTPS ); |
788 | } |
789 | } |
790 | |
791 | // set properties of the form itself |
792 | $form->setAction( $this->getPageTitle()->getLocalURL( $this->getReturnToQueryStringFragment() ) ); |
793 | $form->setName( 'userlogin' . ( $this->isSignup() ? '2' : '' ) ); |
794 | if ( $this->isSignup() ) { |
795 | $form->setId( 'userlogin2' ); |
796 | } |
797 | |
798 | $form->suppressDefaultSubmit(); |
799 | |
800 | $this->authForm = $form; |
801 | |
802 | return $form; |
803 | } |
804 | |
805 | /** @inheritDoc */ |
806 | public function onAuthChangeFormFields( |
807 | array $requests, array $fieldInfo, array &$formDescriptor, $action |
808 | ) { |
809 | $formDescriptor = self::mergeDefaultFormDescriptor( $fieldInfo, $formDescriptor, |
810 | $this->getFieldDefinitions( $fieldInfo ) ); |
811 | } |
812 | |
813 | /** |
814 | * Show extra information such as password recovery information, link from login to signup, |
815 | * CTA etc? Such information should only be shown on the "landing page", ie. when the user |
816 | * is at the first step of the authentication process. |
817 | * @return bool |
818 | */ |
819 | protected function showExtraInformation() { |
820 | return $this->authAction !== $this->getContinueAction( $this->authAction ) |
821 | && !$this->securityLevel; |
822 | } |
823 | |
824 | /** |
825 | * Create a HTMLForm descriptor for the core login fields. |
826 | * @param array $fieldInfo |
827 | * @return array |
828 | */ |
829 | protected function getFieldDefinitions( array $fieldInfo ) { |
830 | $isLoggedIn = $this->getUser()->isRegistered(); |
831 | $continuePart = $this->isContinued() ? 'continue-' : ''; |
832 | $anotherPart = $isLoggedIn ? 'another-' : ''; |
833 | // @phan-suppress-next-line PhanUndeclaredMethod |
834 | $expiration = $this->getRequest()->getSession()->getProvider()->getRememberUserDuration(); |
835 | $expirationDays = ceil( $expiration / ( 3600 * 24 ) ); |
836 | $secureLoginLink = ''; |
837 | if ( $this->mSecureLoginUrl ) { |
838 | $secureLoginLink = Html::element( 'a', [ |
839 | 'href' => $this->mSecureLoginUrl, |
840 | 'class' => 'mw-login-flush-right mw-secure', |
841 | ], $this->msg( 'userlogin-signwithsecure' )->text() ); |
842 | } |
843 | $usernameHelpLink = ''; |
844 | if ( !$this->msg( 'createacct-helpusername' )->isDisabled() ) { |
845 | $usernameHelpLink = Html::rawElement( 'span', [ |
846 | 'class' => 'mw-login-flush-right', |
847 | ], $this->msg( 'createacct-helpusername' )->parse() ); |
848 | } |
849 | |
850 | if ( $this->isSignup() ) { |
851 | $config = $this->getConfig(); |
852 | $hideIf = isset( $fieldInfo['mailpassword'] ) ? [ 'hide-if' => [ '===', 'mailpassword', '1' ] ] : []; |
853 | $fieldDefinitions = [ |
854 | 'statusarea' => [ |
855 | // Used by the mediawiki.special.createaccount module for error display. |
856 | // FIXME: Merge this with HTMLForm's normal status (error) area |
857 | 'type' => 'info', |
858 | 'raw' => true, |
859 | 'default' => Html::element( 'div', [ 'id' => 'mw-createacct-status-area' ] ), |
860 | 'weight' => -105, |
861 | ], |
862 | 'username' => [ |
863 | 'label-raw' => $this->msg( 'userlogin-yourname' )->escaped() . $usernameHelpLink, |
864 | 'id' => 'wpName2', |
865 | 'placeholder-message' => $isLoggedIn ? 'createacct-another-username-ph' |
866 | : 'userlogin-yourname-ph', |
867 | ], |
868 | 'mailpassword' => [ |
869 | // create account without providing password, a temporary one will be mailed |
870 | 'type' => 'check', |
871 | 'label-message' => 'createaccountmail', |
872 | 'name' => 'wpCreateaccountMail', |
873 | 'id' => 'wpCreateaccountMail', |
874 | ], |
875 | 'password' => [ |
876 | 'id' => 'wpPassword2', |
877 | 'autocomplete' => 'new-password', |
878 | 'placeholder-message' => 'createacct-yourpassword-ph', |
879 | 'help-message' => 'createacct-useuniquepass', |
880 | ] + $hideIf, |
881 | 'domain' => [], |
882 | 'retype' => [ |
883 | 'type' => 'password', |
884 | 'label-message' => 'createacct-yourpasswordagain', |
885 | 'id' => 'wpRetype', |
886 | 'cssclass' => 'loginPassword', |
887 | 'size' => 20, |
888 | 'autocomplete' => 'new-password', |
889 | 'validation-callback' => function ( $value, $alldata ) { |
890 | if ( empty( $alldata['mailpassword'] ) && !empty( $alldata['password'] ) ) { |
891 | if ( !$value ) { |
892 | return $this->msg( 'htmlform-required' ); |
893 | } elseif ( $value !== $alldata['password'] ) { |
894 | return $this->msg( 'badretype' ); |
895 | } |
896 | } |
897 | return true; |
898 | }, |
899 | 'placeholder-message' => 'createacct-yourpasswordagain-ph', |
900 | ] + $hideIf, |
901 | 'email' => [ |
902 | 'type' => 'email', |
903 | 'label-message' => $config->get( MainConfigNames::EmailConfirmToEdit ) |
904 | ? 'createacct-emailrequired' : 'createacct-emailoptional', |
905 | 'id' => 'wpEmail', |
906 | 'cssclass' => 'loginText', |
907 | 'size' => '20', |
908 | 'maxlength' => 255, |
909 | 'autocomplete' => 'email', |
910 | // FIXME will break non-standard providers |
911 | 'required' => $config->get( MainConfigNames::EmailConfirmToEdit ), |
912 | 'validation-callback' => function ( $value, $alldata ) { |
913 | // AuthManager will check most of these, but that will make the auth |
914 | // session fail and this won't, so nicer to do it this way |
915 | if ( !$value && |
916 | $this->getConfig()->get( MainConfigNames::EmailConfirmToEdit ) |
917 | ) { |
918 | // no point in allowing registration without email when email is |
919 | // required to edit |
920 | return $this->msg( 'noemailtitle' ); |
921 | } elseif ( !$value && !empty( $alldata['mailpassword'] ) ) { |
922 | // cannot send password via email when there is no email address |
923 | return $this->msg( 'noemailcreate' ); |
924 | } elseif ( $value && !Sanitizer::validateEmail( $value ) ) { |
925 | return $this->msg( 'invalidemailaddress' ); |
926 | } elseif ( is_string( $value ) && strlen( $value ) > 255 ) { |
927 | return $this->msg( 'changeemail-maxlength' ); |
928 | } |
929 | return true; |
930 | }, |
931 | // The following messages are used here: |
932 | // * createacct-email-ph |
933 | // * createacct-another-email-ph |
934 | 'placeholder-message' => 'createacct-' . $anotherPart . 'email-ph', |
935 | ], |
936 | 'realname' => [ |
937 | 'type' => 'text', |
938 | 'help-message' => $isLoggedIn ? 'createacct-another-realname-tip' |
939 | : 'prefs-help-realname', |
940 | 'label-message' => 'createacct-realname', |
941 | 'cssclass' => 'loginText', |
942 | 'size' => 20, |
943 | 'placeholder-message' => 'createacct-realname', |
944 | 'id' => 'wpRealName', |
945 | 'autocomplete' => 'name', |
946 | ], |
947 | 'reason' => [ |
948 | // comment for the user creation log |
949 | 'type' => 'text', |
950 | 'label-message' => 'createacct-reason', |
951 | 'cssclass' => 'loginText', |
952 | 'id' => 'wpReason', |
953 | 'size' => '20', |
954 | 'validation-callback' => function ( $value, $alldata ) { |
955 | // if the user sets an email address as the user creation reason, confirm that |
956 | // that was their intent |
957 | if ( $value && Sanitizer::validateEmail( $value ) ) { |
958 | if ( $this->reasonValidatorResult !== null ) { |
959 | return $this->reasonValidatorResult; |
960 | } |
961 | $this->reasonValidatorResult = true; |
962 | $authManager = MediaWikiServices::getInstance()->getAuthManager(); |
963 | if ( !$authManager->getAuthenticationSessionData( 'reason-retry', false ) ) { |
964 | $authManager->setAuthenticationSessionData( 'reason-retry', true ); |
965 | $this->reasonValidatorResult = $this->msg( 'createacct-reason-confirm' ); |
966 | } |
967 | return $this->reasonValidatorResult; |
968 | } |
969 | return true; |
970 | }, |
971 | 'placeholder-message' => 'createacct-reason-ph', |
972 | ], |
973 | 'createaccount' => [ |
974 | // submit button |
975 | 'type' => 'submit', |
976 | // The following messages are used here: |
977 | // * createacct-submit |
978 | // * createacct-another-submit |
979 | // * createacct-continue-submit |
980 | // * createacct-another-continue-submit |
981 | 'default' => $this->msg( 'createacct-' . $anotherPart . $continuePart . |
982 | 'submit' )->text(), |
983 | 'name' => 'wpCreateaccount', |
984 | 'id' => 'wpCreateaccount', |
985 | 'weight' => 100, |
986 | ], |
987 | ]; |
988 | if ( !$this->msg( 'createacct-username-help' )->isDisabled() ) { |
989 | $fieldDefinitions['username']['help-message'] = 'createacct-username-help'; |
990 | } |
991 | } else { |
992 | // When the user's password is too weak, they might be asked to provide a stronger one |
993 | // as a followup step. That is a form with only two fields, 'password' and 'retype', |
994 | // and they should behave more like account creation. |
995 | $passwordRequest = AuthenticationRequest::getRequestByClass( $this->authRequests, |
996 | PasswordAuthenticationRequest::class ); |
997 | $changePassword = $passwordRequest && $passwordRequest->action == AuthManager::ACTION_CHANGE; |
998 | $fieldDefinitions = [ |
999 | 'username' => ( |
1000 | [ |
1001 | 'label-raw' => $this->msg( 'userlogin-yourname' )->escaped() . $secureLoginLink, |
1002 | 'id' => 'wpName1', |
1003 | 'placeholder-message' => 'userlogin-yourname-ph', |
1004 | ] + ( $changePassword ? [ |
1005 | // There is no username field on the AuthManager level when changing |
1006 | // passwords. Fake one because password |
1007 | 'baseField' => 'password', |
1008 | 'nodata' => true, |
1009 | 'readonly' => true, |
1010 | 'cssclass' => 'mw-htmlform-hidden-field', |
1011 | ] : [] ) |
1012 | ), |
1013 | 'password' => ( |
1014 | $changePassword ? [ |
1015 | 'autocomplete' => 'new-password', |
1016 | 'placeholder-message' => 'createacct-yourpassword-ph', |
1017 | 'help-message' => 'createacct-useuniquepass', |
1018 | ] : [ |
1019 | 'id' => 'wpPassword1', |
1020 | 'autocomplete' => 'current-password', |
1021 | 'placeholder-message' => 'userlogin-yourpassword-ph', |
1022 | ] |
1023 | ), |
1024 | 'retype' => [ |
1025 | 'type' => 'password', |
1026 | 'autocomplete' => 'new-password', |
1027 | 'placeholder-message' => 'createacct-yourpasswordagain-ph', |
1028 | ], |
1029 | 'domain' => [], |
1030 | 'rememberMe' => [ |
1031 | // option for saving the user token to a cookie |
1032 | 'type' => 'check', |
1033 | 'cssclass' => 'mw-userlogin-rememberme', |
1034 | 'name' => 'wpRemember', |
1035 | 'label-message' => $this->msg( 'userlogin-remembermypassword' ) |
1036 | ->numParams( $expirationDays ), |
1037 | 'id' => 'wpRemember', |
1038 | ], |
1039 | 'loginattempt' => [ |
1040 | // submit button |
1041 | 'type' => 'submit', |
1042 | // The following messages are used here: |
1043 | // * pt-login-button |
1044 | // * pt-login-continue-button |
1045 | 'default' => $this->msg( 'pt-login-' . $continuePart . 'button' )->text(), |
1046 | 'id' => 'wpLoginAttempt', |
1047 | 'weight' => 100, |
1048 | ], |
1049 | 'linkcontainer' => [ |
1050 | // help link |
1051 | 'type' => 'info', |
1052 | 'cssclass' => 'mw-form-related-link-container mw-userlogin-help', |
1053 | // 'id' => 'mw-userlogin-help', // FIXME HTMLInfoField ignores this |
1054 | 'raw' => true, |
1055 | 'default' => Html::element( 'a', [ |
1056 | 'href' => Skin::makeInternalOrExternalUrl( $this->msg( 'helplogin-url' ) |
1057 | ->inContentLanguage() |
1058 | ->text() ), |
1059 | ], $this->msg( 'userlogin-helplink2' )->text() ), |
1060 | 'weight' => 200, |
1061 | ], |
1062 | // button for ResetPasswordSecondaryAuthenticationProvider |
1063 | 'skipReset' => [ |
1064 | 'weight' => 110, |
1065 | 'flags' => [], |
1066 | ], |
1067 | ]; |
1068 | } |
1069 | |
1070 | $fieldDefinitions['username'] += [ |
1071 | 'type' => 'text', |
1072 | 'name' => 'wpName', |
1073 | 'cssclass' => 'loginText mw-userlogin-username', |
1074 | 'size' => 20, |
1075 | 'autocomplete' => 'username', |
1076 | // 'required' => true, |
1077 | ]; |
1078 | $fieldDefinitions['password'] += [ |
1079 | 'type' => 'password', |
1080 | // 'label-message' => 'userlogin-yourpassword', // would override the changepassword label |
1081 | 'name' => 'wpPassword', |
1082 | 'cssclass' => 'loginPassword mw-userlogin-password', |
1083 | 'size' => 20, |
1084 | // 'required' => true, |
1085 | ]; |
1086 | |
1087 | if ( $this->mEntryError ) { |
1088 | $defaultHtml = ''; |
1089 | if ( $this->mEntryErrorType === 'error' ) { |
1090 | $defaultHtml = Html::errorBox( $this->mEntryError ); |
1091 | } elseif ( $this->mEntryErrorType === 'warning' ) { |
1092 | $defaultHtml = Html::warningBox( $this->mEntryError ); |
1093 | } |
1094 | $fieldDefinitions['entryError'] = [ |
1095 | 'type' => 'info', |
1096 | 'default' => $defaultHtml, |
1097 | 'raw' => true, |
1098 | 'rawrow' => true, |
1099 | 'weight' => -100, |
1100 | ]; |
1101 | } |
1102 | if ( !$this->showExtraInformation() ) { |
1103 | unset( $fieldDefinitions['linkcontainer'], $fieldDefinitions['signupend'] ); |
1104 | } |
1105 | if ( $this->isSignup() && $this->showExtraInformation() ) { |
1106 | // blank signup footer for site customization |
1107 | // uses signupend-https for HTTPS requests if it's not blank, signupend otherwise |
1108 | $signupendMsg = $this->msg( 'signupend' ); |
1109 | $signupendHttpsMsg = $this->msg( 'signupend-https' ); |
1110 | if ( !$signupendMsg->isDisabled() ) { |
1111 | $usingHTTPS = $this->getRequest()->getProtocol() === 'https'; |
1112 | $signupendText = ( $usingHTTPS && !$signupendHttpsMsg->isBlank() ) |
1113 | ? $signupendHttpsMsg->parse() : $signupendMsg->parse(); |
1114 | $fieldDefinitions['signupend'] = [ |
1115 | 'type' => 'info', |
1116 | 'raw' => true, |
1117 | 'default' => Html::rawElement( 'div', [ 'id' => 'signupend' ], $signupendText ), |
1118 | 'weight' => 225, |
1119 | ]; |
1120 | } |
1121 | } |
1122 | if ( !$this->isSignup() && $this->showExtraInformation() ) { |
1123 | $passwordReset = MediaWikiServices::getInstance()->getPasswordReset(); |
1124 | if ( $passwordReset->isAllowed( $this->getUser() )->isGood() ) { |
1125 | $fieldDefinitions['passwordReset'] = [ |
1126 | 'type' => 'info', |
1127 | 'raw' => true, |
1128 | 'cssclass' => 'mw-form-related-link-container', |
1129 | 'default' => $this->getLinkRenderer()->makeLink( |
1130 | SpecialPage::getTitleFor( 'PasswordReset' ), |
1131 | $this->msg( 'userlogin-resetpassword-link' )->text() |
1132 | ), |
1133 | 'weight' => 230, |
1134 | ]; |
1135 | } |
1136 | |
1137 | // Don't show a "create account" link if the user can't. |
1138 | if ( $this->showCreateAccountLink() ) { |
1139 | // link to the other action |
1140 | $linkTitle = $this->getTitleFor( $this->isSignup() ? 'Userlogin' : 'CreateAccount' ); |
1141 | $linkq = $this->getReturnToQueryStringFragment(); |
1142 | // Pass any language selection on to the mode switch link |
1143 | if ( $this->mLanguage ) { |
1144 | $linkq .= '&uselang=' . urlencode( $this->mLanguage ); |
1145 | } |
1146 | if ( $this->mVariant ) { |
1147 | $linkq .= '&variant=' . urlencode( $this->mVariant ); |
1148 | } |
1149 | $isLoggedIn = $this->getUser()->isRegistered() |
1150 | && !$this->getUser()->isTemp(); |
1151 | |
1152 | $fieldDefinitions['createOrLogin'] = [ |
1153 | 'type' => 'info', |
1154 | 'raw' => true, |
1155 | 'linkQuery' => $linkq, |
1156 | 'default' => function ( $params ) use ( $isLoggedIn, $linkTitle ) { |
1157 | $buttonClasses = 'cdx-button cdx-button--action-progressive ' |
1158 | . 'cdx-button--fake-button cdx-button--fake-button--enabled'; |
1159 | |
1160 | return Html::rawElement( 'div', |
1161 | [ 'id' => 'mw-createaccount' . ( !$isLoggedIn ? '-cta' : '' ), |
1162 | 'class' => ( $isLoggedIn ? 'mw-form-related-link-container' : 'mw-ui-vform-field' ) ], |
1163 | ( $isLoggedIn ? '' : $this->msg( 'userlogin-noaccount' )->escaped() ) |
1164 | . Html::element( 'a', |
1165 | [ |
1166 | 'id' => 'mw-createaccount-join' . ( $isLoggedIn ? '-loggedin' : '' ), |
1167 | 'href' => $linkTitle->getLocalURL( $params['linkQuery'] ), |
1168 | 'class' => $isLoggedIn ? '' : $buttonClasses, |
1169 | 'tabindex' => 100, |
1170 | ], |
1171 | $this->msg( |
1172 | $isLoggedIn ? 'userlogin-createanother' : 'userlogin-joinproject' |
1173 | )->text() |
1174 | ) |
1175 | ); |
1176 | }, |
1177 | 'weight' => 235, |
1178 | ]; |
1179 | } |
1180 | } |
1181 | |
1182 | return $fieldDefinitions; |
1183 | } |
1184 | |
1185 | /** |
1186 | * Check if a session cookie is present. |
1187 | * |
1188 | * This will not pick up a cookie set during _this_ request, but is meant |
1189 | * to ensure that the client is returning the cookie which was set on a |
1190 | * previous pass through the system. |
1191 | * |
1192 | * @return bool |
1193 | */ |
1194 | protected function hasSessionCookie() { |
1195 | $config = $this->getConfig(); |
1196 | return $config->get( 'InitialSessionId' ) && |
1197 | $this->getRequest()->getSession()->getId() === (string)$config->get( 'InitialSessionId' ); |
1198 | } |
1199 | |
1200 | /** |
1201 | * Returns a string that can be appended to the URL (without encoding) to preserve the |
1202 | * return target. Does not include leading '?'/'&'. |
1203 | * @return string |
1204 | */ |
1205 | protected function getReturnToQueryStringFragment() { |
1206 | $returnto = ''; |
1207 | if ( $this->mReturnTo !== '' ) { |
1208 | $returnto = 'returnto=' . wfUrlencode( $this->mReturnTo ); |
1209 | if ( $this->mReturnToQuery !== '' ) { |
1210 | $returnto .= '&returntoquery=' . wfUrlencode( $this->mReturnToQuery ); |
1211 | } |
1212 | } |
1213 | return $returnto; |
1214 | } |
1215 | |
1216 | /** |
1217 | * Whether the login/create account form should display a link to the |
1218 | * other form (in addition to whatever the skin provides). |
1219 | * @return bool |
1220 | */ |
1221 | private function showCreateAccountLink() { |
1222 | return $this->isSignup() || |
1223 | $this->getContext()->getAuthority()->isAllowed( 'createaccount' ); |
1224 | } |
1225 | |
1226 | protected function getTokenName() { |
1227 | return $this->isSignup() ? 'wpCreateaccountToken' : 'wpLoginToken'; |
1228 | } |
1229 | |
1230 | /** |
1231 | * Produce a bar of links which allow the user to select another language |
1232 | * during login/registration but retain "returnto" |
1233 | * |
1234 | * @return string |
1235 | */ |
1236 | protected function makeLanguageSelector() { |
1237 | $msg = $this->msg( 'loginlanguagelinks' )->inContentLanguage(); |
1238 | if ( $msg->isBlank() ) { |
1239 | return ''; |
1240 | } |
1241 | $langs = explode( "\n", $msg->text() ); |
1242 | $links = []; |
1243 | foreach ( $langs as $lang ) { |
1244 | $lang = trim( $lang, '* ' ); |
1245 | $parts = explode( '|', $lang ); |
1246 | if ( count( $parts ) >= 2 ) { |
1247 | $links[] = $this->makeLanguageSelectorLink( $parts[0], trim( $parts[1] ) ); |
1248 | } |
1249 | } |
1250 | |
1251 | return count( $links ) > 0 ? $this->msg( 'loginlanguagelabel' )->rawParams( |
1252 | $this->getLanguage()->pipeList( $links ) )->escaped() : ''; |
1253 | } |
1254 | |
1255 | /** |
1256 | * Create a language selector link for a particular language |
1257 | * Links back to this page preserving type and returnto |
1258 | * |
1259 | * @param string $text Link text |
1260 | * @param string $lang Language code |
1261 | * @return string |
1262 | */ |
1263 | protected function makeLanguageSelectorLink( $text, $lang ) { |
1264 | if ( $this->getLanguage()->getCode() == $lang ) { |
1265 | // no link for currently used language |
1266 | return htmlspecialchars( $text ); |
1267 | } |
1268 | $query = [ 'uselang' => $lang ]; |
1269 | if ( $this->mVariant ) { |
1270 | $query['variant'] = $this->mVariant; |
1271 | } |
1272 | if ( $this->mReturnTo !== '' ) { |
1273 | $query['returnto'] = $this->mReturnTo; |
1274 | $query['returntoquery'] = $this->mReturnToQuery; |
1275 | } |
1276 | |
1277 | $attr = []; |
1278 | $targetLanguage = MediaWikiServices::getInstance()->getLanguageFactory() |
1279 | ->getLanguage( $lang ); |
1280 | $attr['lang'] = $attr['hreflang'] = $targetLanguage->getHtmlCode(); |
1281 | |
1282 | return $this->getLinkRenderer()->makeKnownLink( |
1283 | $this->getPageTitle(), |
1284 | $text, |
1285 | $attr, |
1286 | $query |
1287 | ); |
1288 | } |
1289 | |
1290 | protected function getGroupName() { |
1291 | return 'login'; |
1292 | } |
1293 | |
1294 | /** |
1295 | * @param array &$formDescriptor |
1296 | * @param array $requests |
1297 | */ |
1298 | protected function postProcessFormDescriptor( &$formDescriptor, $requests ) { |
1299 | // Pre-fill username (if not creating an account, T46775). |
1300 | if ( |
1301 | isset( $formDescriptor['username'] ) && |
1302 | !isset( $formDescriptor['username']['default'] ) && |
1303 | !$this->isSignup() |
1304 | ) { |
1305 | $user = $this->getUser(); |
1306 | if ( $user->isRegistered() && !$user->isTemp() ) { |
1307 | $formDescriptor['username']['default'] = $user->getName(); |
1308 | } else { |
1309 | $formDescriptor['username']['default'] = |
1310 | $this->getRequest()->getSession()->suggestLoginUsername(); |
1311 | } |
1312 | } |
1313 | |
1314 | // don't show a submit button if there is nothing to submit (i.e. the only form content |
1315 | // is other submit buttons, for redirect flows) |
1316 | if ( !$this->needsSubmitButton( $requests ) ) { |
1317 | unset( $formDescriptor['createaccount'], $formDescriptor['loginattempt'] ); |
1318 | } |
1319 | |
1320 | if ( !$this->isSignup() ) { |
1321 | // FIXME HACK don't focus on non-empty field |
1322 | // maybe there should be an autofocus-if similar to hide-if? |
1323 | if ( |
1324 | isset( $formDescriptor['username'] ) |
1325 | && empty( $formDescriptor['username']['default'] ) |
1326 | && !$this->getRequest()->getCheck( 'wpName' ) |
1327 | ) { |
1328 | $formDescriptor['username']['autofocus'] = true; |
1329 | } elseif ( isset( $formDescriptor['password'] ) ) { |
1330 | $formDescriptor['password']['autofocus'] = true; |
1331 | } |
1332 | } |
1333 | |
1334 | $this->addTabIndex( $formDescriptor ); |
1335 | } |
1336 | |
1337 | /** |
1338 | * Generates the HTML for a notice box to be displayed to a temporary user. |
1339 | * |
1340 | * @return string HTML representing the notice box |
1341 | */ |
1342 | protected function getNoticeHtml() { |
1343 | $noticeContent = $this->msg( 'createacct-temp-warning', $this->getUser()->getName() )->parse(); |
1344 | return Html::noticeBox( |
1345 | $noticeContent, |
1346 | '', |
1347 | '', |
1348 | 'mw-userLogin-icon--user-temporary' |
1349 | ); |
1350 | } |
1351 | |
1352 | } |
1353 | |
1354 | /** @deprecated class alias since 1.41 */ |
1355 | class_alias( LoginSignupSpecialPage::class, 'LoginSignupSpecialPage' ); |