Completed
Branch master (2b1da7)
by
unknown
23:13
created

LoginSignupSpecialPage::getAuthForm()   C

Complexity

Conditions 9
Paths 25

Size

Total Lines 53
Code Lines 31

Duplication

Lines 5
Ratio 9.43 %

Importance

Changes 0
Metric Value
cc 9
eloc 31
nc 25
nop 4
dl 5
loc 53
rs 6.8963
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
use MediaWiki\Auth\AuthenticationRequest;
25
use MediaWiki\Auth\AuthenticationResponse;
26
use MediaWiki\Auth\AuthManager;
27
use MediaWiki\Auth\Throttler;
28
use MediaWiki\Logger\LoggerFactory;
29
use MediaWiki\Session\SessionManager;
30
31
/**
32
 * Holds shared logic for login and account creation pages.
33
 *
34
 * @ingroup SpecialPage
35
 */
36
abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
37
	protected $mReturnTo;
38
	protected $mPosted;
39
	protected $mAction;
40
	protected $mLanguage;
41
	protected $mReturnToQuery;
42
	protected $mToken;
43
	protected $mStickHTTPS;
44
	protected $mFromHTTP;
45
	protected $mEntryError = '';
46
	protected $mEntryErrorType = 'error';
47
48
	protected $mLoaded = false;
49
	protected $mLoadedRequest = false;
50
	protected $mSecureLoginUrl;
51
52
	/** @var string */
53
	protected $securityLevel;
54
55
	/** @var bool True if the user if creating an account for someone else. Flag used for internal
56
	 * communication, only set at the very end. */
57
	protected $proxyAccountCreation;
58
	/** @var User FIXME another flag for passing data. */
59
	protected $targetUser;
60
61
	/** @var HTMLForm */
62
	protected $authForm;
63
64
	/** @var FakeAuthTemplate */
65
	protected $fakeTemplate;
66
67
	abstract protected function isSignup();
68
69
	/**
70
	 * @param bool $direct True if the action was successful just now; false if that happened
71
	 *    pre-redirection (so this handler was called already)
72
	 * @param StatusValue|null $extraMessages
73
	 * @return void
74
	 */
75
	abstract protected function successfulAction( $direct = false, $extraMessages = null );
76
77
	/**
78
	 * Logs to the authmanager-stats channel.
79
	 * @param bool $success
80
	 * @param string|null $status Error message key
81
	 */
82
	abstract protected function logAuthResult( $success, $status = null );
83
84
	public function __construct( $name ) {
85
		global $wgUseMediaWikiUIEverywhere;
86
		parent::__construct( $name );
87
88
		// Override UseMediaWikiEverywhere to true, to force login and create form to use mw ui
89
		$wgUseMediaWikiUIEverywhere = true;
90
	}
91
92
	protected function setRequest( array $data, $wasPosted = null ) {
93
		parent::setRequest( $data, $wasPosted );
94
		$this->mLoadedRequest = false;
95
	}
96
97
	/**
98
	 * Load basic request parameters for this Special page.
99
	 * @param $subPage
100
	 */
101
	private function loadRequestParameters( $subPage ) {
102
		if ( $this->mLoadedRequest ) {
103
			return;
104
		}
105
		$this->mLoadedRequest = true;
106
		$request = $this->getRequest();
107
108
		$this->mPosted = $request->wasPosted();
109
		$this->mIsReturn = $subPage === 'return';
0 ignored issues
show
Bug introduced by
The property mIsReturn does not seem to exist. Did you mean isReturn?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
110
		$this->mAction = $request->getVal( 'action' );
111
		$this->mFromHTTP = $request->getBool( 'fromhttp', false )
112
			|| $request->getBool( 'wpFromhttp', false );
113
		$this->mStickHTTPS = ( !$this->mFromHTTP && $request->getProtocol() === 'https' )
114
			|| $request->getBool( 'wpForceHttps', false );
115
		$this->mLanguage = $request->getText( 'uselang' );
116
		$this->mReturnTo = $request->getVal( 'returnto', '' );
117
		$this->mReturnToQuery = $request->getVal( 'returntoquery', '' );
118
	}
119
120
	/**
121
	 * Load data from request.
122
	 * @private
123
	 * @param string $subPage Subpage of Special:Userlogin
124
	 */
125
	protected function load( $subPage ) {
126
		global $wgSecureLogin;
127
128
		$this->loadRequestParameters( $subPage );
129
		if ( $this->mLoaded ) {
130
			return;
131
		}
132
		$this->mLoaded = true;
133
		$request = $this->getRequest();
134
135
		$securityLevel = $this->getRequest()->getText( 'force' );
136
		if (
137
			$securityLevel && AuthManager::singleton()->securitySensitiveOperationStatus(
138
				$securityLevel ) === AuthManager::SEC_REAUTH
139
		) {
140
			$this->securityLevel = $securityLevel;
141
		}
142
143
		$this->loadAuth( $subPage );
144
145
		$this->mToken = $request->getVal( $this->getTokenName() );
146
147
		// Show an error or warning passed on from a previous page
148
		$entryError = $this->msg( $request->getVal( 'error', '' ) );
149
		$entryWarning = $this->msg( $request->getVal( 'warning', '' ) );
150
		// bc: provide login link as a parameter for messages where the translation
151
		// was not updated
152
		$loginreqlink = Linker::linkKnown(
153
			$this->getPageTitle(),
154
			$this->msg( 'loginreqlink' )->escaped(),
155
			[],
156
			[
157
				'returnto' => $this->mReturnTo,
158
				'returntoquery' => $this->mReturnToQuery,
159
				'uselang' => $this->mLanguage ?: null,
160
				'fromhttp' => $wgSecureLogin && $this->mFromHTTP ? '1' : null,
161
			]
162
		);
163
164
		// Only show valid error or warning messages.
165
		if ( $entryError->exists()
166
			&& in_array( $entryError->getKey(), LoginHelper::getValidErrorMessages(), true )
167
		) {
168
			$this->mEntryErrorType = 'error';
169
			$this->mEntryError = $entryError->rawParams( $loginreqlink )->parse();
170
171
		} elseif ( $entryWarning->exists()
172
			&& in_array( $entryWarning->getKey(), LoginHelper::getValidErrorMessages(), true )
173
		) {
174
			$this->mEntryErrorType = 'warning';
175
			$this->mEntryError = $entryWarning->rawParams( $loginreqlink )->parse();
176
		}
177
178
		# 1. When switching accounts, it sucks to get automatically logged out
179
		# 2. Do not return to PasswordReset after a successful password change
180
		#    but goto Wiki start page (Main_Page) instead ( bug 33997 )
181
		$returnToTitle = Title::newFromText( $this->mReturnTo );
182
		if ( is_object( $returnToTitle )
183
			&& ( $returnToTitle->isSpecial( 'Userlogout' )
184
				|| $returnToTitle->isSpecial( 'PasswordReset' ) )
185
		) {
186
			$this->mReturnTo = '';
187
			$this->mReturnToQuery = '';
188
		}
189
	}
190
191
	protected function getPreservedParams( $withToken = false ) {
192
		global $wgSecureLogin;
193
194
		$params = parent::getPreservedParams( $withToken );
195
		$params += [
196
			'returnto' => $this->mReturnTo ?: null,
197
			'returntoquery' => $this->mReturnToQuery ?: null,
198
		];
199
		if ( $wgSecureLogin && !$this->isSignup() ) {
200
			$params['fromhttp'] = $this->mFromHTTP ? '1' : null;
201
		}
202
		return $params;
203
	}
204
205
	protected function beforeExecute( $subPage ) {
206
		// finish initializing the class before processing the request - T135924
207
		$this->loadRequestParameters( $subPage );
208
		return parent::beforeExecute( $subPage );
209
	}
210
211
	/**
212
	 * @param string|null $subPage
213
	 */
214
	public function execute( $subPage ) {
215
		$authManager = AuthManager::singleton();
216
		$session = SessionManager::getGlobalSession();
217
218
		// Session data is used for various things in the authentication process, so we must make
219
		// sure a session cookie or some equivalent mechanism is set.
220
		$session->persist();
221
222
		$this->load( $subPage );
223
		$this->setHeaders();
224
		$this->checkPermissions();
225
226
		// Make sure the system configuration allows log in / sign up
227
		if ( !$this->isSignup() && !$authManager->canAuthenticateNow() ) {
228 View Code Duplication
			if ( !$session->canSetUser() ) {
229
				throw new ErrorPageError( 'cannotloginnow-title', 'cannotloginnow-text', [
230
					$session->getProvider()->describe( RequestContext::getMain()->getLanguage() )
231
				] );
232
			}
233
			throw new ErrorPageError( 'cannotlogin-title', 'cannotlogin-text' );
234
		} elseif ( $this->isSignup() && !$authManager->canCreateAccounts() ) {
235
			throw new ErrorPageError( 'cannotcreateaccount-title', 'cannotcreateaccount-text' );
236
		}
237
238
		/*
239
		 * In the case where the user is already logged in, and was redirected to
240
		 * the login form from a page that requires login, do not show the login
241
		 * page. The use case scenario for this is when a user opens a large number
242
		 * of tabs, is redirected to the login page on all of them, and then logs
243
		 * in on one, expecting all the others to work properly.
244
		 *
245
		 * However, do show the form if it was visited intentionally (no 'returnto'
246
		 * is present). People who often switch between several accounts have grown
247
		 * accustomed to this behavior.
248
		 *
249
		 * Also make an exception when force=<level> is set in the URL, which means the user must
250
		 * reauthenticate for security reasons.
251
		 */
252
		if ( !$this->isSignup() && !$this->mPosted && !$this->securityLevel &&
253
			 ( $this->mReturnTo !== '' || $this->mReturnToQuery !== '' ) &&
254
			 $this->getUser()->isLoggedIn()
255
		) {
256
			$this->successfulAction();
257
		}
258
259
		// If logging in and not on HTTPS, either redirect to it or offer a link.
260
		global $wgSecureLogin;
261
		if ( $this->getRequest()->getProtocol() !== 'https' ) {
262
			$title = $this->getFullTitle();
263
			$query = $this->getPreservedParams( false ) + [
264
					'title' => null,
265
					( $this->mEntryErrorType === 'error' ? 'error'
266
						: 'warning' ) => $this->mEntryError,
267
				] + $this->getRequest()->getQueryValues();
268
			$url = $title->getFullURL( $query, false, PROTO_HTTPS );
269
			if ( $wgSecureLogin && !$this->mFromHTTP &&
270
				 wfCanIPUseHTTPS( $this->getRequest()->getIP() )
271
			) {
272
				// Avoid infinite redirect
273
				$url = wfAppendQuery( $url, 'fromhttp=1' );
274
				$this->getOutput()->redirect( $url );
275
				// Since we only do this redir to change proto, always vary
276
				$this->getOutput()->addVaryHeader( 'X-Forwarded-Proto' );
277
278
				return;
279
			} else {
280
				// A wiki without HTTPS login support should set $wgServer to
281
				// http://somehost, in which case the secure URL generated
282
				// above won't actually start with https://
283
				if ( substr( $url, 0, 8 ) === 'https://' ) {
284
					$this->mSecureLoginUrl = $url;
285
				}
286
			}
287
		}
288
289
		if ( !$this->isActionAllowed( $this->authAction ) ) {
290
			// FIXME how do we explain this to the user? can we handle session loss better?
291
			// messages used: authpage-cannot-login, authpage-cannot-login-continue,
292
			// authpage-cannot-create, authpage-cannot-create-continue
293
			$this->mainLoginForm( [], 'authpage-cannot-' . $this->authAction );
294
			return;
295
		}
296
297
		$status = $this->trySubmit();
298
299
		if ( !$status || !$status->isGood() ) {
300
			$this->mainLoginForm( $this->authRequests, $status ? $status->getMessage() : '', 'error' );
301
			return;
302
		}
303
304
		/** @var AuthenticationResponse $response */
305
		$response = $status->getValue();
306
307
		$returnToUrl = $this->getPageTitle( 'return' )
308
			->getFullURL( $this->getPreservedParams( true ), false, PROTO_HTTPS );
309
		switch ( $response->status ) {
310
			case AuthenticationResponse::PASS:
311
				$this->logAuthResult( true );
312
				$this->proxyAccountCreation = $this->isSignup() && !$this->getUser()->isAnon();
313
				$this->targetUser = User::newFromName( $response->username );
0 ignored issues
show
Documentation Bug introduced by
It seems like \User::newFromName($response->username) can also be of type false. However, the property $targetUser is declared as type object<User>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
314
315
				if (
316
					!$this->proxyAccountCreation
317
					&& $response->loginRequest
318
					&& $authManager->canAuthenticateNow()
319
				) {
320
					// successful registration; log the user in instantly
321
					$response2 = $authManager->beginAuthentication( [ $response->loginRequest ],
322
						$returnToUrl );
323
					if ( $response2->status !== AuthenticationResponse::PASS ) {
324
						LoggerFactory::getInstance( 'login' )
325
							->error( 'Could not log in after account creation' );
326
						$this->successfulAction( true, Status::newFatal( 'createacct-loginerror' ) );
327
						break;
328
					}
329
				}
330
331
				if ( !$this->proxyAccountCreation ) {
332
					// Ensure that the context user is the same as the session user.
333
					$this->setSessionUserForCurrentRequest();
334
				}
335
336
				$this->successfulAction( true );
337
				break;
338
			case AuthenticationResponse::FAIL:
339
				// fall through
340
			case AuthenticationResponse::RESTART:
341
				unset( $this->authForm );
342
				if ( $response->status === AuthenticationResponse::FAIL ) {
343
					$action = $this->getDefaultAction( $subPage );
344
					$messageType = 'error';
345
				} else {
346
					$action = $this->getContinueAction( $this->authAction );
347
					$messageType = 'warning';
348
				}
349
				$this->logAuthResult( false, $response->message ? $response->message->getKey() : '-' );
350
				$this->loadAuth( $subPage, $action, true );
351
				$this->mainLoginForm( $this->authRequests, $response->message, $messageType );
0 ignored issues
show
Bug introduced by
It seems like $response->message can be null; however, mainLoginForm() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
352
				break;
353
			case AuthenticationResponse::REDIRECT:
354
				unset( $this->authForm );
355
				$this->getOutput()->redirect( $response->redirectTarget );
356
				break;
357
			case AuthenticationResponse::UI:
358
				unset( $this->authForm );
359
				$this->authAction = $this->isSignup() ? AuthManager::ACTION_CREATE_CONTINUE
360
					: AuthManager::ACTION_LOGIN_CONTINUE;
361
				$this->authRequests = $response->neededRequests;
362
				$this->mainLoginForm( $response->neededRequests, $response->message, $response->messageType );
0 ignored issues
show
Bug introduced by
It seems like $response->message can be null; however, mainLoginForm() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
363
				break;
364
			default:
365
				throw new LogicException( 'invalid AuthenticationResponse' );
366
		}
367
	}
368
369
	/**
370
	 * Show the success page.
371
	 *
372
	 * @param string $type Condition of return to; see `executeReturnTo`
373
	 * @param string|Message $title Page's title
374
	 * @param string $msgname
375
	 * @param string $injected_html
376
	 * @param StatusValue|null $extraMessages
377
	 */
378
	protected function showSuccessPage(
379
		$type, $title, $msgname, $injected_html, $extraMessages
380
	) {
381
		$out = $this->getOutput();
382
		$out->setPageTitle( $title );
383
		if ( $msgname ) {
384
			$out->addWikiMsg( $msgname, wfEscapeWikiText( $this->getUser()->getName() ) );
385
		}
386
		if ( $extraMessages ) {
387
			$extraMessages = Status::wrap( $extraMessages );
388
			$out->addWikiText( $extraMessages->getWikiText() );
389
		}
390
391
		$out->addHTML( $injected_html );
392
393
		$helper = new LoginHelper( $this->getContext() );
394
		$helper->showReturnToPage( $type, $this->mReturnTo, $this->mReturnToQuery, $this->mStickHTTPS );
395
	}
396
397
	/**
398
	 * Add a "return to" link or redirect to it.
399
	 * Extensions can use this to reuse the "return to" logic after
400
	 * inject steps (such as redirection) into the login process.
401
	 *
402
	 * @param string $type One of the following:
403
	 *    - error: display a return to link ignoring $wgRedirectOnLogin
404
	 *    - signup: display a return to link using $wgRedirectOnLogin if needed
405
	 *    - success: display a return to link using $wgRedirectOnLogin if needed
406
	 *    - successredirect: send an HTTP redirect using $wgRedirectOnLogin if needed
407
	 * @param string $returnTo
408
	 * @param array|string $returnToQuery
409
	 * @param bool $stickHTTPS Keep redirect link on HTTPS
410
	 * @since 1.22
411
	 */
412
	public function showReturnToPage(
413
		$type, $returnTo = '', $returnToQuery = '', $stickHTTPS = false
414
	) {
415
		$helper = new LoginHelper( $this->getContext() );
416
		$helper->showReturnToPage( $type, $returnTo, $returnToQuery, $stickHTTPS );
417
	}
418
419
	/**
420
	 * Replace some globals to make sure the fact that the user has just been logged in is
421
	 * reflected in the current request.
422
	 * @param User $user
0 ignored issues
show
Bug introduced by
There is no parameter named $user. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
423
	 */
424
	protected function setSessionUserForCurrentRequest() {
425
		global $wgUser, $wgLang;
426
427
		$context = RequestContext::getMain();
428
		$localContext = $this->getContext();
429
		if ( $context !== $localContext ) {
430
			// remove AuthManagerSpecialPage context hack
431
			$this->setContext( $context );
432
		}
433
434
		$user = $context->getRequest()->getSession()->getUser();
435
436
		$wgUser = $user;
437
		$context->setUser( $user );
438
439
		$code = $this->getRequest()->getVal( 'uselang', $user->getOption( 'language' ) );
440
		$userLang = Language::factory( $code );
441
		$wgLang = $userLang;
442
		$context->setLanguage( $userLang );
443
	}
444
445
	/**
446
	 * @param AuthenticationRequest[] $requests A list of AuthorizationRequest objects,
447
	 *   used to generate the form fields. An empty array means a fatal error
448
	 *   (authentication cannot continue).
449
	 * @param string|Message $msg
450
	 * @param string $msgtype
451
	 * @throws ErrorPageError
452
	 * @throws Exception
453
	 * @throws FatalError
454
	 * @throws MWException
455
	 * @throws PermissionsError
456
	 * @throws ReadOnlyError
457
	 * @private
458
	 */
459
	protected function mainLoginForm( array $requests, $msg = '', $msgtype = 'error' ) {
460
		$titleObj = $this->getPageTitle();
0 ignored issues
show
Unused Code introduced by
$titleObj is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
461
		$user = $this->getUser();
462
		$out = $this->getOutput();
463
464
		// FIXME how to handle empty $requests - restart, or no form, just an error message?
465
		// no form would be better for no session type errors, restart is better when can* fails.
466
		if ( !$requests ) {
467
			$this->authAction = $this->getDefaultAction( $this->subPage );
468
			$this->authForm = null;
469
			$requests = AuthManager::singleton()->getAuthenticationRequests( $this->authAction, $user );
470
		}
471
472
		// Generic styles and scripts for both login and signup form
473
		$out->addModuleStyles( [
474
			'mediawiki.ui',
475
			'mediawiki.ui.button',
476
			'mediawiki.ui.checkbox',
477
			'mediawiki.ui.input',
478
			'mediawiki.special.userlogin.common.styles'
479
		] );
480
		if ( $this->isSignup() ) {
481
			// XXX hack pending RL or JS parse() support for complex content messages T27349
482
			$out->addJsConfigVars( 'wgCreateacctImgcaptchaHelp',
483
				$this->msg( 'createacct-imgcaptcha-help' )->parse() );
484
485
			// Additional styles and scripts for signup form
486
			$out->addModules( [
487
				'mediawiki.special.userlogin.signup.js'
488
			] );
489
			$out->addModuleStyles( [
490
				'mediawiki.special.userlogin.signup.styles'
491
			] );
492
		} else {
493
			// Additional styles for login form
494
			$out->addModuleStyles( [
495
				'mediawiki.special.userlogin.login.styles'
496
			] );
497
		}
498
		$out->disallowUserJs(); // just in case...
499
500
		$form = $this->getAuthForm( $requests, $this->authAction, $msg, $msgtype );
501
		$form->prepareForm();
502
503
		$submitStatus = Status::newGood();
504
		if ( $msg && $msgtype === 'warning' ) {
505
			$submitStatus->warning( $msg );
506
		} elseif ( $msg && $msgtype === 'error' ) {
507
			$submitStatus->fatal( $msg );
508
		}
509
510
		// warning header for non-standard workflows (e.g. security reauthentication)
511
		if (
512
			!$this->isSignup() &&
513
			$this->getUser()->isLoggedIn() &&
514
			$this->authAction !== AuthManager::ACTION_LOGIN_CONTINUE
515
		) {
516
			$reauthMessage = $this->securityLevel ? 'userlogin-reauth' : 'userlogin-loggedin';
517
			$submitStatus->warning( $reauthMessage, $this->getUser()->getName() );
518
		}
519
520
		$formHtml = $form->getHTML( $submitStatus );
521
522
		$out->addHTML( $this->getPageHtml( $formHtml ) );
523
	}
524
525
	/**
526
	 * Add page elements which are outside the form.
527
	 * FIXME this should probably be a template, but use a sane language (handlebars?)
528
	 * @param string $formHtml
529
	 * @return string
530
	 */
531
	protected function getPageHtml( $formHtml ) {
532
		global $wgLoginLanguageSelector;
533
534
		$loginPrompt = $this->isSignup() ? '' : Html::rawElement( 'div',
535
			[ 'id' => 'userloginprompt' ], $this->msg( 'loginprompt' )->parseAsBlock() );
536
		$languageLinks = $wgLoginLanguageSelector ? $this->makeLanguageSelector() : '';
537
		$signupStartMsg = $this->msg( 'signupstart' );
538
		$signupStart = ( $this->isSignup() && !$signupStartMsg->isDisabled() )
539
			? Html::rawElement( 'div', [ 'id' => 'signupstart' ], $signupStartMsg->parseAsBlock() ) : '';
540
		if ( $languageLinks ) {
541
			$languageLinks = Html::rawElement( 'div', [ 'id' => 'languagelinks' ],
542
				Html::rawElement( 'p', [], $languageLinks )
543
			);
544
		}
545
546
		$benefitsContainer = '';
547
		if ( $this->isSignup() && $this->showExtraInformation() ) {
548
			// messages used:
549
			// createacct-benefit-icon1 createacct-benefit-head1 createacct-benefit-body1
550
			// createacct-benefit-icon2 createacct-benefit-head2 createacct-benefit-body2
551
			// createacct-benefit-icon3 createacct-benefit-head3 createacct-benefit-body3
552
			$benefitCount = 3;
553
			$benefitList = '';
554
			for ( $benefitIdx = 1; $benefitIdx <= $benefitCount; $benefitIdx++ ) {
555
				$headUnescaped = $this->msg( "createacct-benefit-head$benefitIdx" )->text();
556
				$iconClass = $this->msg( "createacct-benefit-icon$benefitIdx" )->escaped();
557
				$benefitList .= Html::rawElement( 'div', [ 'class' => "mw-number-text $iconClass" ],
558
					Html::rawElement( 'h3', [],
559
						$this->msg( "createacct-benefit-head$benefitIdx" )->escaped()
560
					)
561
					. Html::rawElement( 'p', [],
562
						$this->msg( "createacct-benefit-body$benefitIdx" )->params( $headUnescaped )->escaped()
563
					)
564
				);
565
			}
566
			$benefitsContainer = Html::rawElement( 'div', [ 'class' => 'mw-createacct-benefits-container' ],
567
				Html::rawElement( 'h2', [], $this->msg( 'createacct-benefit-heading' )->escaped() )
568
				. Html::rawElement( 'div', [ 'class' => 'mw-createacct-benefits-list' ],
569
					$benefitList
570
				)
571
			);
572
		}
573
574
		$html = Html::rawElement( 'div', [ 'class' => 'mw-ui-container' ],
575
			$loginPrompt
576
			. $languageLinks
577
			. $signupStart
578
			. Html::rawElement( 'div', [ 'id' => 'userloginForm' ],
579
				$formHtml
580
			)
581
			. $benefitsContainer
582
		);
583
584
		return $html;
585
	}
586
587
	/**
588
	 * Generates a form from the given request.
589
	 * @param AuthenticationRequest[] $requests
590
	 * @param string $action AuthManager action name
591
	 * @param string|Message $msg
592
	 * @param string $msgType
593
	 * @return HTMLForm
594
	 */
595
	protected function getAuthForm( array $requests, $action, $msg = '', $msgType = 'error' ) {
596
		global $wgSecureLogin, $wgLoginLanguageSelector;
597
		// FIXME merge this with parent
598
599
		if ( isset( $this->authForm ) ) {
600
			return $this->authForm;
601
		}
602
603
		$usingHTTPS = $this->getRequest()->getProtocol() === 'https';
604
605
		// get basic form description from the auth logic
606
		$fieldInfo = AuthenticationRequest::mergeFieldInfo( $requests );
607
		$fakeTemplate = $this->getFakeTemplate( $msg, $msgType );
608
		$this->fakeTemplate = $fakeTemplate; // FIXME there should be a saner way to pass this to the hook
609
		// this will call onAuthChangeFormFields()
610
		$formDescriptor = static::fieldInfoToFormDescriptor( $requests, $fieldInfo, $this->authAction );
611
		$this->postProcessFormDescriptor( $formDescriptor, $requests );
612
613
		$context = $this->getContext();
614 View Code Duplication
		if ( $context->getRequest() !== $this->getRequest() ) {
615
			// We have overridden the request, need to make sure the form uses that too.
616
			$context = new DerivativeContext( $this->getContext() );
617
			$context->setRequest( $this->getRequest() );
618
		}
619
		$form = HTMLForm::factory( 'vform', $formDescriptor, $context );
620
621
		$form->addHiddenField( 'authAction', $this->authAction );
622
		if ( $wgLoginLanguageSelector && $this->mLanguage ) {
623
			$form->addHiddenField( 'uselang', $this->mLanguage );
624
		}
625
		$form->addHiddenField( 'force', $this->securityLevel );
626
		$form->addHiddenField( $this->getTokenName(), $this->getToken()->toString() );
627
		if ( $wgSecureLogin ) {
628
			// If using HTTPS coming from HTTP, then the 'fromhttp' parameter must be preserved
629
			if ( !$this->isSignup() ) {
630
				$form->addHiddenField( 'wpForceHttps', (int)$this->mStickHTTPS );
631
				$form->addHiddenField( 'wpFromhttp', $usingHTTPS );
632
			}
633
		}
634
635
		// set properties of the form itself
636
		$form->setAction( $this->getPageTitle()->getLocalURL( $this->getReturnToQueryStringFragment() ) );
637
		$form->setName( 'userlogin' . ( $this->isSignup() ? '2' : '' ) );
638
		if ( $this->isSignup() ) {
639
			$form->setId( 'userlogin2' );
640
		}
641
642
		$form->suppressDefaultSubmit();
643
644
		$this->authForm = $form;
645
646
		return $form;
647
	}
648
649
	/**
650
	 * Temporary B/C method to handle extensions using the UserLoginForm/UserCreateForm hooks.
651
	 * @param string|Message $msg
652
	 * @param string $msgType
653
	 * @return FakeAuthTemplate
654
	 */
655
	protected function getFakeTemplate( $msg, $msgType ) {
656
		global $wgAuth, $wgEnableEmail, $wgHiddenPrefs, $wgEmailConfirmToEdit, $wgEnableUserEmail,
657
			   $wgSecureLogin, $wgLoginLanguageSelector, $wgPasswordResetRoutes;
658
659
		// make a best effort to get the value of fields which used to be fixed in the old login
660
		// template but now might or might not exist depending on what providers are used
661
		$request = $this->getRequest();
662
		$data = (object)[
663
			'mUsername' => $request->getText( 'wpName' ),
664
			'mPassword' => $request->getText( 'wpPassword' ),
665
			'mRetype' => $request->getText( 'wpRetype' ),
666
			'mEmail' => $request->getText( 'wpEmail' ),
667
			'mRealName' => $request->getText( 'wpRealName' ),
668
			'mDomain' => $request->getText( 'wpDomain' ),
669
			'mReason' => $request->getText( 'wpReason' ),
670
			'mRemember' => $request->getCheck( 'wpRemember' ),
671
		];
672
673
		// Preserves a bunch of logic from the old code that was rewritten in getAuthForm().
674
		// There is no code reuse to make this easier to remove .
675
		// If an extension tries to change any of these values, they are out of luck - we only
676
		// actually use the domain/usedomain/domainnames, extraInput and extrafields keys.
677
678
		$titleObj = $this->getPageTitle();
679
		$user = $this->getUser();
680
		$template = new FakeAuthTemplate();
681
682
		// Pre-fill username (if not creating an account, bug 44775).
683
		if ( $data->mUsername == '' && $this->isSignup() ) {
684
			if ( $user->isLoggedIn() ) {
685
				$data->mUsername = $user->getName();
686
			} else {
687
				$data->mUsername = $this->getRequest()->getSession()->suggestLoginUsername();
688
			}
689
		}
690
691
		if ( $this->isSignup() ) {
692
			// Must match number of benefits defined in messages
693
			$template->set( 'benefitCount', 3 );
694
695
			$q = 'action=submitlogin&type=signup';
696
			$linkq = 'type=login';
697
		} else {
698
			$q = 'action=submitlogin&type=login';
699
			$linkq = 'type=signup';
700
		}
701
702
		if ( $this->mReturnTo !== '' ) {
703
			$returnto = '&returnto=' . wfUrlencode( $this->mReturnTo );
704
			if ( $this->mReturnToQuery !== '' ) {
705
				$returnto .= '&returntoquery=' .
706
							 wfUrlencode( $this->mReturnToQuery );
707
			}
708
			$q .= $returnto;
709
			$linkq .= $returnto;
710
		}
711
712
		# Don't show a "create account" link if the user can't.
713
		if ( $this->showCreateAccountLink() ) {
714
			# Pass any language selection on to the mode switch link
715
			if ( $wgLoginLanguageSelector && $this->mLanguage ) {
716
				$linkq .= '&uselang=' . $this->mLanguage;
717
			}
718
			// Supply URL, login template creates the button.
719
			$template->set( 'createOrLoginHref', $titleObj->getLocalURL( $linkq ) );
720
		} else {
721
			$template->set( 'link', '' );
722
		}
723
724
		$resetLink = $this->isSignup()
725
			? null
726
			: is_array( $wgPasswordResetRoutes )
727
			  && in_array( true, array_values( $wgPasswordResetRoutes ), true );
728
729
		$template->set( 'header', '' );
730
		$template->set( 'formheader', '' );
731
		$template->set( 'skin', $this->getSkin() );
732
733
		$template->set( 'name', $data->mUsername );
734
		$template->set( 'password', $data->mPassword );
735
		$template->set( 'retype', $data->mRetype );
736
		$template->set( 'createemailset', false ); // no easy way to get that from AuthManager
737
		$template->set( 'email', $data->mEmail );
738
		$template->set( 'realname', $data->mRealName );
739
		$template->set( 'domain', $data->mDomain );
740
		$template->set( 'reason', $data->mReason );
741
		$template->set( 'remember', $data->mRemember );
742
743
		$template->set( 'action', $titleObj->getLocalURL( $q ) );
744
		$template->set( 'message', $msg );
745
		$template->set( 'messagetype', $msgType );
746
		$template->set( 'createemail', $wgEnableEmail && $user->isLoggedIn() );
747
		$template->set( 'userealname', !in_array( 'realname', $wgHiddenPrefs, true ) );
748
		$template->set( 'useemail', $wgEnableEmail );
749
		$template->set( 'emailrequired', $wgEmailConfirmToEdit );
750
		$template->set( 'emailothers', $wgEnableUserEmail );
751
		$template->set( 'canreset', $wgAuth->allowPasswordChange() );
752
		$template->set( 'resetlink', $resetLink );
753
		$template->set( 'canremember', $request->getSession()->getProvider()
754
			->getRememberUserDuration() !== null );
755
		$template->set( 'usereason', $user->isLoggedIn() );
756
		$template->set( 'cansecurelogin', ( $wgSecureLogin ) );
757
		$template->set( 'stickhttps', (int)$this->mStickHTTPS );
758
		$template->set( 'loggedin', $user->isLoggedIn() );
759
		$template->set( 'loggedinuser', $user->getName() );
760
		$template->set( 'token', $this->getToken()->toString() );
761
762
		$action = $this->isSignup() ? 'signup' : 'login';
763
		$wgAuth->modifyUITemplate( $template, $action );
764
765
		$oldTemplate = $template;
766
767
		// Both Hooks::run are explicit here to make findHooks.php happy
768
		if ( $this->isSignup() ) {
769
			Hooks::run( 'UserCreateForm', [ &$template ] );
770
			if ( $oldTemplate !== $template ) {
771
				wfDeprecated( "reference in UserCreateForm hook", '1.27' );
772
			}
773
		} else {
774
			Hooks::run( 'UserLoginForm', [ &$template ] );
775
			if ( $oldTemplate !== $template ) {
776
				wfDeprecated( "reference in UserLoginForm hook", '1.27' );
777
			}
778
		}
779
780
		return $template;
781
	}
782
783
	public function onAuthChangeFormFields(
784
		array $requests, array $fieldInfo, array &$formDescriptor, $action
785
	) {
786
		$coreFieldDescriptors = $this->getFieldDefinitions( $this->fakeTemplate );
787
		$specialFields = array_merge( [ 'extraInput' ],
788
			array_keys( $this->fakeTemplate->getExtraInputDefinitions() ) );
789
790
		// keep the ordering from getCoreFieldDescriptors() where there is no explicit weight
791
		foreach ( $coreFieldDescriptors as $fieldName => $coreField ) {
792
			$requestField = isset( $formDescriptor[$fieldName] ) ?
793
				$formDescriptor[$fieldName] : [];
794
795
			// remove everything that is not in the fieldinfo, is not marked as a supplemental field
796
			// to something in the fieldinfo, is not B/C for the pre-AuthManager templates,
797
			// and is not an info field or a submit button
798
			if (
799
				!isset( $fieldInfo[$fieldName] )
800
				&& (
801
					!isset( $coreField['baseField'] )
802
					|| !isset( $fieldInfo[$coreField['baseField']] )
803
				)
804
				&& !in_array( $fieldName, $specialFields, true )
805
				&& (
806
					!isset( $coreField['type'] )
807
					|| !in_array( $coreField['type'], [ 'submit', 'info' ], true )
808
				)
809
			) {
810
				$coreFieldDescriptors[$fieldName] = null;
811
				continue;
812
			}
813
814
			// core message labels should always take priority
815
			if (
816
				isset( $coreField['label'] )
817
				|| isset( $coreField['label-message'] )
818
				|| isset( $coreField['label-raw'] )
819
			) {
820
				unset( $requestField['label'], $requestField['label-message'], $coreField['label-raw'] );
821
			}
822
823
			$coreFieldDescriptors[$fieldName] += $requestField;
824
		}
825
826
		$formDescriptor = array_filter( $coreFieldDescriptors + $formDescriptor );
827
		return true;
828
	}
829
830
	/**
831
	 * Show extra information such as password recovery information, link from login to signup,
832
	 * CTA etc? Such information should only be shown on the "landing page", ie. when the user
833
	 * is at the first step of the authentication process.
834
	 * @return bool
835
	 */
836
	protected function showExtraInformation() {
837
		return $this->authAction !== $this->getContinueAction( $this->authAction )
838
			&& !$this->securityLevel;
839
	}
840
841
	/**
842
	 * Create a HTMLForm descriptor for the core login fields.
843
	 * @param FakeAuthTemplate $template B/C data (not used but needed by getBCFieldDefinitions)
844
	 * @return array
845
	 */
846
	protected function getFieldDefinitions( $template ) {
847
		global $wgEmailConfirmToEdit, $wgLoginLanguageSelector;
848
849
		$isLoggedIn = $this->getUser()->isLoggedIn();
850
		$continuePart = $this->isContinued() ? 'continue-' : '';
851
		$anotherPart = $isLoggedIn ? 'another-' : '';
852
		$expiration = $this->getRequest()->getSession()->getProvider()->getRememberUserDuration();
853
		$expirationDays = ceil( $expiration / ( 3600 * 24 ) );
854
		$secureLoginLink = '';
855
		if ( $this->mSecureLoginUrl ) {
856
			$secureLoginLink = Html::element( 'a', [
857
				'href' => $this->mSecureLoginUrl,
858
				'class' => 'mw-ui-flush-right mw-secure',
859
			], $this->msg( 'userlogin-signwithsecure' )->text() );
860
		}
861
		$usernameHelpLink = '';
862
		if ( !$this->msg( 'createacct-helpusername' )->isDisabled() ) {
863
			$usernameHelpLink = Html::rawElement( 'span', [
864
				'class' => 'mw-ui-flush-right',
865
			], $this->msg( 'createacct-helpusername' )->parse() );
866
		}
867
868
		if ( $this->isSignup() ) {
869
			$fieldDefinitions = [
870
				'statusarea' => [
871
					// used by the mediawiki.special.userlogin.signup.js module for error display
872
					// FIXME merge this with HTMLForm's normal status (error) area
873
					'type' => 'info',
874
					'raw' => true,
875
					'default' => Html::element( 'div', [ 'id' => 'mw-createacct-status-area' ] ),
876
					'weight' => -105,
877
				],
878
				'username' => [
879
					'label-raw' => $this->msg( 'userlogin-yourname' )->escaped() . $usernameHelpLink,
880
					'id' => 'wpName2',
881
					'placeholder-message' => $isLoggedIn ? 'createacct-another-username-ph'
882
						: 'userlogin-yourname-ph',
883
				],
884
				'mailpassword' => [
885
					// create account without providing password, a temporary one will be mailed
886
					'type' => 'check',
887
					'label-message' => 'createaccountmail',
888
					'name' => 'wpCreateaccountMail',
889
					'id' => 'wpCreateaccountMail',
890
				],
891
				'password' => [
892
					'id' => 'wpPassword2',
893
					'placeholder-message' => 'createacct-yourpassword-ph',
894
					'hide-if' => [ '===', 'wpCreateaccountMail', '1' ],
895
				],
896
				'domain' => [],
897
				'retype' => [
898
					'baseField' => 'password',
899
					'type' => 'password',
900
					'label-message' => 'createacct-yourpasswordagain',
901
					'id' => 'wpRetype',
902
					'cssclass' => 'loginPassword',
903
					'size' => 20,
904
					'validation-callback' => function ( $value, $alldata ) {
905
						if ( empty( $alldata['mailpassword'] ) && !empty( $alldata['password'] ) ) {
906
							if ( !$value ) {
907
								return $this->msg( 'htmlform-required' );
908
							} elseif ( $value !== $alldata['password'] ) {
909
								return $this->msg( 'badretype' );
910
							}
911
						}
912
						return true;
913
					},
914
					'hide-if' => [ '===', 'wpCreateaccountMail', '1' ],
915
					'placeholder-message' => 'createacct-yourpasswordagain-ph',
916
				],
917
				'email' => [
918
					'type' => 'email',
919
					'label-message' => $wgEmailConfirmToEdit ? 'createacct-emailrequired'
920
						: 'createacct-emailoptional',
921
					'id' => 'wpEmail',
922
					'cssclass' => 'loginText',
923
					'size' => '20',
924
					// FIXME will break non-standard providers
925
					'required' => $wgEmailConfirmToEdit,
926
					'validation-callback' => function ( $value, $alldata ) {
927
						global $wgEmailConfirmToEdit;
928
929
						// AuthManager will check most of these, but that will make the auth
930
						// session fail and this won't, so nicer to do it this way
931
						if ( !$value && $wgEmailConfirmToEdit ) {
932
							// no point in allowing registration without email when email is
933
							// required to edit
934
							return $this->msg( 'noemailtitle' );
935
						} elseif ( !$value && !empty( $alldata['mailpassword'] ) ) {
936
							// cannot send password via email when there is no email address
937
							return $this->msg( 'noemailcreate' );
938
						} elseif ( $value && !Sanitizer::validateEmail( $value ) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \Sanitizer::validateEmail($value) of type null|boolean is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
939
							return $this->msg( 'invalidemailaddress' );
940
						}
941
						return true;
942
					},
943
					'placeholder-message' => 'createacct-' . $anotherPart . 'email-ph',
944
				],
945
				'realname' => [
946
					'type' => 'text',
947
					'help-message' => $isLoggedIn ? 'createacct-another-realname-tip'
948
						: 'prefs-help-realname',
949
					'label-message' => 'createacct-realname',
950
					'cssclass' => 'loginText',
951
					'size' => 20,
952
					'id' => 'wpRealName',
953
				],
954
				'reason' => [
955
					// comment for the user creation log
956
					'type' => 'text',
957
					'label-message' => 'createacct-reason',
958
					'cssclass' => 'loginText',
959
					'id' => 'wpReason',
960
					'size' => '20',
961
					'placeholder-message' => 'createacct-reason-ph',
962
				],
963
				'extrainput' => [], // placeholder for fields coming from the template
964
				'createaccount' => [
965
					// submit button
966
					'type' => 'submit',
967
					'default' => $this->msg( 'createacct-' . $anotherPart . $continuePart .
968
						'submit' )->text(),
969
					'name' => 'wpCreateaccount',
970
					'id' => 'wpCreateaccount',
971
					'weight' => 100,
972
				],
973
			];
974
		} else {
975
			$fieldDefinitions = [
976
				'username' => [
977
					'label-raw' => $this->msg( 'userlogin-yourname' )->escaped() . $secureLoginLink,
978
					'id' => 'wpName1',
979
					'placeholder-message' => 'userlogin-yourname-ph',
980
				],
981
				'password' => [
982
					'id' => 'wpPassword1',
983
					'placeholder-message' => 'userlogin-yourpassword-ph',
984
				],
985
				'domain' => [],
986
				'extrainput' => [],
987
				'rememberMe' => [
988
					// option for saving the user token to a cookie
989
					'type' => 'check',
990
					'name' => 'wpRemember',
991
					'label-message' => $this->msg( 'userlogin-remembermypassword' )
992
						->numParams( $expirationDays ),
993
					'id' => 'wpRemember',
994
				],
995
				'loginattempt' => [
996
					// submit button
997
					'type' => 'submit',
998
					'default' => $this->msg( 'pt-login-' . $continuePart . 'button' )->text(),
999
					'id' => 'wpLoginAttempt',
1000
					'weight' => 100,
1001
				],
1002
				'linkcontainer' => [
1003
					// help link
1004
					'type' => 'info',
1005
					'cssclass' => 'mw-form-related-link-container mw-userlogin-help',
1006
					// 'id' => 'mw-userlogin-help', // FIXME HTMLInfoField ignores this
1007
					'raw' => true,
1008
					'default' => Html::element( 'a', [
1009
						'href' => Skin::makeInternalOrExternalUrl( wfMessage( 'helplogin-url' )
1010
							->inContentLanguage()
1011
							->text() ),
1012
					], $this->msg( 'userlogin-helplink2' )->text() ),
1013
					'weight' => 200,
1014
				],
1015
				// button for ResetPasswordSecondaryAuthenticationProvider
1016
				'skipReset' => [
1017
					'weight' => 110,
1018
					'flags' => [],
1019
				],
1020
			];
1021
		}
1022
1023
		$fieldDefinitions['username'] += [
1024
			'type' => 'text',
1025
			'name' => 'wpName',
1026
			'cssclass' => 'loginText',
1027
			'size' => 20,
1028
			// 'required' => true,
1029
		];
1030
		$fieldDefinitions['password'] += [
1031
			'type' => 'password',
1032
			// 'label-message' => 'userlogin-yourpassword', // would override the changepassword label
1033
			'name' => 'wpPassword',
1034
			'cssclass' => 'loginPassword',
1035
			'size' => 20,
1036
			// 'required' => true,
1037
		];
1038
1039
		if ( $template->get( 'header' ) || $template->get( 'formheader' ) ) {
1040
			// B/C for old extensions that haven't been converted to AuthManager (or have been
1041
			// but somebody is using the old version) and still use templates via the
1042
			// UserCreateForm/UserLoginForm hook.
1043
			// 'header' used by ConfirmEdit, CondfirmAccount, Persona, WikimediaIncubator, SemanticSignup
1044
			// 'formheader' used by MobileFrontend
1045
			$fieldDefinitions['header'] = [
1046
				'type' => 'info',
1047
				'raw' => true,
1048
				'default' => $template->get( 'header' ) ?: $template->get( 'formheader' ),
1049
				'weight' => - 110,
1050
			];
1051
		}
1052
		if ( $this->mEntryError ) {
1053
			$fieldDefinitions['entryError'] = [
1054
				'type' => 'info',
1055
				'default' => Html::rawElement( 'div', [ 'class' => $this->mEntryErrorType . 'box', ],
1056
					$this->mEntryError ),
1057
				'raw' => true,
1058
				'rawrow' => true,
1059
				'weight' => -100,
1060
			];
1061
		}
1062
		if ( !$this->showExtraInformation() ) {
1063
			unset( $fieldDefinitions['linkcontainer'], $fieldDefinitions['signupend'] );
1064
		}
1065
		if ( $this->isSignup() && $this->showExtraInformation() ) {
1066
			// blank signup footer for site customization
1067
			// uses signupend-https for HTTPS requests if it's not blank, signupend otherwise
1068
			$signupendMsg = $this->msg( 'signupend' );
1069
			$signupendHttpsMsg = $this->msg( 'signupend-https' );
1070
			if ( !$signupendMsg->isDisabled() ) {
1071
				$usingHTTPS = $this->getRequest()->getProtocol() === 'https';
1072
				$signupendText = ( $usingHTTPS && !$signupendHttpsMsg->isBlank() )
1073
					? $signupendHttpsMsg ->parse() : $signupendMsg->parse();
1074
				$fieldDefinitions['signupend'] = [
1075
					'type' => 'info',
1076
					'raw' => true,
1077
					'default' => Html::rawElement( 'div', [ 'id' => 'signupend' ], $signupendText ),
1078
					'weight' => 225,
1079
				];
1080
			}
1081
		}
1082
		if ( !$this->isSignup() && $this->showExtraInformation() ) {
1083
			$passwordReset = new PasswordReset( $this->getConfig(), AuthManager::singleton() );
1084
			if ( $passwordReset->isAllowed( $this->getUser() )->isGood() ) {
1085
				$fieldDefinitions['passwordReset'] = [
1086
					'type' => 'info',
1087
					'raw' => true,
1088
					'cssclass' => 'mw-form-related-link-container',
1089
					'default' => Linker::link(
1090
						SpecialPage::getTitleFor( 'PasswordReset' ),
1091
						$this->msg( 'userlogin-resetpassword-link' )->escaped()
1092
					),
1093
					'weight' => 230,
1094
				];
1095
			}
1096
1097
			// Don't show a "create account" link if the user can't.
1098
			if ( $this->showCreateAccountLink() ) {
1099
				// link to the other action
1100
				$linkTitle = $this->getTitleFor( $this->isSignup() ? 'Userlogin' :'CreateAccount' );
1101
				$linkq = $this->getReturnToQueryStringFragment();
1102
				// Pass any language selection on to the mode switch link
1103
				if ( $wgLoginLanguageSelector && $this->mLanguage ) {
1104
					$linkq .= '&uselang=' . $this->mLanguage;
1105
				}
1106
				$loggedIn = $this->getUser()->isLoggedIn();
1107
1108
				$fieldDefinitions['createOrLogin'] = [
1109
					'type' => 'info',
1110
					'raw' => true,
1111
					'linkQuery' => $linkq,
1112
					'default' => function ( $params ) use ( $loggedIn, $linkTitle ) {
1113
						return Html::rawElement( 'div',
1114
							[ 'id' => 'mw-createaccount' . ( !$loggedIn ? '-cta' : '' ),
1115
								'class' => ( $loggedIn ? 'mw-form-related-link-container' : 'mw-ui-vform-field' ) ],
1116
							( $loggedIn ? '' : $this->msg( 'userlogin-noaccount' )->escaped() )
1117
							. Html::element( 'a',
1118
								[
1119
									'id' => 'mw-createaccount-join' . ( $loggedIn ? '-loggedin' : '' ),
1120
									'href' => $linkTitle->getLocalURL( $params['linkQuery'] ),
1121
									'class' => ( $loggedIn ? '' : 'mw-ui-button' ),
1122
									'tabindex' => 100,
1123
								],
1124
								$this->msg(
1125
									$loggedIn ? 'userlogin-createanother' : 'userlogin-joinproject'
1126
								)->escaped()
1127
							)
1128
						);
1129
					},
1130
					'weight' => 235,
1131
				];
1132
			}
1133
		}
1134
1135
		$fieldDefinitions = $this->getBCFieldDefinitions( $fieldDefinitions, $template );
1136
		$fieldDefinitions = array_filter( $fieldDefinitions );
1137
1138
		return $fieldDefinitions;
1139
	}
1140
1141
	/**
1142
	 * Adds fields provided via the deprecated UserLoginForm / UserCreateForm hooks
1143
	 * @param $fieldDefinitions array
1144
	 * @param FakeAuthTemplate $template
1145
	 * @return array
1146
	 */
1147
	protected function getBCFieldDefinitions( $fieldDefinitions, $template ) {
1148
		if ( $template->get( 'usedomain', false ) ) {
1149
			// TODO probably should be translated to the new domain notation in AuthManager
1150
			$fieldDefinitions['domain'] = [
1151
				'type' => 'select',
1152
				'label-message' => 'yourdomainname',
1153
				'options' => array_combine( $template->get( 'domainnames', [] ),
1154
					$template->get( 'domainnames', [] ) ),
1155
				'default' => $template->get( 'domain', '' ),
1156
				'name' => 'wpDomain',
1157
				// FIXME id => 'mw-user-domain-section' on the parent div
1158
			];
1159
		}
1160
1161
		// poor man's associative array_splice
1162
		$extraInputPos = array_search( 'extrainput', array_keys( $fieldDefinitions ), true );
1163
		$fieldDefinitions = array_slice( $fieldDefinitions, 0, $extraInputPos, true )
1164
							+ $template->getExtraInputDefinitions()
1165
							+ array_slice( $fieldDefinitions, $extraInputPos + 1, null, true );
1166
1167
		return $fieldDefinitions;
1168
	}
1169
1170
	/**
1171
	 * Check if a session cookie is present.
1172
	 *
1173
	 * This will not pick up a cookie set during _this_ request, but is meant
1174
	 * to ensure that the client is returning the cookie which was set on a
1175
	 * previous pass through the system.
1176
	 *
1177
	 * @return bool
1178
	 */
1179
	protected function hasSessionCookie() {
1180
		global $wgDisableCookieCheck, $wgInitialSessionId;
1181
1182
		return $wgDisableCookieCheck || (
1183
			$wgInitialSessionId &&
1184
			$this->getRequest()->getSession()->getId() === (string)$wgInitialSessionId
1185
		);
1186
	}
1187
1188
	/**
1189
	 * Returns a string that can be appended to the URL (without encoding) to preserve the
1190
	 * return target. Does not include leading '?'/'&'.
1191
	 */
1192
	protected function getReturnToQueryStringFragment() {
1193
		$returnto = '';
1194
		if ( $this->mReturnTo !== '' ) {
1195
			$returnto = 'returnto=' . wfUrlencode( $this->mReturnTo );
1196
			if ( $this->mReturnToQuery !== '' ) {
1197
				$returnto .= '&returntoquery=' . wfUrlencode( $this->mReturnToQuery );
1198
			}
1199
		}
1200
		return $returnto;
1201
	}
1202
1203
	/**
1204
	 * Whether the login/create account form should display a link to the
1205
	 * other form (in addition to whatever the skin provides).
1206
	 * @return bool
1207
	 */
1208
	private function showCreateAccountLink() {
1209
		if ( $this->isSignup() ) {
1210
			return true;
1211
		} elseif ( $this->getUser()->isAllowed( 'createaccount' ) ) {
1212
			return true;
1213
		} else {
1214
			return false;
1215
		}
1216
	}
1217
1218
	protected function getTokenName() {
1219
		return $this->isSignup() ? 'wpCreateaccountToken' : 'wpLoginToken';
1220
	}
1221
1222
	/**
1223
	 * Produce a bar of links which allow the user to select another language
1224
	 * during login/registration but retain "returnto"
1225
	 *
1226
	 * @return string
1227
	 */
1228
	protected function makeLanguageSelector() {
1229
		$msg = $this->msg( 'loginlanguagelinks' )->inContentLanguage();
1230
		if ( $msg->isBlank() ) {
1231
			return '';
1232
		}
1233
		$langs = explode( "\n", $msg->text() );
1234
		$links = [];
1235
		foreach ( $langs as $lang ) {
1236
			$lang = trim( $lang, '* ' );
1237
			$parts = explode( '|', $lang );
1238
			if ( count( $parts ) >= 2 ) {
1239
				$links[] = $this->makeLanguageSelectorLink( $parts[0], trim( $parts[1] ) );
1240
			}
1241
		}
1242
1243
		return count( $links ) > 0 ? $this->msg( 'loginlanguagelabel' )->rawParams(
1244
			$this->getLanguage()->pipeList( $links ) )->escaped() : '';
1245
	}
1246
1247
	/**
1248
	 * Create a language selector link for a particular language
1249
	 * Links back to this page preserving type and returnto
1250
	 *
1251
	 * @param string $text Link text
1252
	 * @param string $lang Language code
1253
	 * @return string
1254
	 */
1255
	protected function makeLanguageSelectorLink( $text, $lang ) {
1256
		if ( $this->getLanguage()->getCode() == $lang ) {
1257
			// no link for currently used language
1258
			return htmlspecialchars( $text );
1259
		}
1260
		$query = [ 'uselang' => $lang ];
1261
		if ( $this->mReturnTo !== '' ) {
1262
			$query['returnto'] = $this->mReturnTo;
1263
			$query['returntoquery'] = $this->mReturnToQuery;
1264
		}
1265
1266
		$attr = [];
1267
		$targetLanguage = Language::factory( $lang );
1268
		$attr['lang'] = $attr['hreflang'] = $targetLanguage->getHtmlCode();
1269
1270
		return Linker::linkKnown(
1271
			$this->getPageTitle(),
1272
			htmlspecialchars( $text ),
1273
			$attr,
1274
			$query
1275
		);
1276
	}
1277
1278
	protected function getGroupName() {
1279
		return 'login';
1280
	}
1281
1282
	/**
1283
	 * @param array $formDescriptor
1284
	 */
1285
	protected function postProcessFormDescriptor( &$formDescriptor, $requests ) {
1286
		// Pre-fill username (if not creating an account, T46775).
1287
		if (
1288
			isset( $formDescriptor['username'] ) &&
1289
			!isset( $formDescriptor['username']['default'] ) &&
1290
			!$this->isSignup()
1291
		) {
1292
			$user = $this->getUser();
1293
			if ( $user->isLoggedIn() ) {
1294
				$formDescriptor['username']['default'] = $user->getName();
1295
			} else {
1296
				$formDescriptor['username']['default'] =
1297
					$this->getRequest()->getSession()->suggestLoginUsername();
1298
			}
1299
		}
1300
1301
		// don't show a submit button if there is nothing to submit (i.e. the only form content
1302
		// is other submit buttons, for redirect flows)
1303
		if ( !$this->needsSubmitButton( $requests ) ) {
1304
			unset( $formDescriptor['createaccount'], $formDescriptor['loginattempt'] );
1305
		}
1306
1307
		if ( !$this->isSignup() ) {
1308
			// FIXME HACK don't focus on non-empty field
1309
			// maybe there should be an autofocus-if similar to hide-if?
1310
			if (
1311
				isset( $formDescriptor['username'] )
1312
				&& empty( $formDescriptor['username']['default'] )
1313
				&& !$this->getRequest()->getCheck( 'wpName' )
1314
			) {
1315
				$formDescriptor['username']['autofocus'] = true;
1316
			} elseif ( isset( $formDescriptor['password'] ) ) {
1317
				$formDescriptor['password']['autofocus'] = true;
1318
			}
1319
		}
1320
1321
		$this->addTabIndex( $formDescriptor );
1322
	}
1323
}
1324
1325
/**
1326
 * B/C class to try handling login/signup template modifications even though login/signup does not
1327
 * actually happen through a template anymore. Just collects extra field definitions and allows
1328
 * some other class to do decide what to do with threm..
1329
 * TODO find the right place for adding extra fields and kill this
1330
 */
1331
class FakeAuthTemplate extends BaseTemplate {
1332
	public function execute() {
1333
		throw new LogicException( 'not used' );
1334
	}
1335
1336
	/**
1337
	 * Extensions (AntiSpoof and TitleBlacklist) call this in response to
1338
	 * UserCreateForm hook to add checkboxes to the create account form.
1339
	 */
1340
	public function addInputItem( $name, $value, $type, $msg, $helptext = false ) {
1341
		// use the same indexes as UserCreateForm just in case someone adds an item manually
1342
		$this->data['extrainput'][] = [
0 ignored issues
show
Bug introduced by
The property data does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
1343
			'name' => $name,
1344
			'value' => $value,
1345
			'type' => $type,
1346
			'msg' => $msg,
1347
			'helptext' => $helptext,
1348
		];
1349
	}
1350
1351
	/**
1352
	 * Turns addInputItem-style field definitions into HTMLForm field definitions.
1353
	 * @return array
1354
	 */
1355
	public function getExtraInputDefinitions() {
1356
		$definitions = [];
1357
1358
		foreach ( $this->get( 'extrainput', [] ) as $field ) {
1359
			$definition = [
1360
				'type' => $field['type'] === 'checkbox' ? 'check' : $field['type'],
1361
				'name' => $field['name'],
1362
				'value' => $field['value'],
1363
				'id' => $field['name'],
1364
			];
1365
			if ( $field['msg'] ) {
1366
				$definition['label-message'] = $this->getMsg( $field['msg'] );
1367
			}
1368
			if ( $field['helptext'] ) {
1369
				$definition['help'] = $this->msgWiki( $field['helptext'] );
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $definition['help'] is correct as $this->msgWiki($field['helptext']) (which targets BaseTemplate::msgWiki()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1370
			}
1371
1372
			// the array key doesn't matter much when name is defined explicitly but
1373
			// let's try and follow HTMLForm conventions
1374
			$name = preg_replace( '/^wp(?=[A-Z])/', '', $field['name'] );
1375
			$definitions[$name] = $definition;
1376
		}
1377
1378
		if ( $this->haveData( 'extrafields' ) ) {
1379
			$definitions['extrafields'] = [
1380
				'type' => 'info',
1381
				'raw' => true,
1382
				'default' => $this->get( 'extrafields' ),
1383
			];
1384
		}
1385
1386
		return $definitions;
1387
	}
1388
}
1389
1390
/**
1391
 * LoginForm as a special page has been replaced by SpecialUserLogin and SpecialCreateAccount,
1392
 * but some extensions called its public methods directly, so the class is retained as a
1393
 * B/C wrapper. Anything that used it before should use AuthManager instead.
1394
 */
1395
class LoginForm extends SpecialPage {
1396
	const SUCCESS = 0;
1397
	const NO_NAME = 1;
1398
	const ILLEGAL = 2;
1399
	const WRONG_PLUGIN_PASS = 3;
1400
	const NOT_EXISTS = 4;
1401
	const WRONG_PASS = 5;
1402
	const EMPTY_PASS = 6;
1403
	const RESET_PASS = 7;
1404
	const ABORTED = 8;
1405
	const CREATE_BLOCKED = 9;
1406
	const THROTTLED = 10;
1407
	const USER_BLOCKED = 11;
1408
	const NEED_TOKEN = 12;
1409
	const WRONG_TOKEN = 13;
1410
	const USER_MIGRATED = 14;
1411
1412
	public static $statusCodes = [
1413
		self::SUCCESS => 'success',
1414
		self::NO_NAME => 'no_name',
1415
		self::ILLEGAL => 'illegal',
1416
		self::WRONG_PLUGIN_PASS => 'wrong_plugin_pass',
1417
		self::NOT_EXISTS => 'not_exists',
1418
		self::WRONG_PASS => 'wrong_pass',
1419
		self::EMPTY_PASS => 'empty_pass',
1420
		self::RESET_PASS => 'reset_pass',
1421
		self::ABORTED => 'aborted',
1422
		self::CREATE_BLOCKED => 'create_blocked',
1423
		self::THROTTLED => 'throttled',
1424
		self::USER_BLOCKED => 'user_blocked',
1425
		self::NEED_TOKEN => 'need_token',
1426
		self::WRONG_TOKEN => 'wrong_token',
1427
		self::USER_MIGRATED => 'user_migrated',
1428
	];
1429
1430
	/**
1431
	 * @param WebRequest $request
1432
	 */
1433
	public function __construct( $request = null ) {
1434
		wfDeprecated( 'LoginForm', '1.27' );
1435
		parent::__construct();
1436
	}
1437
1438
	/**
1439
	 * @deprecated since 1.27 - call LoginHelper::getValidErrorMessages instead.
1440
	 */
1441
	public static function getValidErrorMessages() {
1442
		return LoginHelper::getValidErrorMessages();
1443
	}
1444
1445
	/**
1446
	 * @deprecated since 1.27 - don't use LoginForm, use AuthManager instead
1447
	 */
1448 View Code Duplication
	public static function incrementLoginThrottle( $username ) {
1449
		wfDeprecated( __METHOD__, "1.27" );
1450
		global $wgRequest;
1451
		$username = User::getCanonicalName( $username, 'usable' ) ?: $username;
1452
		$throttler = new Throttler();
1453
		return $throttler->increase( $username, $wgRequest->getIP(), __METHOD__ );
1454
	}
1455
1456
	/**
1457
	 * @deprecated since 1.27 - don't use LoginForm, use AuthManager instead
1458
	 */
1459
	public static function incLoginThrottle( $username ) {
1460
		wfDeprecated( __METHOD__, "1.27" );
1461
		$res = self::incrementLoginThrottle( $username );
1462
		return is_array( $res ) ? true : 0;
1463
	}
1464
1465
	/**
1466
	 * @deprecated since 1.27 - don't use LoginForm, use AuthManager instead
1467
	 */
1468 View Code Duplication
	public static function clearLoginThrottle( $username ) {
1469
		wfDeprecated( __METHOD__, "1.27" );
1470
		global $wgRequest;
1471
		$username = User::getCanonicalName( $username, 'usable' ) ?: $username;
1472
		$throttler = new Throttler();
1473
		return $throttler->clear( $username, $wgRequest->getIP() );
1474
	}
1475
1476
	/**
1477
	 * @deprecated since 1.27 - don't use LoginForm, use AuthManager instead
1478
	 */
1479
	public static function getLoginToken() {
1480
		wfDeprecated( __METHOD__, '1.27' );
1481
		global $wgRequest;
1482
		return $wgRequest->getSession()->getToken( '', 'login' )->toString();
1483
	}
1484
1485
	/**
1486
	 * @deprecated since 1.27 - don't use LoginForm, use AuthManager instead
1487
	 */
1488
	public static function setLoginToken() {
1489
		wfDeprecated( __METHOD__, '1.27' );
1490
	}
1491
1492
	/**
1493
	 * @deprecated since 1.27 - don't use LoginForm, use AuthManager instead
1494
	 */
1495
	public static function clearLoginToken() {
1496
		wfDeprecated( __METHOD__, '1.27' );
1497
		global $wgRequest;
1498
		$wgRequest->getSession()->resetToken( 'login' );
1499
	}
1500
1501
	/**
1502
	 * @deprecated since 1.27 - don't use LoginForm, use AuthManager instead
1503
	 */
1504
	public static function getCreateaccountToken() {
1505
		wfDeprecated( __METHOD__, '1.27' );
1506
		global $wgRequest;
1507
		return $wgRequest->getSession()->getToken( '', 'createaccount' )->toString();
1508
	}
1509
1510
	/**
1511
	 * @deprecated since 1.27 - don't use LoginForm, use AuthManager instead
1512
	 */
1513
	public static function setCreateaccountToken() {
1514
		wfDeprecated( __METHOD__, '1.27' );
1515
	}
1516
1517
	/**
1518
	 * @deprecated since 1.27 - don't use LoginForm, use AuthManager instead
1519
	 */
1520
	public static function clearCreateaccountToken() {
1521
		wfDeprecated( __METHOD__, '1.27' );
1522
		global $wgRequest;
1523
		$wgRequest->getSession()->resetToken( 'createaccount' );
1524
	}
1525
}
1526