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