Use AuthManager on special pages
Rewrite authentication-related special pages to use AuthManager. All the changes mentioned below only take effect when $wgDisableAuthManager is false. LoginForm is rewritten to use HTMLForm and split into UserLogin and CreateAccount; ChangePassword and PasswordReset are rewritten; ChangeEmail and Preferences are updated. Four new special pages are added to handle the new capabilities of AuthManager (linked accounts, secondary authentication providers): LinkAccounts, UnlinkAccounts, ChangeCredentials, RemoveCredentials. The old form-based hooks (ChangePasswordForm, UserCreateForm, UserLoginForm) are deprecated. A new, more generic hook is available to alter the forms (AuthChangeFormFields); form changes that involve new fields should be done via $wgAuthManagerConfig. UserLoginComplete is limited to web-based login; for more generic functionality UserLoggedIn can be used instead. Hooks that assume password-based login (PrefsPasswordAudit, AbortChangePassword) are removed; the first functionality is replaced by ChangeAuthenticationDataAudit, the second is handled by AuthManager. LoginPasswordResetMessage is removed, the functionality can be recreated via authentication providers. There are several smaller backwards incompatible changes: * Adding fields to the login/signup forms by manipulating the template via the extraInput/extrafields parameters is not supported anymore. Depending on the authn configuration the login/signup process might be multistep and it would be complicated to ensure that extensions can access the data at the right moment. Instead, you can create an AuthenticationProvider which can define its own fields and process them when the authentication is over. (There is B/C support for a transitional period that works with the default login form, but might break with configurations that require multiple steps or redirects.) * Removed cookie redirect check. This was added in 2003 in9ead07fe9for the benefit of bots, but with MediaWiki having an API these days there is little reason to keep it. Same for the wpSkipCookieCheck flag (added in 2008 in29c73e8265). * Instead of embedding a password field on sensitive special pages such as ChangeEmail, such pages rely on AuthManager for elevated security (which typically involves requiring the user to log in again unless their last login was more than a few minutes ago). Accordingly, wgRequirePasswordforEmailChange is removed. * Special:ChangePassword requires login now. * Special:ResetPassword now sends a separate email to each user when called with a shared email address. * the Reason field had a message with 'prefsectiontip' class which was sorta broken but used in extensions for formatting. HTMLForm does not support that, so this commit turns it into a help message which will break formatting. See https://gerrit.wikimedia.org/r/#/c/231884 Bug: T110277 Change-Id: I8b52ec8ddf494f23941807638f149f15b5e46b0c Depends-On: If4e0dfb6ee6674f0dace80a01850e2d0cbbdb47a
This commit is contained in:
parent
d245bd25ae
commit
3617c982c9
39 changed files with 5313 additions and 659 deletions
22
autoload.php
22
autoload.php
|
|
@ -145,6 +145,7 @@ $wgAutoloadLocalClasses = [
|
|||
'AtomFeed' => __DIR__ . '/includes/Feed.php',
|
||||
'AtomicSectionUpdate' => __DIR__ . '/includes/deferred/AtomicSectionUpdate.php',
|
||||
'AttachLatest' => __DIR__ . '/maintenance/attachLatest.php',
|
||||
'AuthManagerSpecialPage' => __DIR__ . '/includes/specialpage/AuthManagerSpecialPage.php',
|
||||
'AuthPlugin' => __DIR__ . '/includes/AuthPlugin.php',
|
||||
'AuthPluginUser' => __DIR__ . '/includes/AuthPlugin.php',
|
||||
'AutoLoader' => __DIR__ . '/includes/AutoLoader.php',
|
||||
|
|
@ -421,6 +422,7 @@ $wgAutoloadLocalClasses = [
|
|||
'FSFileOpHandle' => __DIR__ . '/includes/filebackend/FSFileBackend.php',
|
||||
'FSLockManager' => __DIR__ . '/includes/filebackend/lockmanager/FSLockManager.php',
|
||||
'FSRepo' => __DIR__ . '/includes/filerepo/FSRepo.php',
|
||||
'FakeAuthTemplate' => __DIR__ . '/includes/specialpage/LoginSignupSpecialPage.php',
|
||||
'FakeConverter' => __DIR__ . '/languages/FakeConverter.php',
|
||||
'FakeMaintenance' => __DIR__ . '/maintenance/Maintenance.php',
|
||||
'FakeResultWrapper' => __DIR__ . '/includes/db/DatabaseUtility.php',
|
||||
|
|
@ -730,7 +732,11 @@ $wgAutoloadLocalClasses = [
|
|||
'LogPager' => __DIR__ . '/includes/logging/LogPager.php',
|
||||
'LoggedOutEditToken' => __DIR__ . '/includes/user/LoggedOutEditToken.php',
|
||||
'LoggedUpdateMaintenance' => __DIR__ . '/maintenance/Maintenance.php',
|
||||
'LoginForm' => __DIR__ . '/includes/specials/SpecialUserlogin.php',
|
||||
'LoginForm' => __DIR__ . '/includes/specialpage/LoginSignupSpecialPage.php',
|
||||
'LoginFormAuthManager' => __DIR__ . '/includes/specialpage/LoginSignupSpecialPage.php',
|
||||
'LoginFormPreAuthManager' => __DIR__ . '/includes/specials/pre-authmanager/SpecialUserlogin.php',
|
||||
'LoginHelper' => __DIR__ . '/includes/specials/helpers/LoginHelper.php',
|
||||
'LoginSignupSpecialPage' => __DIR__ . '/includes/specialpage/LoginSignupSpecialPage.php',
|
||||
'LonelyPagesPage' => __DIR__ . '/includes/specials/SpecialLonelypages.php',
|
||||
'LongPagesPage' => __DIR__ . '/includes/specials/SpecialLongpages.php',
|
||||
'MIMEsearchPage' => __DIR__ . '/includes/specials/SpecialMIMEsearch.php',
|
||||
|
|
@ -798,6 +804,7 @@ $wgAutoloadLocalClasses = [
|
|||
'MediaWiki\\Auth\\CreateFromLoginAuthenticationRequest' => __DIR__ . '/includes/auth/CreateFromLoginAuthenticationRequest.php',
|
||||
'MediaWiki\\Auth\\CreatedAccountAuthenticationRequest' => __DIR__ . '/includes/auth/CreatedAccountAuthenticationRequest.php',
|
||||
'MediaWiki\\Auth\\CreationReasonAuthenticationRequest' => __DIR__ . '/includes/auth/CreationReasonAuthenticationRequest.php',
|
||||
'MediaWiki\\Auth\\EmailNotificationSecondaryAuthenticationProvider' => __DIR__ . '/includes/auth/EmailNotificationSecondaryAuthenticationProvider.php',
|
||||
'MediaWiki\\Auth\\LegacyHookPreAuthenticationProvider' => __DIR__ . '/includes/auth/LegacyHookPreAuthenticationProvider.php',
|
||||
'MediaWiki\\Auth\\LocalPasswordPrimaryAuthenticationProvider' => __DIR__ . '/includes/auth/LocalPasswordPrimaryAuthenticationProvider.php',
|
||||
'MediaWiki\\Auth\\PasswordAuthenticationRequest' => __DIR__ . '/includes/auth/PasswordAuthenticationRequest.php',
|
||||
|
|
@ -996,6 +1003,7 @@ $wgAutoloadLocalClasses = [
|
|||
'PasswordError' => __DIR__ . '/includes/password/PasswordError.php',
|
||||
'PasswordFactory' => __DIR__ . '/includes/password/PasswordFactory.php',
|
||||
'PasswordPolicyChecks' => __DIR__ . '/includes/password/PasswordPolicyChecks.php',
|
||||
'PasswordReset' => __DIR__ . '/includes/user/PasswordReset.php',
|
||||
'PatchSql' => __DIR__ . '/maintenance/patchSql.php',
|
||||
'PathRouter' => __DIR__ . '/includes/PathRouter.php',
|
||||
'PathRouterPatternReplacer' => __DIR__ . '/includes/PathRouter.php',
|
||||
|
|
@ -1235,11 +1243,15 @@ $wgAutoloadLocalClasses = [
|
|||
'SpecialCachedPage' => __DIR__ . '/includes/specials/SpecialCachedPage.php',
|
||||
'SpecialCategories' => __DIR__ . '/includes/specials/SpecialCategories.php',
|
||||
'SpecialChangeContentModel' => __DIR__ . '/includes/specials/SpecialChangeContentModel.php',
|
||||
'SpecialChangeCredentials' => __DIR__ . '/includes/specials/SpecialChangeCredentials.php',
|
||||
'SpecialChangeEmail' => __DIR__ . '/includes/specials/SpecialChangeEmail.php',
|
||||
'SpecialChangeEmailPreAuthManager' => __DIR__ . '/includes/specials/pre-authmanager/SpecialChangeEmail.php',
|
||||
'SpecialChangePassword' => __DIR__ . '/includes/specials/SpecialChangePassword.php',
|
||||
'SpecialChangePasswordPreAuthManager' => __DIR__ . '/includes/specials/pre-authmanager/SpecialChangePassword.php',
|
||||
'SpecialComparePages' => __DIR__ . '/includes/specials/SpecialComparePages.php',
|
||||
'SpecialContributions' => __DIR__ . '/includes/specials/SpecialContributions.php',
|
||||
'SpecialCreateAccount' => __DIR__ . '/includes/specials/SpecialCreateAccount.php',
|
||||
'SpecialCreateAccountPreAuthManager' => __DIR__ . '/includes/specials/pre-authmanager/SpecialCreateAccount.php',
|
||||
'SpecialDiff' => __DIR__ . '/includes/specials/SpecialDiff.php',
|
||||
'SpecialEditTags' => __DIR__ . '/includes/specials/SpecialEditTags.php',
|
||||
'SpecialEditWatchlist' => __DIR__ . '/includes/specials/SpecialEditWatchlist.php',
|
||||
|
|
@ -1249,6 +1261,7 @@ $wgAutoloadLocalClasses = [
|
|||
'SpecialFilepath' => __DIR__ . '/includes/specials/SpecialFilepath.php',
|
||||
'SpecialImport' => __DIR__ . '/includes/specials/SpecialImport.php',
|
||||
'SpecialJavaScriptTest' => __DIR__ . '/includes/specials/SpecialJavaScriptTest.php',
|
||||
'SpecialLinkAccounts' => __DIR__ . '/includes/specials/SpecialLinkAccounts.php',
|
||||
'SpecialListAdmins' => __DIR__ . '/includes/specials/SpecialListusers.php',
|
||||
'SpecialListBots' => __DIR__ . '/includes/specials/SpecialListusers.php',
|
||||
'SpecialListFiles' => __DIR__ . '/includes/specials/SpecialListfiles.php',
|
||||
|
|
@ -1271,6 +1284,7 @@ $wgAutoloadLocalClasses = [
|
|||
'SpecialPageLanguage' => __DIR__ . '/includes/specials/SpecialPageLanguage.php',
|
||||
'SpecialPagesWithProp' => __DIR__ . '/includes/specials/SpecialPagesWithProp.php',
|
||||
'SpecialPasswordReset' => __DIR__ . '/includes/specials/SpecialPasswordReset.php',
|
||||
'SpecialPasswordResetPreAuthManager' => __DIR__ . '/includes/specials/pre-authmanager/SpecialPasswordReset.php',
|
||||
'SpecialPermanentLink' => __DIR__ . '/includes/specials/SpecialPermanentLink.php',
|
||||
'SpecialPreferences' => __DIR__ . '/includes/specials/SpecialPreferences.php',
|
||||
'SpecialPrefixindex' => __DIR__ . '/includes/specials/SpecialPrefixindex.php',
|
||||
|
|
@ -1283,6 +1297,7 @@ $wgAutoloadLocalClasses = [
|
|||
'SpecialRecentChangesLinked' => __DIR__ . '/includes/specials/SpecialRecentchangeslinked.php',
|
||||
'SpecialRedirect' => __DIR__ . '/includes/specials/SpecialRedirect.php',
|
||||
'SpecialRedirectToSpecial' => __DIR__ . '/includes/specialpage/RedirectSpecialPage.php',
|
||||
'SpecialRemoveCredentials' => __DIR__ . '/includes/specials/SpecialRemoveCredentials.php',
|
||||
'SpecialResetTokens' => __DIR__ . '/includes/specials/SpecialResetTokens.php',
|
||||
'SpecialRevisionDelete' => __DIR__ . '/includes/specials/SpecialRevisiondelete.php',
|
||||
'SpecialRunJobs' => __DIR__ . '/includes/specials/SpecialRunJobs.php',
|
||||
|
|
@ -1293,11 +1308,14 @@ $wgAutoloadLocalClasses = [
|
|||
'SpecialTrackingCategories' => __DIR__ . '/includes/specials/SpecialTrackingCategories.php',
|
||||
'SpecialUnblock' => __DIR__ . '/includes/specials/SpecialUnblock.php',
|
||||
'SpecialUndelete' => __DIR__ . '/includes/specials/SpecialUndelete.php',
|
||||
'SpecialUnlinkAccounts' => __DIR__ . '/includes/specials/SpecialUnlinkAccounts.php',
|
||||
'SpecialUnlockdb' => __DIR__ . '/includes/specials/SpecialUnlockdb.php',
|
||||
'SpecialUpload' => __DIR__ . '/includes/specials/SpecialUpload.php',
|
||||
'SpecialUploadStash' => __DIR__ . '/includes/specials/SpecialUploadStash.php',
|
||||
'SpecialUploadStashTooLargeException' => __DIR__ . '/includes/specials/SpecialUploadStash.php',
|
||||
'SpecialUserlogout' => __DIR__ . '/includes/specials/SpecialUserlogout.php',
|
||||
'SpecialUserLogin' => __DIR__ . '/includes/specials/SpecialUserLogin.php',
|
||||
'SpecialUserLogout' => __DIR__ . '/includes/specials/SpecialUserLogout.php',
|
||||
'SpecialUserlogoutPreAuthManager' => __DIR__ . '/includes/specials/pre-authmanager/SpecialUserlogout.php',
|
||||
'SpecialVersion' => __DIR__ . '/includes/specials/SpecialVersion.php',
|
||||
'SpecialWatchlist' => __DIR__ . '/includes/specials/SpecialWatchlist.php',
|
||||
'SpecialWhatLinksHere' => __DIR__ . '/includes/specials/SpecialWhatlinkshere.php',
|
||||
|
|
|
|||
|
|
@ -249,12 +249,6 @@ $user: the User object about to be created (read-only, incomplete)
|
|||
$autoblockip: The IP going to be autoblocked.
|
||||
&$block: The block from which the autoblock is coming.
|
||||
|
||||
'AbortChangePassword': Return false to cancel password change.
|
||||
$user: the User object to which the password change is occuring
|
||||
$mOldpass: the old password provided by the user
|
||||
$newpass: the new password provided by the user
|
||||
&$abortMsg: the message identifier for abort reason
|
||||
|
||||
'AbortDiffCache': Can be used to cancel the caching of a diff.
|
||||
&$diffEngine: DifferenceEngine object
|
||||
|
||||
|
|
@ -298,7 +292,8 @@ $name: name of the action
|
|||
&$fields: HTMLForm descriptor array
|
||||
$article: Article object
|
||||
|
||||
'AddNewAccount': After a user account is created.
|
||||
'AddNewAccount': DEPRECATED! Use LocalUserCreated.
|
||||
After a user account is created.
|
||||
$user: the User object that was created. (Parameter added in 1.7)
|
||||
$byEmail: true when account was created "by email" (added in 1.12)
|
||||
|
||||
|
|
@ -747,6 +742,15 @@ viewing.
|
|||
redirect was followed.
|
||||
&$article: target article (object)
|
||||
|
||||
'AuthChangeFormFields': After converting a field information array obtained
|
||||
from a set of AuthenticationRequest classes into a form descriptor; hooks
|
||||
can tweak the array to change how login etc. forms should look.
|
||||
$requests: array of AuthenticationRequests the fields are created from
|
||||
$fieldInfo: field information array (union of all AuthenticationRequest::getFieldInfo() responses).
|
||||
&$formDescriptor: HTMLForm descriptor. The special key 'weight' can be set
|
||||
to change the order of the fields.
|
||||
$action: one of the AuthManager::ACTION_* constants.
|
||||
|
||||
'AuthManagerLoginAuthenticateAudit': A login attempt either succeeded or failed
|
||||
for a reason other than misconfiguration or session loss. No return data is
|
||||
accepted; this hook is for auditing only.
|
||||
|
|
@ -929,8 +933,14 @@ $html: Requested html content of anchor
|
|||
&$link: Returned value. When set to a non-null value by a hook subscriber
|
||||
this value will be used as the anchor instead of Linker::link
|
||||
|
||||
'ChangePasswordForm': For extensions that need to add a field to the
|
||||
ChangePassword form via the Preferences form.
|
||||
'ChangeAuthenticationDataAudit': Called when user changes his password.
|
||||
No return data is accepted; this hook is for auditing only.
|
||||
$req: AuthenticationRequest object describing the change (and target user)
|
||||
$status: StatusValue with the result of the action
|
||||
|
||||
'ChangePasswordForm': DEPRECATED! Use AuthChangeFormFields or security levels.
|
||||
For extensions that need to add a field to the ChangePassword form via the
|
||||
Preferences form.
|
||||
&$extraFields: An array of arrays that hold fields like would be passed to the
|
||||
pretty function.
|
||||
|
||||
|
|
@ -1937,12 +1947,6 @@ in LoginForm::$validErrorMessages).
|
|||
&$messages: Already added messages (inclusive messages from
|
||||
LoginForm::$validErrorMessages)
|
||||
|
||||
'LoginPasswordResetMessage': User is being requested to reset their password on
|
||||
login. Use this hook to change the Message that will be output on
|
||||
Special:ChangePassword.
|
||||
&$msg: Message object that will be shown to the user
|
||||
$username: Username of the user who's password was expired.
|
||||
|
||||
'LoginUserMigrated': DEPRECATED! Create a PreAuthenticationProvider instead.
|
||||
Called during login to allow extensions the opportunity to inform a user that
|
||||
their username doesn't exist for a specific reason, instead of letting the
|
||||
|
|
@ -2441,11 +2445,6 @@ $user: User (object) changing his email address
|
|||
$oldaddr: old email address (string)
|
||||
$newaddr: new email address (string)
|
||||
|
||||
'PrefsPasswordAudit': Called when user changes his password.
|
||||
$user: User (object) changing his password
|
||||
$newPass: new password
|
||||
$error: error (string) 'badretype', 'wrongpassword', 'error' or 'success'
|
||||
|
||||
'ProtectionForm::buildForm': Called after all protection type fieldsets are made
|
||||
in the form.
|
||||
$article: the title being (un)protected
|
||||
|
|
@ -3279,7 +3278,8 @@ messages!" message, return false to not delete it.
|
|||
&$user: User (object) that will clear the message
|
||||
$oldid: ID of the talk page revision being viewed (0 means the most recent one)
|
||||
|
||||
'UserCreateForm': change to manipulate the login form
|
||||
'UserCreateForm': DEPRECATED! Create an AuthenticationProvider instead.
|
||||
Manipulate the login form.
|
||||
&$template: SimpleTemplate instance for the form
|
||||
|
||||
'UserEffectiveGroups': Called in User::getEffectiveGroups().
|
||||
|
|
@ -3382,12 +3382,14 @@ $user: User object
|
|||
'UserLoggedIn': Called after a user is logged in
|
||||
$user: User object for the logged-in user
|
||||
|
||||
'UserLoginComplete': After a user has logged in.
|
||||
'UserLoginComplete': Show custom content after a user has logged in via the web interface.
|
||||
For functionality that needs to run after any login (API or web) use UserLoggedIn.
|
||||
&$user: the user object that was created on login
|
||||
&$inject_html: Any HTML to inject after the "logged in" message.
|
||||
|
||||
'UserLoginForm': change to manipulate the login form
|
||||
&$template: SimpleTemplate instance for the form
|
||||
'UserLoginForm': DEPRECATED! Create an AuthenticationProvider instead.
|
||||
Manipulate the login form.
|
||||
&$template: QuickTemplate instance for the form
|
||||
|
||||
'UserLogout': Before a user logs out.
|
||||
&$user: the user object that is about to be logged out
|
||||
|
|
|
|||
|
|
@ -4473,6 +4473,10 @@ $wgAuthManagerAutoConfig = [
|
|||
// 'class' => MediaWiki\Auth\ConfirmLinkSecondaryAuthenticationProvider::class,
|
||||
// 'sort' => 100,
|
||||
// ],
|
||||
MediaWiki\Auth\EmailNotificationSecondaryAuthenticationProvider::class => [
|
||||
'class' => MediaWiki\Auth\EmailNotificationSecondaryAuthenticationProvider::class,
|
||||
'sort' => 200,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
|
|
@ -4496,6 +4500,32 @@ $wgAllowSecuritySensitiveOperationIfCannotReauthenticate = [
|
|||
'default' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* List of AuthenticationRequest class names which are not changeable through
|
||||
* Special:ChangeCredentials and the changeauthenticationdata API.
|
||||
* This is only enforced on the client level; AuthManager itself (e.g.
|
||||
* AuthManager::allowsAuthenticationDataChange calls) is not affected.
|
||||
* Class names are checked for exact match (not for subclasses).
|
||||
* @since 1.27
|
||||
* @var string[]
|
||||
*/
|
||||
$wgChangeCredentialsBlacklist = [
|
||||
\MediaWiki\Auth\TemporaryPasswordAuthenticationRequest::class
|
||||
];
|
||||
|
||||
/**
|
||||
* List of AuthenticationRequest class names which are not removable through
|
||||
* Special:RemoveCredentials and the removeauthenticationdata API.
|
||||
* This is only enforced on the client level; AuthManager itself (e.g.
|
||||
* AuthManager::allowsAuthenticationDataChange calls) is not affected.
|
||||
* Class names are checked for exact match (not for subclasses).
|
||||
* @since 1.27
|
||||
* @var string[]
|
||||
*/
|
||||
$wgRemoveCredentialsBlacklist = [
|
||||
\MediaWiki\Auth\PasswordAuthenticationRequest::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* For compatibility with old installations set to false
|
||||
* @deprecated since 1.24 will be removed in future
|
||||
|
|
|
|||
|
|
@ -2835,7 +2835,7 @@ class EditPage {
|
|||
// Log-in link
|
||||
'{{fullurl:Special:UserLogin|returnto={{FULLPAGENAMEE}}}}',
|
||||
// Sign-up link
|
||||
'{{fullurl:Special:UserLogin/signup|returnto={{FULLPAGENAMEE}}}}' ]
|
||||
'{{fullurl:Special:CreateAccount|returnto={{FULLPAGENAMEE}}}}' ]
|
||||
);
|
||||
} else {
|
||||
$wgOut->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
*
|
||||
* @file
|
||||
*/
|
||||
use MediaWiki\Auth\AuthManager;
|
||||
use MediaWiki\Auth\PasswordAuthenticationRequest;
|
||||
|
||||
/**
|
||||
* We're now using the HTMLForm object with some customisation to generate the
|
||||
|
|
@ -205,7 +207,7 @@ class Preferences {
|
|||
* @return void
|
||||
*/
|
||||
static function profilePreferences( $user, IContextSource $context, &$defaultPreferences ) {
|
||||
global $wgAuth, $wgContLang, $wgParser;
|
||||
global $wgAuth, $wgContLang, $wgParser, $wgDisableAuthManager;
|
||||
|
||||
$config = $context->getConfig();
|
||||
// retrieving user name for GENDER and misc.
|
||||
|
|
@ -281,16 +283,21 @@ class Preferences {
|
|||
$canEditPrivateInfo = $user->isAllowed( 'editmyprivateinfo' );
|
||||
|
||||
// Actually changeable stuff
|
||||
$realnameChangeAllowed = $wgDisableAuthManager ? $wgAuth->allowPropChange( 'realname' )
|
||||
: AuthManager::singleton()->allowsPropertyChange( 'realname' );
|
||||
$defaultPreferences['realname'] = [
|
||||
// (not really "private", but still shouldn't be edited without permission)
|
||||
'type' => $canEditPrivateInfo && $wgAuth->allowPropChange( 'realname' ) ? 'text' : 'info',
|
||||
'type' => $canEditPrivateInfo && $realnameChangeAllowed ? 'text' : 'info',
|
||||
'default' => $user->getRealName(),
|
||||
'section' => 'personal/info',
|
||||
'label-message' => 'yourrealname',
|
||||
'help-message' => 'prefs-help-realname',
|
||||
];
|
||||
|
||||
if ( $canEditPrivateInfo && $wgAuth->allowPasswordChange() ) {
|
||||
$allowPasswordChange = $wgDisableAuthManager ? $wgAuth->allowPasswordChange()
|
||||
: AuthManager::singleton()->allowsAuthenticationDataChange(
|
||||
new PasswordAuthenticationRequest(), false );
|
||||
if ( $canEditPrivateInfo && $allowPasswordChange ) {
|
||||
$link = Linker::link( SpecialPage::getTitleFor( 'ChangePassword' ),
|
||||
$context->msg( 'prefs-resetpass' )->escaped(), [],
|
||||
[ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
|
||||
|
|
@ -411,8 +418,10 @@ class Preferences {
|
|||
'default' => $oldsigHTML,
|
||||
'section' => 'personal/signature',
|
||||
];
|
||||
$nicknameChangeAllowed = $wgDisableAuthManager ? $wgAuth->allowPropChange( 'nickname' )
|
||||
: AuthManager::singleton()->allowsPropertyChange( 'nickname' );
|
||||
$defaultPreferences['nickname'] = [
|
||||
'type' => $wgAuth->allowPropChange( 'nickname' ) ? 'text' : 'info',
|
||||
'type' => $nicknameChangeAllowed ? 'text' : 'info',
|
||||
'maxlength' => $config->get( 'MaxSigChars' ),
|
||||
'label-message' => 'yournick',
|
||||
'validation-callback' => [ 'Preferences', 'validateSignature' ],
|
||||
|
|
@ -441,7 +450,9 @@ class Preferences {
|
|||
}
|
||||
|
||||
$emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
|
||||
if ( $canEditPrivateInfo && $wgAuth->allowPropChange( 'emailaddress' ) ) {
|
||||
$emailChangeAllowed = $wgDisableAuthManager ? $wgAuth->allowPropChange( 'emailaddress' )
|
||||
: AuthManager::singleton()->allowsPropertyChange( 'emailaddress' );
|
||||
if ( $canEditPrivateInfo && $emailChangeAllowed ) {
|
||||
$link = Linker::link(
|
||||
SpecialPage::getTitleFor( 'ChangeEmail' ),
|
||||
$context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->escaped(),
|
||||
|
|
@ -1428,6 +1439,7 @@ class Preferences {
|
|||
|
||||
// Fortunately, the realname field is MUCH simpler
|
||||
// (not really "private", but still shouldn't be edited without permission)
|
||||
|
||||
if ( !in_array( 'realname', $hiddenPrefs )
|
||||
&& $user->isAllowed( 'editmyprivateinfo' )
|
||||
&& array_key_exists( 'realname', $formData )
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Auth;
|
||||
|
||||
use Config;
|
||||
use StatusValue;
|
||||
|
||||
/**
|
||||
* Handles email notification / email address confirmation for account creation.
|
||||
*
|
||||
* Set 'no-email' to true (via AuthManager::setAuthenticationSessionData) to skip this provider.
|
||||
* Primary providers doing so are expected to take care of email address confirmation.
|
||||
*/
|
||||
class EmailNotificationSecondaryAuthenticationProvider
|
||||
extends AbstractSecondaryAuthenticationProvider
|
||||
{
|
||||
/** @var bool */
|
||||
protected $sendConfirmationEmail;
|
||||
|
||||
/**
|
||||
* @param array $params
|
||||
* - sendConfirmationEmail: (bool) send an email asking the user to confirm their email
|
||||
* address after a successful registration
|
||||
*/
|
||||
public function __construct( $params = [] ) {
|
||||
if ( isset( $params['sendConfirmationEmail'] ) ) {
|
||||
$this->sendConfirmationEmail = (bool)$params['sendConfirmationEmail'];
|
||||
}
|
||||
}
|
||||
|
||||
public function setConfig( Config $config ) {
|
||||
parent::setConfig( $config );
|
||||
|
||||
if ( $this->sendConfirmationEmail === null ) {
|
||||
$this->sendConfirmationEmail = $this->config->get( 'EnableEmail' )
|
||||
&& $this->config->get( 'EmailAuthentication' );
|
||||
}
|
||||
}
|
||||
|
||||
public function getAuthenticationRequests( $action, array $options ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function beginSecondaryAuthentication( $user, array $reqs ) {
|
||||
return AuthenticationResponse::newAbstain();
|
||||
}
|
||||
|
||||
public function beginSecondaryAccountCreation( $user, $creator, array $reqs ) {
|
||||
if (
|
||||
$this->sendConfirmationEmail
|
||||
&& $user->getEmail()
|
||||
&& !$this->manager->getAuthenticationSessionData( 'no-email' )
|
||||
) {
|
||||
$status = $user->sendConfirmationMail();
|
||||
$user->saveSettings();
|
||||
if ( $status->isGood() ) {
|
||||
// TODO show 'confirmemail_oncreate' success message
|
||||
} else {
|
||||
// TODO show 'confirmemail_sendfailed' error message
|
||||
$this->logger->warning( 'Could not send confirmation email: ' .
|
||||
$status->getWikiText( false, false, 'en' ) );
|
||||
}
|
||||
}
|
||||
|
||||
return AuthenticationResponse::newPass();
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
* 'exception-nologin' as a title and 'exception-nologin-text' for the message.
|
||||
*
|
||||
* @note In order for this exception to redirect, the error message passed to the
|
||||
* constructor has to be explicitly added to LoginForm::validErrorMessages or with
|
||||
* constructor has to be explicitly added to LoginHelper::validErrorMessages or with
|
||||
* the LoginFormValidErrorMessages hook. Otherwise, the user will just be shown the message
|
||||
* rather than redirected.
|
||||
*
|
||||
|
|
@ -79,7 +79,7 @@ class UserNotLoggedIn extends ErrorPageError {
|
|||
public function report() {
|
||||
// If an unsupported message is used, don't try redirecting to Special:Userlogin,
|
||||
// since the message may not be compatible.
|
||||
if ( !in_array( $this->msg, LoginForm::getValidErrorMessages() ) ) {
|
||||
if ( !in_array( $this->msg, LoginHelper::getValidErrorMessages() ) ) {
|
||||
parent::report();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -663,19 +663,35 @@ class SkinTemplate extends Skin {
|
|||
$loginlink = $this->getUser()->isAllowed( 'createaccount' ) && $useCombinedLoginLink
|
||||
? 'nav-login-createaccount'
|
||||
: 'pt-login';
|
||||
$is_signup = $request->getText( 'type' ) == 'signup';
|
||||
|
||||
$login_url = [
|
||||
'text' => $this->msg( $loginlink )->text(),
|
||||
'href' => self::makeSpecialUrl( 'Userlogin', $returnto ),
|
||||
'active' => $title->isSpecial( 'Userlogin' )
|
||||
&& ( $loginlink == 'nav-login-createaccount' || !$is_signup ),
|
||||
];
|
||||
$createaccount_url = [
|
||||
'text' => $this->msg( 'pt-createaccount' )->text(),
|
||||
'href' => self::makeSpecialUrl( 'Userlogin', "$returnto&type=signup" ),
|
||||
'active' => $title->isSpecial( 'Userlogin' ) && $is_signup,
|
||||
];
|
||||
// TODO remove this after AuthManager is stable
|
||||
global $wgDisableAuthManager;
|
||||
if ( $wgDisableAuthManager ) {
|
||||
$is_signup = $request->getText( 'type' ) == 'signup';
|
||||
$login_url = [
|
||||
'text' => $this->msg( $loginlink )->text(),
|
||||
'href' => self::makeSpecialUrl( 'Userlogin', $returnto ),
|
||||
'active' => $title->isSpecial( 'Userlogin' )
|
||||
&& ( $loginlink == 'nav-login-createaccount' || !$is_signup ),
|
||||
];
|
||||
$createaccount_url = [
|
||||
'text' => $this->msg( 'pt-createaccount' )->text(),
|
||||
'href' => self::makeSpecialUrl( 'Userlogin', "$returnto&type=signup" ),
|
||||
'active' => $title->isSpecial( 'Userlogin' ) && $is_signup,
|
||||
];
|
||||
} else {
|
||||
$login_url = [
|
||||
'text' => $this->msg( $loginlink )->text(),
|
||||
'href' => self::makeSpecialUrl( 'Userlogin', $returnto ),
|
||||
'active' => $title->isSpecial( 'Userlogin' ) ||
|
||||
$title->isSpecial( 'CreateAccount' ) && $useCombinedLoginLink,
|
||||
];
|
||||
$createaccount_url = [
|
||||
'text' => $this->msg( 'pt-createaccount' )->text(),
|
||||
'href' => self::makeSpecialUrl( 'CreateAccount', $returnto ),
|
||||
'active' => $title->isSpecial( 'CreateAccount' ),
|
||||
];
|
||||
}
|
||||
|
||||
// No need to show Talk and Contributions to anons if they can't contribute!
|
||||
if ( User::groupHasPermission( '*', 'edit' ) ) {
|
||||
|
|
|
|||
744
includes/specialpage/AuthManagerSpecialPage.php
Normal file
744
includes/specialpage/AuthManagerSpecialPage.php
Normal file
|
|
@ -0,0 +1,744 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\Auth\AuthenticationRequest;
|
||||
use MediaWiki\Auth\AuthenticationResponse;
|
||||
use MediaWiki\Auth\AuthManager;
|
||||
use MediaWiki\Logger\LoggerFactory;
|
||||
use MediaWiki\Session\SessionManager;
|
||||
use MediaWiki\Session\Token;
|
||||
|
||||
/**
|
||||
* A special page subclass for authentication-related special pages. It generates a form from
|
||||
* a set of AuthenticationRequest objects, submits the result to AuthManager and
|
||||
* partially handles the response.
|
||||
*/
|
||||
abstract class AuthManagerSpecialPage extends SpecialPage {
|
||||
/** @var string[] The list of actions this special page deals with. Subclasses should override
|
||||
* this. */
|
||||
protected static $allowedActions = [
|
||||
AuthManager::ACTION_LOGIN, AuthManager::ACTION_LOGIN_CONTINUE,
|
||||
AuthManager::ACTION_CREATE, AuthManager::ACTION_CREATE_CONTINUE,
|
||||
AuthManager::ACTION_LINK, AuthManager::ACTION_LINK_CONTINUE,
|
||||
AuthManager::ACTION_CHANGE, AuthManager::ACTION_REMOVE, AuthManager::ACTION_UNLINK,
|
||||
];
|
||||
|
||||
/** @var array Customized messages */
|
||||
protected static $messages = [];
|
||||
|
||||
/** @var string one of the AuthManager::ACTION_* constants. */
|
||||
protected $authAction;
|
||||
|
||||
/** @var AuthenticationRequest[] */
|
||||
protected $authRequests;
|
||||
|
||||
/** @var string Subpage of the special page. */
|
||||
protected $subPage;
|
||||
|
||||
/** @var bool True if the current request is a result of returning from a redirect flow. */
|
||||
protected $isReturn;
|
||||
|
||||
/** @var WebRequest|null If set, will be used instead of the real request. Used for redirection. */
|
||||
protected $savedRequest;
|
||||
|
||||
/**
|
||||
* Change the form descriptor that determines how a field will look in the authentication form.
|
||||
* Called from fieldInfoToFormDescriptor().
|
||||
* @param AuthenticationRequest[] $requests
|
||||
* @param string $fieldInfo Field information array (union of all
|
||||
* AuthenticationRequest::getFieldInfo() responses).
|
||||
* @param array $formDescriptor HTMLForm descriptor. The special key 'weight' can be set to
|
||||
* change the order of the fields.
|
||||
* @param string $action Authentication type (one of the AuthManager::ACTION_* constants)
|
||||
* @return bool
|
||||
*/
|
||||
public function onAuthChangeFormFields(
|
||||
array $requests, array $fieldInfo, array &$formDescriptor, $action
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getLoginSecurityLevel() {
|
||||
return $this->getName();
|
||||
}
|
||||
|
||||
public function getRequest() {
|
||||
return $this->savedRequest ?: $this->getContext()->getRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the POST data, GET data from the real request is preserved.
|
||||
*
|
||||
* Used to preserve POST data over a HTTP redirect.
|
||||
*
|
||||
* @param array $data
|
||||
* @param bool $wasPosted
|
||||
*/
|
||||
protected function setRequest( array $data, $wasPosted = null ) {
|
||||
$request = $this->getContext()->getRequest();
|
||||
if ( $wasPosted === null ) {
|
||||
$wasPosted = $request->wasPosted();
|
||||
}
|
||||
$this->savedRequest = new DerivativeRequest( $request, $data + $request->getQueryValues(),
|
||||
$wasPosted );
|
||||
}
|
||||
|
||||
protected function beforeExecute( $subPage ) {
|
||||
$this->getOutput()->disallowUserJs();
|
||||
|
||||
return $this->handleReturnBeforeExecute( $subPage )
|
||||
&& $this->handleReauthBeforeExecute( $subPage );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle redirection from the /return subpage.
|
||||
*
|
||||
* This is used in the redirect flow where we need
|
||||
* to be able to process data that was sent via a GET request. We set the /return subpage as
|
||||
* the reentry point so we know we need to treat GET as POST, but we don't want to handle all
|
||||
* future GETs as POSTs so we need to normalize the URL. (Also we don't want to show any
|
||||
* received parameters around in the URL; they are ugly and might be sensitive.)
|
||||
*
|
||||
* Thus when on the /return subpage, we stash the request data in the session, redirect, then
|
||||
* use the session to detect that we have been redirected, recover the data and replace the
|
||||
* real WebRequest with a fake one that contains the saved data.
|
||||
*
|
||||
* @param string $subPage
|
||||
* @return bool False if execution should be stopped.
|
||||
*/
|
||||
protected function handleReturnBeforeExecute( $subPage ) {
|
||||
$authManager = AuthManager::singleton();
|
||||
$key = 'AuthManagerSpecialPage:return:' . $this->getName();
|
||||
|
||||
if ( $subPage === 'return' ) {
|
||||
$this->loadAuth( $subPage );
|
||||
$preservedParams = $this->getPreservedParams( false );
|
||||
|
||||
// FIXME save POST values only from request
|
||||
$authData = array_diff_key( $this->getRequest()->getValues(),
|
||||
$preservedParams, [ 'title' => 1 ] );
|
||||
$authManager->setAuthenticationSessionData( $key, $authData );
|
||||
|
||||
$url = $this->getPageTitle()->getFullURL( $preservedParams, false, PROTO_HTTPS );
|
||||
$this->getOutput()->redirect( $url );
|
||||
return false;
|
||||
}
|
||||
|
||||
$authData = $authManager->getAuthenticationSessionData( $key );
|
||||
if ( $authData ) {
|
||||
$authManager->removeAuthenticationSessionData( $key );
|
||||
$this->isReturn = true;
|
||||
$this->setRequest( $authData, true );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle redirection when the user needs to (re)authenticate.
|
||||
*
|
||||
* Send the user to the login form if needed; in case the request was a POST, stash in the
|
||||
* session and simulate it once the user gets back.
|
||||
*
|
||||
* @param string $subPage
|
||||
* @return bool False if execution should be stopped.
|
||||
* @throws ErrorPageError When the user is not allowed to use this page.
|
||||
*/
|
||||
protected function handleReauthBeforeExecute( $subPage ) {
|
||||
$authManager = AuthManager::singleton();
|
||||
$request = $this->getRequest();
|
||||
$key = 'AuthManagerSpecialPage:reauth:' . $this->getName();
|
||||
|
||||
$securityLevel = $this->getLoginSecurityLevel();
|
||||
if ( $securityLevel ) {
|
||||
$securityStatus = AuthManager::singleton()
|
||||
->securitySensitiveOperationStatus( $securityLevel );
|
||||
if ( $securityStatus === AuthManager::SEC_REAUTH ) {
|
||||
$queryParams = array_diff_key( $request->getQueryValues(), [ 'title' => true ] );
|
||||
|
||||
if ( $request->wasPosted() ) {
|
||||
// unique ID in case the same special page is open in multiple browser tabs
|
||||
$uniqueId = MWCryptRand::generateHex( 6 );
|
||||
$key = $key . ':' . $uniqueId;
|
||||
|
||||
$queryParams = [ 'authUniqueId' => $uniqueId ] + $queryParams;
|
||||
$authData = array_diff_key( $request->getValues(),
|
||||
$this->getPreservedParams( false ), [ 'title' => 1 ] );
|
||||
$authManager->setAuthenticationSessionData( $key, $authData );
|
||||
}
|
||||
|
||||
$title = SpecialPage::getTitleFor( 'Userlogin' );
|
||||
$url = $title->getFullURL( [
|
||||
'returnto' => $this->getFullTitle()->getPrefixedDBkey(),
|
||||
'returntoquery' => wfArrayToCgi( $queryParams ),
|
||||
'force' => $securityLevel,
|
||||
], false, PROTO_HTTPS );
|
||||
|
||||
$this->getOutput()->redirect( $url );
|
||||
return false;
|
||||
} elseif ( $securityStatus !== AuthManager::SEC_OK ) {
|
||||
throw new ErrorPageError( 'cannotauth-not-allowed-title', 'cannotauth-not-allowed' );
|
||||
}
|
||||
}
|
||||
|
||||
$uniqueId = $request->getVal( 'authUniqueId' );
|
||||
if ( $uniqueId ) {
|
||||
$key = $key . ':' . $uniqueId;
|
||||
$authData = $authManager->getAuthenticationSessionData( $key );
|
||||
if ( $authData ) {
|
||||
$authManager->removeAuthenticationSessionData( $key );
|
||||
$this->setRequest( $authData, true );
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default action for this special page, if none is given via URL/POST data.
|
||||
* Subclasses should override this (or override loadAuth() so this is never called).
|
||||
* @param string $subPage Subpage of the special page.
|
||||
* @return string an AuthManager::ACTION_* constant.
|
||||
*/
|
||||
protected function getDefaultAction( $subPage ) {
|
||||
throw new BadMethodCallException( 'Subclass did not implement getDefaultAction' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return custom message key.
|
||||
* Allows subclasses to customize messages.
|
||||
* @return string
|
||||
*/
|
||||
protected function messageKey( $defaultKey ) {
|
||||
return array_key_exists( $defaultKey, static::$messages )
|
||||
? static::$messages[$defaultKey] : $defaultKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows blacklisting certain request types.
|
||||
* @return array A list of AuthenticationRequest subclass names
|
||||
*/
|
||||
protected function getRequestBlacklist() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load or initialize $authAction, $authRequests and $subPage.
|
||||
* Subclasses should call this from execute() or otherwise ensure the variables are initialized.
|
||||
* @param string $subPage Subpage of the special page.
|
||||
* @param string $authAction Override auth action specified in request (this is useful
|
||||
* when the form needs to be changed from <action> to <action>_CONTINUE after a successful
|
||||
* authentication step)
|
||||
* @param bool $reset Regenerate the requests even if a cached version is available
|
||||
*/
|
||||
protected function loadAuth( $subPage, $authAction = null, $reset = false ) {
|
||||
// Do not load if already loaded, to cut down on the number of getAuthenticationRequests
|
||||
// calls. This is important for requests which have hidden information so any
|
||||
// getAuthenticationRequests call would mean putting data into some cache.
|
||||
if (
|
||||
!$reset && $this->subPage === $subPage && $this->authAction
|
||||
&& ( !$authAction || $authAction === $this->authAction )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$request = $this->getRequest();
|
||||
$this->subPage = $subPage;
|
||||
$this->authAction = $authAction ?: $request->getText( 'authAction' );
|
||||
if ( !in_array( $this->authAction, static::$allowedActions, true ) ) {
|
||||
$this->authAction = $this->getDefaultAction( $subPage );
|
||||
if ( $request->wasPosted() ) {
|
||||
$continueAction = $this->getContinueAction( $this->authAction );
|
||||
if ( in_array( $continueAction, static::$allowedActions, true ) ) {
|
||||
$this->authAction = $continueAction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$allReqs = AuthManager::singleton()->getAuthenticationRequests(
|
||||
$this->authAction, $this->getUser() );
|
||||
$this->authRequests = array_filter( $allReqs, function ( $req ) use ( $subPage ) {
|
||||
return !in_array( get_class( $req ), $this->getRequestBlacklist(), true );
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this is not the first step of the authentication.
|
||||
* @return bool
|
||||
*/
|
||||
protected function isContinued() {
|
||||
return in_array( $this->authAction, [
|
||||
AuthManager::ACTION_LOGIN_CONTINUE,
|
||||
AuthManager::ACTION_CREATE_CONTINUE,
|
||||
AuthManager::ACTION_LINK_CONTINUE,
|
||||
], true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the _CONTINUE version of an action.
|
||||
* @param string $action An AuthManager::ACTION_* constant.
|
||||
* @return string An AuthManager::ACTION_*_CONTINUE constant.
|
||||
*/
|
||||
protected function getContinueAction( $action ) {
|
||||
switch ( $action ) {
|
||||
case AuthManager::ACTION_LOGIN:
|
||||
$action = AuthManager::ACTION_LOGIN_CONTINUE;
|
||||
break;
|
||||
case AuthManager::ACTION_CREATE:
|
||||
$action = AuthManager::ACTION_CREATE_CONTINUE;
|
||||
break;
|
||||
case AuthManager::ACTION_LINK:
|
||||
$action = AuthManager::ACTION_LINK_CONTINUE;
|
||||
break;
|
||||
}
|
||||
return $action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether AuthManager is ready to perform the action.
|
||||
* ACTION_CHANGE needs special verification (AuthManager::allowsAuthenticationData*) which is
|
||||
* the caller's responsibility.
|
||||
* @param string $action One of the AuthManager::ACTION_* constants in static::$allowedActions
|
||||
* @return bool
|
||||
* @throws LogicException if $action is invalid
|
||||
*/
|
||||
protected function isActionAllowed( $action ) {
|
||||
$authManager = AuthManager::singleton();
|
||||
if ( !in_array( $action, static::$allowedActions, true ) ) {
|
||||
throw new InvalidArgumentException( 'invalid action: ' . $action );
|
||||
}
|
||||
|
||||
// calling getAuthenticationRequests can be expensive, avoid if possible
|
||||
$requests = ( $action === $this->authAction ) ? $this->authRequests
|
||||
: $authManager->getAuthenticationRequests( $action );
|
||||
if ( !$requests ) {
|
||||
// no provider supports this action in the current state
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ( $action ) {
|
||||
case AuthManager::ACTION_LOGIN:
|
||||
case AuthManager::ACTION_LOGIN_CONTINUE:
|
||||
return $authManager->canAuthenticateNow();
|
||||
case AuthManager::ACTION_CREATE:
|
||||
case AuthManager::ACTION_CREATE_CONTINUE:
|
||||
return $authManager->canCreateAccounts();
|
||||
case AuthManager::ACTION_LINK:
|
||||
case AuthManager::ACTION_LINK_CONTINUE:
|
||||
return $authManager->canLinkAccounts();
|
||||
case AuthManager::ACTION_CHANGE:
|
||||
case AuthManager::ACTION_REMOVE:
|
||||
case AuthManager::ACTION_UNLINK:
|
||||
return true;
|
||||
default:
|
||||
// should never reach here but makes static code analyzers happy
|
||||
throw new InvalidArgumentException( 'invalid action: ' . $action );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action One of the AuthManager::ACTION_* constants
|
||||
* @param AuthenticationRequest[] $requests
|
||||
* @return AuthenticationResponse
|
||||
* @throws LogicException if $action is invalid
|
||||
*/
|
||||
protected function performAuthenticationStep( $action, array $requests ) {
|
||||
if ( !in_array( $action, static::$allowedActions, true ) ) {
|
||||
throw new InvalidArgumentException( 'invalid action: ' . $action );
|
||||
}
|
||||
|
||||
$authManager = AuthManager::singleton();
|
||||
$returnToUrl = $this->getPageTitle( 'return' )
|
||||
->getFullURL( $this->getPreservedParams( true ), false, PROTO_HTTPS );
|
||||
|
||||
switch ( $action ) {
|
||||
case AuthManager::ACTION_LOGIN:
|
||||
return $authManager->beginAuthentication( $requests, $returnToUrl );
|
||||
case AuthManager::ACTION_LOGIN_CONTINUE:
|
||||
return $authManager->continueAuthentication( $requests );
|
||||
case AuthManager::ACTION_CREATE:
|
||||
return $authManager->beginAccountCreation( $this->getUser(), $requests,
|
||||
$returnToUrl );
|
||||
case AuthManager::ACTION_CREATE_CONTINUE:
|
||||
return $authManager->continueAccountCreation( $requests );
|
||||
case AuthManager::ACTION_LINK:
|
||||
return $authManager->beginAccountLink( $this->getUser(), $requests, $returnToUrl );
|
||||
case AuthManager::ACTION_LINK_CONTINUE:
|
||||
return $authManager->continueAccountLink( $requests );
|
||||
case AuthManager::ACTION_CHANGE:
|
||||
case AuthManager::ACTION_REMOVE:
|
||||
case AuthManager::ACTION_UNLINK:
|
||||
if ( count( $requests ) > 1 ) {
|
||||
throw new InvalidArgumentException( 'only one auth request can be changed at a time' );
|
||||
} elseif ( !$requests ) {
|
||||
throw new InvalidArgumentException( 'no auth request' );
|
||||
}
|
||||
$req = reset( $requests );
|
||||
$status = $authManager->allowsAuthenticationDataChange( $req );
|
||||
Hooks::run( 'ChangeAuthenticationDataAudit', [ $req, $status ] );
|
||||
if ( !$status->isOK() ) {
|
||||
return AuthenticationResponse::newFail( $status->getMessage() );
|
||||
}
|
||||
$authManager->changeAuthenticationData( $req );
|
||||
return AuthenticationResponse::newPass();
|
||||
default:
|
||||
// should never reach here but makes static code analyzers happy
|
||||
throw new InvalidArgumentException( 'invalid action: ' . $action );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to do an authentication step with the submitted data.
|
||||
* Subclasses should probably call this from execute().
|
||||
* @return false|Status
|
||||
* - false if there was no submit at all
|
||||
* - a good Status wrapping an AuthenticationResponse if the form submit was successful.
|
||||
* This does not necessarily mean that the authentication itself was successful; see the
|
||||
* response for that.
|
||||
* - a bad Status for form errors.
|
||||
*/
|
||||
protected function trySubmit() {
|
||||
$status = false;
|
||||
|
||||
$form = $this->getAuthForm( $this->authRequests, $this->authAction );
|
||||
$form->setSubmitCallback( [ $this, 'handleFormSubmit' ] );
|
||||
|
||||
if ( $this->getRequest()->wasPosted() ) {
|
||||
// handle tokens manually; $form->tryAuthorizedSubmit only works for logged-in users
|
||||
$requestTokenValue = $this->getRequest()->getVal( $this->getTokenName() );
|
||||
$sessionToken = $this->getToken();
|
||||
if ( $sessionToken->wasNew() ) {
|
||||
return Status::newFatal( $this->messageKey( 'authform-newtoken' ) );
|
||||
} elseif ( !$requestTokenValue ) {
|
||||
return Status::newFatal( $this->messageKey( 'authform-notoken' ) );
|
||||
} elseif ( !$sessionToken->match( $requestTokenValue ) ) {
|
||||
return Status::newFatal( $this->messageKey( 'authform-wrongtoken' ) );
|
||||
}
|
||||
|
||||
$form->prepareForm();
|
||||
$status = $form->trySubmit();
|
||||
|
||||
// HTMLForm submit return values are a mess; let's ensure it is false or a Status
|
||||
// FIXME this probably should be in HTMLForm
|
||||
if ( $status === true ) {
|
||||
// not supposed to happen since our submit handler should always return a Status
|
||||
throw new UnexpectedValueException( 'HTMLForm::trySubmit() returned true' );
|
||||
} elseif ( $status === false ) {
|
||||
// form was not submitted; nothing to do
|
||||
} elseif ( $status instanceof Status ) {
|
||||
// already handled by the form; nothing to do
|
||||
} elseif ( $status instanceof StatusValue ) {
|
||||
// in theory not an allowed return type but nothing stops the submit handler from
|
||||
// accidentally returning it so best check and fix
|
||||
$status = Status::wrap( $status );
|
||||
} elseif ( is_string( $status ) ) {
|
||||
$status = Status::newFatal( new RawMessage( '$1', $status ) );
|
||||
} elseif ( is_array( $status ) ) {
|
||||
if ( is_string( reset( $status ) ) ) {
|
||||
$status = call_user_func_array( 'Status::newFatal', $status );
|
||||
} elseif ( is_array( reset( $status ) ) ) {
|
||||
$status = Status::newGood();
|
||||
foreach ( $status as $message ) {
|
||||
call_user_func_array( [ $status, 'fatal' ], $message );
|
||||
}
|
||||
} else {
|
||||
throw new UnexpectedValueException( 'invalid HTMLForm::trySubmit() return value: '
|
||||
. 'first element of array is ' . gettype( reset( $status ) ) );
|
||||
}
|
||||
} else {
|
||||
// not supposed to happen but HTMLForm does not actually verify the return type
|
||||
// from the submit callback; better safe then sorry
|
||||
throw new UnexpectedValueException( 'invalid HTMLForm::trySubmit() return type: '
|
||||
. gettype( $status ) );
|
||||
}
|
||||
|
||||
if ( ( !$status || !$status->isOK() ) && $this->isReturn ) {
|
||||
// This is awkward. There was a form validation error, which means the data was not
|
||||
// passed to AuthManager. Normally we would display the form with an error message,
|
||||
// but for the data we received via the redirect flow that would not be helpful at all.
|
||||
// Let's just submit the data to AuthManager directly instead.
|
||||
LoggerFactory::getInstance( 'authmanager' )
|
||||
->warning( 'Validation error on return', [ 'data' => $form->mFieldData,
|
||||
'status' => $status->getWikiText() ] );
|
||||
$status = $this->handleFormSubmit( $form->mFieldData );
|
||||
}
|
||||
}
|
||||
|
||||
$changeActions = [
|
||||
AuthManager::ACTION_CHANGE, AuthManager::ACTION_REMOVE, AuthManager::ACTION_UNLINK
|
||||
];
|
||||
if ( in_array( $this->authAction, $changeActions, true ) && $status && !$status->isOK() ) {
|
||||
Hooks::run( 'ChangeAuthenticationDataAudit', [ reset( $this->authRequests ), $status ] );
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit handler callback for HTMLForm
|
||||
* @private
|
||||
* @param $data array Submitted data
|
||||
* @return Status
|
||||
*/
|
||||
public function handleFormSubmit( $data ) {
|
||||
$requests = AuthenticationRequest::loadRequestsFromSubmission( $this->authRequests, $data );
|
||||
$response = $this->performAuthenticationStep( $this->authAction, $requests );
|
||||
|
||||
// we can't handle FAIL or similar as failure here since it might require changing the form
|
||||
return Status::newGood( $response );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns URL query parameters which can be used to reload the page (or leave and return) while
|
||||
* preserving all information that is necessary for authentication to continue. These parameters
|
||||
* will be preserved in the action URL of the form and in the return URL for redirect flow.
|
||||
* @param bool $withToken Include CSRF token
|
||||
* @return array
|
||||
*/
|
||||
protected function getPreservedParams( $withToken = false ) {
|
||||
$params = [];
|
||||
if ( $this->authAction !== $this->getDefaultAction( $this->subPage ) ) {
|
||||
$params['authAction'] = $this->getContinueAction( $this->authAction );
|
||||
}
|
||||
if ( $withToken ) {
|
||||
$params[$this->getTokenName()] = $this->getToken()->toString();
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a HTMLForm descriptor array from a set of authentication requests.
|
||||
* @param AuthenticationRequest[] $requests
|
||||
* @param string $action AuthManager action name (one of the AuthManager::ACTION_* constants)
|
||||
* @return array
|
||||
*/
|
||||
protected function getAuthFormDescriptor( $requests, $action ) {
|
||||
$fieldInfo = AuthenticationRequest::mergeFieldInfo( $requests );
|
||||
$formDescriptor = $this->fieldInfoToFormDescriptor( $requests, $fieldInfo, $action );
|
||||
|
||||
$this->addTabIndex( $formDescriptor );
|
||||
|
||||
return $formDescriptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AuthenticationRequest[] $requests
|
||||
* @param string $action AuthManager action name (one of the AuthManager::ACTION_* constants)
|
||||
* @return HTMLForm
|
||||
*/
|
||||
protected function getAuthForm( array $requests, $action ) {
|
||||
$formDescriptor = $this->getAuthFormDescriptor( $requests, $action );
|
||||
$context = $this->getContext();
|
||||
if ( $context->getRequest() !== $this->getRequest() ) {
|
||||
// We have overridden the request, need to make sure the form uses that too.
|
||||
$context = new DerivativeContext( $this->getContext() );
|
||||
$context->setRequest( $this->getRequest() );
|
||||
}
|
||||
$form = HTMLForm::factory( 'ooui', $formDescriptor, $context );
|
||||
$form->setAction( $this->getFullTitle()->getFullURL( $this->getPreservedParams() ) );
|
||||
$form->addHiddenField( $this->getTokenName(), $this->getToken()->toString() );
|
||||
$form->addHiddenField( 'authAction', $this->authAction );
|
||||
$form->suppressDefaultSubmit( !$this->needsSubmitButton( $formDescriptor ) );
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the form.
|
||||
* @param false|Status|StatusValue $status A form submit status, as in HTMLForm::trySubmit()
|
||||
*/
|
||||
protected function displayForm( $status ) {
|
||||
if ( $status instanceof StatusValue ) {
|
||||
$status = Status::wrap( $status );
|
||||
}
|
||||
$form = $this->getAuthForm( $this->authRequests, $this->authAction );
|
||||
$form->prepareForm()->displayForm( $status );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the form has fields which take values. If all available providers use the
|
||||
* redirect flow, the form might contain nothing but submit buttons, in which case we should
|
||||
* not add an extra submit button which does nothing.
|
||||
*
|
||||
* @param array $formDescriptor A HTMLForm descriptor
|
||||
* @return bool
|
||||
*/
|
||||
protected function needsSubmitButton( $formDescriptor ) {
|
||||
return (bool)array_filter( $formDescriptor, function ( $item ) {
|
||||
$class = false;
|
||||
if ( array_key_exists( 'class', $item ) ) {
|
||||
$class = $item['class'];
|
||||
} elseif ( array_key_exists( 'type', $item ) ) {
|
||||
$class = HTMLForm::$typeMappings[$item['type']];
|
||||
}
|
||||
return !in_array( $class, [ 'HTMLInfoField', 'HTMLSubmitField' ], true );
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a sequential tabindex starting from 1 to all form elements. This way the user can
|
||||
* use the tab key to traverse the form without having to step through all links and such.
|
||||
* @param $formDescriptor
|
||||
*/
|
||||
protected function addTabIndex( &$formDescriptor ) {
|
||||
$i = 1;
|
||||
foreach ( $formDescriptor as $field => &$definition ) {
|
||||
$class = false;
|
||||
if ( array_key_exists( 'class', $definition ) ) {
|
||||
$class = $definition['class'];
|
||||
} elseif ( array_key_exists( 'type', $definition ) ) {
|
||||
$class = HTMLForm::$typeMappings[$definition['type']];
|
||||
}
|
||||
if ( $class !== 'HTMLInfoField' ) {
|
||||
$definition['tabindex'] = $i;
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the CSRF token.
|
||||
* @return Token
|
||||
*/
|
||||
protected function getToken() {
|
||||
return $this->getRequest()->getSession()->getToken( 'AuthManagerSpecialPage:'
|
||||
. $this->getName() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the CSRF token (under which it should be found in the POST or GET data).
|
||||
* @return string
|
||||
*/
|
||||
protected function getTokenName() {
|
||||
return 'wpAuthToken';
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a field info array into a form descriptor. Behavior can be modified by the
|
||||
* AuthChangeFormFields hook.
|
||||
* @param AuthenticationRequest[] $requests
|
||||
* @param array $fieldInfo Field information, in the format used by
|
||||
* AuthenticationRequest::getFieldInfo()
|
||||
* @param string $action One of the AuthManager::ACTION_* constants
|
||||
* @return array A form descriptor that can be passed to HTMLForm
|
||||
*/
|
||||
protected function fieldInfoToFormDescriptor( array $requests, array $fieldInfo, $action ) {
|
||||
$formDescriptor = [];
|
||||
foreach ( $fieldInfo as $fieldName => $singleFieldInfo ) {
|
||||
$formDescriptor[$fieldName] = self::mapSingleFieldInfo( $singleFieldInfo, $fieldName );
|
||||
}
|
||||
|
||||
$requestSnapshot = serialize( $requests );
|
||||
$this->onAuthChangeFormFields( $requests, $fieldInfo, $formDescriptor, $action );
|
||||
\Hooks::run( 'AuthChangeFormFields', [ $requests, $fieldInfo, &$formDescriptor, $action ] );
|
||||
if ( $requestSnapshot !== serialize( $requests ) ) {
|
||||
LoggerFactory::getInstance( 'authentication' )->warning(
|
||||
'AuthChangeFormFields hook changed auth requests' );
|
||||
}
|
||||
|
||||
// Process the special 'weight' property, which is a way for AuthChangeFormFields hook
|
||||
// subscribers (who only see one field at a time) to influence ordering.
|
||||
self::sortFormDescriptorFields( $formDescriptor );
|
||||
|
||||
return $formDescriptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps an authentication field configuration for a single field (as returned by
|
||||
* AuthenticationRequest::getFieldInfo()) to a HTMLForm field descriptor.
|
||||
* @param array $singleFieldInfo
|
||||
* @return array
|
||||
*/
|
||||
protected static function mapSingleFieldInfo( $singleFieldInfo, $fieldName ) {
|
||||
$type = self::mapFieldInfoTypeToFormDescriptorType( $singleFieldInfo['type'] );
|
||||
$descriptor = [
|
||||
'type' => $type,
|
||||
// Do not prefix input name with 'wp'. This is important for the redirect flow.
|
||||
'name' => $fieldName,
|
||||
];
|
||||
|
||||
if ( $type === 'submit' && isset( $singleFieldInfo['label'] ) ) {
|
||||
$descriptor['default'] = wfMessage( $singleFieldInfo['label'] )->plain();
|
||||
} elseif ( $type !== 'submit' ) {
|
||||
$descriptor += array_filter( [
|
||||
// help-message is omitted as it is usually not really useful for a web interface
|
||||
'label-message' => self::getField( $singleFieldInfo, 'label' ),
|
||||
] );
|
||||
|
||||
if ( isset( $singleFieldInfo['options'] ) ) {
|
||||
$descriptor['options'] = array_flip( array_map( function ( $message ) {
|
||||
/** @var $message Message */
|
||||
return $message->parse();
|
||||
}, $singleFieldInfo['options'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $singleFieldInfo['value'] ) ) {
|
||||
$descriptor['default'] = $singleFieldInfo['value'];
|
||||
}
|
||||
|
||||
if ( empty( $singleFieldInfo['optional'] ) ) {
|
||||
$descriptor['required'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $descriptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the fields of a form descriptor by their 'weight' property. (Fields with higher weight
|
||||
* are shown closer to the bottom; weight defaults to 0. Negative weight is allowed.)
|
||||
* Keep order if weights are equal.
|
||||
* @param array $formDescriptor
|
||||
* @return array
|
||||
*/
|
||||
protected static function sortFormDescriptorFields( array &$formDescriptor ) {
|
||||
$i = 0;
|
||||
foreach ( $formDescriptor as &$field ) {
|
||||
$field['__index'] = $i++;
|
||||
}
|
||||
uasort( $formDescriptor, function ( $first, $second ) {
|
||||
return self::getField( $first, 'weight', 0 ) - self::getField( $second, 'weight', 0 )
|
||||
?: $first['__index'] - $second['__index'];
|
||||
} );
|
||||
foreach ( $formDescriptor as &$field ) {
|
||||
unset( $field['__index'] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array value, or a default if it does not exist.
|
||||
* @param array $array
|
||||
* @param string $fieldName
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
protected static function getField( array $array, $fieldName, $default = null ) {
|
||||
if ( array_key_exists( $fieldName, $array ) ) {
|
||||
return $array[$fieldName];
|
||||
} else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps AuthenticationRequest::getFieldInfo() types to HTMLForm types
|
||||
* @param string $type
|
||||
* @return string
|
||||
* @throws \LogicException
|
||||
*/
|
||||
protected static function mapFieldInfoTypeToFormDescriptorType( $type ) {
|
||||
$map = [
|
||||
'string' => 'text',
|
||||
'password' => 'password',
|
||||
'select' => 'select',
|
||||
'checkbox' => 'check',
|
||||
'multiselect' => 'multiselect',
|
||||
'button' => 'submit',
|
||||
'hidden' => 'hidden',
|
||||
'null' => 'info',
|
||||
];
|
||||
if ( !array_key_exists( $type, $map ) ) {
|
||||
throw new \LogicException( 'invalid field type: ' . $type );
|
||||
}
|
||||
return $map[$type];
|
||||
}
|
||||
}
|
||||
1636
includes/specialpage/LoginSignupSpecialPage.php
Normal file
1636
includes/specialpage/LoginSignupSpecialPage.php
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -23,6 +23,8 @@ use MediaWiki\MediaWikiServices;
|
|||
* @ingroup SpecialPage
|
||||
*/
|
||||
|
||||
use MediaWiki\Auth\AuthManager;
|
||||
|
||||
/**
|
||||
* Parent class for all special pages.
|
||||
*
|
||||
|
|
@ -296,6 +298,66 @@ class SpecialPage {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the special page does something security-sensitive and needs extra defense against
|
||||
* a stolen account (e.g. a reauthentication). What exactly that will mean is decided by the
|
||||
* authentication framework.
|
||||
* @return bool|string False or the argument for AuthManager::securitySensitiveOperationStatus().
|
||||
* Typically a special page needing elevated security would return its name here.
|
||||
*/
|
||||
protected function getLoginSecurityLevel() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the user meets the security level, possibly reauthenticating them in the process.
|
||||
*
|
||||
* This should be used when the page does something security-sensitive and needs extra defense
|
||||
* against a stolen account (e.g. a reauthentication). The authentication framework will make
|
||||
* an extra effort to make sure the user account is not compromised. What that exactly means
|
||||
* will depend on the system and user settings; e.g. the user might be required to log in again
|
||||
* unless their last login happened recently, or they might be given a second-factor challenge.
|
||||
*
|
||||
* Calling this method will result in one if these actions:
|
||||
* - return true: all good.
|
||||
* - return false and set a redirect: caller should abort; the redirect will take the user
|
||||
* to the login page for reauthentication, and back.
|
||||
* - throw an exception if there is no way for the user to meet the requirements without using
|
||||
* a different access method (e.g. this functionality is only available from a specific IP).
|
||||
*
|
||||
* Note that this does not in any way check that the user is authorized to use this special page
|
||||
* (use checkPermissions() for that).
|
||||
*
|
||||
* @param string $level A security level. Can be an arbitrary string, defaults to the page name.
|
||||
* @return bool False means a redirect to the reauthentication page has been set and processing
|
||||
* of the special page should be aborted.
|
||||
* @throws ErrorPageError If the security level cannot be met, even with reauthentication.
|
||||
*/
|
||||
protected function checkLoginSecurityLevel( $level = null ) {
|
||||
$level = $level ?: $this->getName();
|
||||
$securityStatus = AuthManager::singleton()->securitySensitiveOperationStatus( $level );
|
||||
if ( $securityStatus === AuthManager::SEC_OK ) {
|
||||
return true;
|
||||
} elseif ( $securityStatus === AuthManager::SEC_REAUTH ) {
|
||||
$request = $this->getRequest();
|
||||
$title = SpecialPage::getTitleFor( 'Userlogin' );
|
||||
$query = [
|
||||
'returnto' => $this->getFullTitle()->getPrefixedDBkey(),
|
||||
'returntoquery' => wfArrayToCgi( array_diff_key( $request->getQueryValues(),
|
||||
[ 'title' => true ] ) ),
|
||||
'force' => $level,
|
||||
];
|
||||
$url = $title->getFullURL( $query, false, PROTO_HTTPS );
|
||||
|
||||
$this->getOutput()->redirect( $url );
|
||||
return false;
|
||||
}
|
||||
|
||||
$titleMessage = wfMessage( 'specialpage-securitylevel-not-allowed-title' );
|
||||
$errorMessage = wfMessage( 'specialpage-securitylevel-not-allowed' );
|
||||
throw new ErrorPageError( $titleMessage, $errorMessage );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of subpages beginning with $search that this special page will accept.
|
||||
*
|
||||
|
|
@ -463,6 +525,7 @@ class SpecialPage {
|
|||
public function execute( $subPage ) {
|
||||
$this->setHeaders();
|
||||
$this->checkPermissions();
|
||||
$this->checkLoginSecurityLevel( $this->getLoginSecurityLevel() );
|
||||
$this->outputHeader();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,18 +81,23 @@ class SpecialPageFactory {
|
|||
'PagesWithProp' => 'SpecialPagesWithProp',
|
||||
'TrackingCategories' => 'SpecialTrackingCategories',
|
||||
|
||||
// Login/create account
|
||||
'Userlogin' => 'LoginForm',
|
||||
'CreateAccount' => 'SpecialCreateAccount',
|
||||
// Authentication
|
||||
'Userlogin' => 'SpecialUserLogin',
|
||||
'Userlogout' => 'SpecialUserLogoutPreAuthManager',
|
||||
'CreateAccount' => 'SpecialCreateAccountPreAuthManager',
|
||||
'LinkAccounts' => 'SpecialLinkAccounts',
|
||||
'UnlinkAccounts' => 'SpecialUnlinkAccounts',
|
||||
'ChangeCredentials' => 'SpecialChangeCredentials',
|
||||
'RemoveCredentials' => 'SpecialRemoveCredentials',
|
||||
|
||||
// Users and rights
|
||||
'Activeusers' => 'SpecialActiveUsers',
|
||||
'Block' => 'SpecialBlock',
|
||||
'Unblock' => 'SpecialUnblock',
|
||||
'BlockList' => 'SpecialBlockList',
|
||||
'ChangePassword' => 'SpecialChangePassword',
|
||||
'ChangePassword' => 'SpecialChangePasswordPreAuthManager',
|
||||
'BotPasswords' => 'SpecialBotPasswords',
|
||||
'PasswordReset' => 'SpecialPasswordReset',
|
||||
'PasswordReset' => 'SpecialPasswordResetPreAuthManager',
|
||||
'DeletedContributions' => 'DeletedContributionsPage',
|
||||
'Preferences' => 'SpecialPreferences',
|
||||
'ResetTokens' => 'SpecialResetTokens',
|
||||
|
|
@ -178,7 +183,6 @@ class SpecialPageFactory {
|
|||
'Revisiondelete' => 'SpecialRevisionDelete',
|
||||
'RunJobs' => 'SpecialRunJobs',
|
||||
'Specialpages' => 'SpecialSpecialpages',
|
||||
'Userlogout' => 'SpecialUserlogout',
|
||||
];
|
||||
|
||||
private static $list;
|
||||
|
|
@ -226,6 +230,7 @@ class SpecialPageFactory {
|
|||
global $wgDisableInternalSearch, $wgEmailAuthentication;
|
||||
global $wgEnableEmail, $wgEnableJavaScriptTest;
|
||||
global $wgPageLanguageUseDB, $wgContentHandlerUseDB;
|
||||
global $wgDisableAuthManager;
|
||||
|
||||
if ( !is_array( self::$list ) ) {
|
||||
|
||||
|
|
@ -241,7 +246,7 @@ class SpecialPageFactory {
|
|||
}
|
||||
|
||||
if ( $wgEnableEmail ) {
|
||||
self::$list['ChangeEmail'] = 'SpecialChangeEmail';
|
||||
self::$list['ChangeEmail'] = 'SpecialChangeEmailPreAuthManager';
|
||||
}
|
||||
|
||||
if ( $wgEnableJavaScriptTest ) {
|
||||
|
|
@ -255,6 +260,19 @@ class SpecialPageFactory {
|
|||
self::$list['ChangeContentModel'] = 'SpecialChangeContentModel';
|
||||
}
|
||||
|
||||
// horrible hack to allow selection between old and new classes via a feature flag - T110756
|
||||
// will be removed once AuthManager is stable
|
||||
if ( !$wgDisableAuthManager ) {
|
||||
self::$list = array_map( function ( $class ) {
|
||||
return preg_replace( '/PreAuthManager$/', '', $class );
|
||||
}, self::$list );
|
||||
} else {
|
||||
self::$list['Userlogin'] = 'LoginForm';
|
||||
self::$list = array_diff_key( self::$list, array_fill_keys( [
|
||||
'LinkAccounts', 'UnlinkAccounts', 'ChangeCredentials', 'RemoveCredentials',
|
||||
], true ) );
|
||||
}
|
||||
|
||||
// Add extension special pages
|
||||
self::$list = array_merge( self::$list, $wgSpecialPages );
|
||||
|
||||
|
|
|
|||
252
includes/specials/SpecialChangeCredentials.php
Normal file
252
includes/specials/SpecialChangeCredentials.php
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\Auth\AuthenticationRequest;
|
||||
use MediaWiki\Auth\AuthenticationResponse;
|
||||
use MediaWiki\Auth\AuthManager;
|
||||
use MediaWiki\Session\SessionManager;
|
||||
|
||||
/**
|
||||
* Special change to change credentials (such as the password).
|
||||
*
|
||||
* Also does most of the work for SpecialRemoveCredentials.
|
||||
*/
|
||||
class SpecialChangeCredentials extends AuthManagerSpecialPage {
|
||||
protected static $allowedActions = [ AuthManager::ACTION_CHANGE ];
|
||||
|
||||
protected static $messagePrefix = 'changecredentials';
|
||||
|
||||
/** Change action needs user data; remove action does not */
|
||||
protected static $loadUserData = true;
|
||||
|
||||
public function __construct( $name = 'ChangeCredentials' ) {
|
||||
parent::__construct( $name, 'editmyprivateinfo' );
|
||||
}
|
||||
|
||||
protected function getGroupName() {
|
||||
return 'users';
|
||||
}
|
||||
|
||||
public function isListed() {
|
||||
$this->loadAuth( '' );
|
||||
return (bool)$this->authRequests;
|
||||
}
|
||||
|
||||
public function doesWrites() {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getDefaultAction( $subPage ) {
|
||||
return AuthManager::ACTION_CHANGE;
|
||||
}
|
||||
|
||||
protected function getPreservedParams( $withToken = false ) {
|
||||
$request = $this->getRequest();
|
||||
$params = parent::getPreservedParams( $withToken );
|
||||
$params += [
|
||||
'returnto' => $request->getVal( 'returnto' ),
|
||||
'returntoquery' => $request->getVal( 'returntoquery' ),
|
||||
];
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function onAuthChangeFormFields(
|
||||
array $requests, array $fieldInfo, array &$formDescriptor, $action
|
||||
) {
|
||||
// This method is never called for remove actions.
|
||||
|
||||
$extraFields = [];
|
||||
Hooks::run( 'ChangePasswordForm', [ &$extraFields ], '1.27' );
|
||||
foreach ( $extraFields as $extra ) {
|
||||
list( $name, $label, $type, $default ) = $extra;
|
||||
$formDescriptor[$name] = [
|
||||
'type' => $type,
|
||||
'name' => $name,
|
||||
'label-message' => $label,
|
||||
'default' => $default,
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
return parent::onAuthChangeFormFields( $requests, $fieldInfo, $formDescriptor, $action );
|
||||
}
|
||||
|
||||
public function execute( $subPage ) {
|
||||
$this->setHeaders();
|
||||
$this->outputHeader();
|
||||
|
||||
$this->loadAuth( $subPage );
|
||||
|
||||
if ( !$subPage ) {
|
||||
$this->showSubpageList();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $this->getRequest()->getCheck( 'wpCancel' ) ) {
|
||||
$returnUrl = $this->getReturnUrl() ?: Title::newMainPage()->getFullURL();
|
||||
$this->getOutput()->redirect( $returnUrl );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !$this->authRequests ) {
|
||||
// messages used: changecredentials-invalidsubpage, removecredentials-invalidsubpage
|
||||
$this->showSubpageList( $this->msg( static::$messagePrefix . '-invalidsubpage', $subPage ) );
|
||||
return;
|
||||
}
|
||||
|
||||
$status = $this->trySubmit();
|
||||
|
||||
if ( $status === false || !$status->isOK() ) {
|
||||
$this->displayForm( $status );
|
||||
return;
|
||||
}
|
||||
|
||||
$response = $status->getValue();
|
||||
|
||||
switch ( $response->status ) {
|
||||
case AuthenticationResponse::PASS:
|
||||
$this->success();
|
||||
break;
|
||||
case AuthenticationResponse::FAIL:
|
||||
$this->displayForm( Status::newFatal( $response->message ) );
|
||||
break;
|
||||
default:
|
||||
throw new LogicException( 'invalid AuthenticationResponse' );
|
||||
}
|
||||
}
|
||||
|
||||
protected function loadAuth( $subPage, $authAction = null, $reset = false ) {
|
||||
parent::loadAuth( $subPage, $authAction );
|
||||
if ( $subPage ) {
|
||||
$this->authRequests = array_filter( $this->authRequests, function ( $req ) use ( $subPage ) {
|
||||
return $req->getUniqueId() === $subPage;
|
||||
} );
|
||||
if ( count( $this->authRequests ) > 1 ) {
|
||||
throw new LogicException( 'Multiple AuthenticationRequest objects with same ID!' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getAuthFormDescriptor( $requests, $action ) {
|
||||
if ( !static::$loadUserData ) {
|
||||
return [];
|
||||
} else {
|
||||
return parent::getAuthFormDescriptor( $requests, $action );
|
||||
}
|
||||
}
|
||||
|
||||
protected function getAuthForm( array $requests, $action ) {
|
||||
$form = parent::getAuthForm( $requests, $action );
|
||||
$req = reset( $requests );
|
||||
$info = $req->describeCredentials();
|
||||
|
||||
$form->addPreText(
|
||||
Html::openElement( 'dl' )
|
||||
. Html::element( 'dt', [], wfMessage( 'credentialsform-provider' ) )
|
||||
. Html::element( 'dd', [], $info['provider'] )
|
||||
. Html::element( 'dt', [], wfMessage( 'credentialsform-account' ) )
|
||||
. Html::element( 'dd', [], $info['account'] )
|
||||
. Html::closeElement( 'dl' )
|
||||
);
|
||||
|
||||
// messages used: changecredentials-submit removecredentials-submit
|
||||
// changecredentials-submit-cancel removecredentials-submit-cancel
|
||||
$form->setSubmitTextMsg( static::$messagePrefix . '-submit' );
|
||||
$form->addButton( [
|
||||
'name' => 'wpCancel',
|
||||
'value' => $this->msg( static::$messagePrefix . '-submit-cancel' )->text()
|
||||
] );
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
protected function needsSubmitButton( $formDescriptor ) {
|
||||
// Change/remove forms show are built from a single AuthenticationRequest and do not allow
|
||||
// for redirect flow; they always need a submit button.
|
||||
return true;
|
||||
}
|
||||
|
||||
public function handleFormSubmit( $data ) {
|
||||
// remove requests do not accept user input
|
||||
$requests = $this->authRequests;
|
||||
if ( static::$loadUserData ) {
|
||||
$requests = AuthenticationRequest::loadRequestsFromSubmission( $this->authRequests, $data );
|
||||
}
|
||||
|
||||
$response = $this->performAuthenticationStep( $this->authAction, $requests );
|
||||
|
||||
// we can't handle FAIL or similar as failure here since it might require changing the form
|
||||
return Status::newGood( $response );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Message|null $error
|
||||
*/
|
||||
protected function showSubpageList( $error = null ) {
|
||||
$out = $this->getOutput();
|
||||
|
||||
if ( $error ) {
|
||||
$out->addHTML( $error->parse() );
|
||||
}
|
||||
|
||||
$groupedRequests = [];
|
||||
foreach ( $this->authRequests as $req ) {
|
||||
$info = $req->describeCredentials();
|
||||
$groupedRequests[(string)$info['provider']][] = $req;
|
||||
}
|
||||
|
||||
$out->addHTML( Html::openElement( 'dl' ) );
|
||||
foreach ( $groupedRequests as $group => $members ) {
|
||||
$out->addHTML( Html::element( 'dt', [], $group ) );
|
||||
foreach ( $members as $req ) {
|
||||
/** @var AuthenticationRequest $req */
|
||||
$info = $req->describeCredentials();
|
||||
$out->addHTML( Html::rawElement( 'dd', [],
|
||||
Linker::link( $this->getPageTitle( $req->getUniqueId() ),
|
||||
htmlspecialchars( $info['account'], ENT_QUOTES ) )
|
||||
) );
|
||||
}
|
||||
}
|
||||
$out->addHTML( Html::closeElement( 'dl' ) );
|
||||
}
|
||||
|
||||
protected function success() {
|
||||
$session = $this->getRequest()->getSession();
|
||||
$user = $this->getUser();
|
||||
$out = $this->getOutput();
|
||||
$returnUrl = $this->getReturnUrl();
|
||||
|
||||
// change user token and update the session
|
||||
SessionManager::singleton()->invalidateSessionsForUser( $user );
|
||||
$session->setUser( $user );
|
||||
$session->resetId();
|
||||
|
||||
if ( $returnUrl ) {
|
||||
$out->redirect( $returnUrl );
|
||||
} else {
|
||||
// messages used: changecredentials-success removecredentials-success
|
||||
$out->wrapWikiMsg( "<div class=\"successbox\">\n$1\n</div>", static::$messagePrefix
|
||||
. '-success' );
|
||||
$out->returnToMain();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
protected function getReturnUrl() {
|
||||
$request = $this->getRequest();
|
||||
$returnTo = $request->getText( 'returnto' );
|
||||
$returnToQuery = $request->getText( 'returntoquery', '' );
|
||||
|
||||
if ( !$returnTo ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$title = Title::newFromText( $returnTo );
|
||||
return $title->getFullURL( $returnToQuery );
|
||||
}
|
||||
|
||||
protected function getRequestBlacklist() {
|
||||
return $this->getConfig()->get( 'ChangeCredentialsBlacklist' );
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,8 @@
|
|||
* @ingroup SpecialPage
|
||||
*/
|
||||
|
||||
use MediaWiki\Auth\AuthManager;
|
||||
|
||||
/**
|
||||
* Let users change their email address.
|
||||
*
|
||||
|
|
@ -44,9 +46,7 @@ class SpecialChangeEmail extends FormSpecialPage {
|
|||
* @return bool
|
||||
*/
|
||||
public function isListed() {
|
||||
global $wgAuth;
|
||||
|
||||
return $wgAuth->allowPropChange( 'emailaddress' );
|
||||
return AuthManager::singleton()->allowsPropertyChange( 'emailaddress' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -54,6 +54,8 @@ class SpecialChangeEmail extends FormSpecialPage {
|
|||
* @param string $par
|
||||
*/
|
||||
function execute( $par ) {
|
||||
$this->checkLoginSecurityLevel();
|
||||
|
||||
$out = $this->getOutput();
|
||||
$out->disallowUserJs();
|
||||
|
||||
|
|
@ -61,9 +63,8 @@ class SpecialChangeEmail extends FormSpecialPage {
|
|||
}
|
||||
|
||||
protected function checkExecutePermissions( User $user ) {
|
||||
global $wgAuth;
|
||||
|
||||
if ( !$wgAuth->allowPropChange( 'emailaddress' ) ) {
|
||||
if ( !AuthManager::singleton()->allowsPropertyChange( 'emailaddress' ) ) {
|
||||
throw new ErrorPageError( 'changeemail', 'cannotchangeemail' );
|
||||
}
|
||||
|
||||
|
|
@ -100,13 +101,6 @@ class SpecialChangeEmail extends FormSpecialPage {
|
|||
],
|
||||
];
|
||||
|
||||
if ( $this->getConfig()->get( 'RequirePasswordforEmailChange' ) ) {
|
||||
$fields['Password'] = [
|
||||
'type' => 'password',
|
||||
'label-message' => 'changeemail-password'
|
||||
];
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
|
|
@ -121,14 +115,10 @@ class SpecialChangeEmail extends FormSpecialPage {
|
|||
$form->addHiddenFields( $this->getRequest()->getValues( 'returnto', 'returntoquery' ) );
|
||||
|
||||
$form->addHeaderText( $this->msg( 'changeemail-header' )->parseAsBlock() );
|
||||
if ( $this->getConfig()->get( 'RequirePasswordforEmailChange' ) ) {
|
||||
$form->addHeaderText( $this->msg( 'changeemail-passwordrequired' )->parseAsBlock() );
|
||||
}
|
||||
}
|
||||
|
||||
public function onSubmit( array $data ) {
|
||||
$password = isset( $data['Password'] ) ? $data['Password'] : null;
|
||||
$status = $this->attemptChange( $this->getUser(), $password, $data['NewEmail'] );
|
||||
$status = $this->attemptChange( $this->getUser(), $data['NewEmail'] );
|
||||
|
||||
$this->status = $status;
|
||||
|
||||
|
|
@ -158,11 +148,12 @@ class SpecialChangeEmail extends FormSpecialPage {
|
|||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param string $pass
|
||||
* @param string $newaddr
|
||||
* @return Status
|
||||
*/
|
||||
private function attemptChange( User $user, $pass, $newaddr ) {
|
||||
private function attemptChange( User $user, $newaddr ) {
|
||||
$authManager = AuthManager::singleton();
|
||||
|
||||
if ( $newaddr != '' && !Sanitizer::validateEmail( $newaddr ) ) {
|
||||
return Status::newFatal( 'invalidemailaddress' );
|
||||
}
|
||||
|
|
@ -171,24 +162,6 @@ class SpecialChangeEmail extends FormSpecialPage {
|
|||
return Status::newFatal( 'changeemail-nochange' );
|
||||
}
|
||||
|
||||
$throttleInfo = LoginForm::incrementLoginThrottle( $user->getName() );
|
||||
if ( $throttleInfo ) {
|
||||
$lang = $this->getLanguage();
|
||||
return Status::newFatal(
|
||||
'changeemail-throttled',
|
||||
$lang->formatDuration( $throttleInfo['wait'] )
|
||||
);
|
||||
}
|
||||
|
||||
if ( $this->getConfig()->get( 'RequirePasswordforEmailChange' )
|
||||
&& !$user->checkTemporaryPassword( $pass )
|
||||
&& !$user->checkPassword( $pass )
|
||||
) {
|
||||
return Status::newFatal( 'wrongpassword' );
|
||||
}
|
||||
|
||||
LoginForm::clearLoginThrottle( $user->getName() );
|
||||
|
||||
$oldaddr = $user->getEmail();
|
||||
$status = $user->setEmailWithConfirmation( $newaddr );
|
||||
if ( !$status->isGood() ) {
|
||||
|
|
|
|||
|
|
@ -21,323 +21,16 @@
|
|||
* @ingroup SpecialPage
|
||||
*/
|
||||
|
||||
use MediaWiki\Auth\PasswordAuthenticationRequest;
|
||||
|
||||
/**
|
||||
* Let users recover their password.
|
||||
*
|
||||
* @ingroup SpecialPage
|
||||
*/
|
||||
class SpecialChangePassword extends FormSpecialPage {
|
||||
protected $mUserName;
|
||||
protected $mDomain;
|
||||
|
||||
// Optional Wikitext Message to show above the password change form
|
||||
protected $mPreTextMessage = null;
|
||||
|
||||
// label for old password input
|
||||
protected $mOldPassMsg = null;
|
||||
|
||||
class SpecialChangePassword extends SpecialRedirectToSpecial {
|
||||
public function __construct() {
|
||||
parent::__construct( 'ChangePassword', 'editmyprivateinfo' );
|
||||
$this->listed( false );
|
||||
}
|
||||
|
||||
public function doesWrites() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execution point
|
||||
* @param string|null $par
|
||||
*/
|
||||
function execute( $par ) {
|
||||
$this->getOutput()->disallowUserJs();
|
||||
|
||||
parent::execute( $par );
|
||||
}
|
||||
|
||||
protected function checkExecutePermissions( User $user ) {
|
||||
parent::checkExecutePermissions( $user );
|
||||
|
||||
if ( !$this->getRequest()->wasPosted() ) {
|
||||
$this->requireLogin( 'resetpass-no-info' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a message at the top of the Change Password form
|
||||
* @since 1.23
|
||||
* @param Message $msg Message to parse and add to the form header
|
||||
*/
|
||||
public function setChangeMessage( Message $msg ) {
|
||||
$this->mPreTextMessage = $msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a message at the top of the Change Password form
|
||||
* @since 1.23
|
||||
* @param string $msg Message label for old/temp password field
|
||||
*/
|
||||
public function setOldPasswordMessage( $msg ) {
|
||||
$this->mOldPassMsg = $msg;
|
||||
}
|
||||
|
||||
protected function getFormFields() {
|
||||
$user = $this->getUser();
|
||||
$request = $this->getRequest();
|
||||
|
||||
$oldpassMsg = $this->mOldPassMsg;
|
||||
if ( $oldpassMsg === null ) {
|
||||
$oldpassMsg = $user->isLoggedIn() ? 'oldpassword' : 'resetpass-temp-password';
|
||||
}
|
||||
|
||||
$fields = [
|
||||
'Name' => [
|
||||
'type' => 'info',
|
||||
'label-message' => 'username',
|
||||
'default' => $request->getVal( 'wpName', $user->getName() ),
|
||||
],
|
||||
'Password' => [
|
||||
'type' => 'password',
|
||||
'label-message' => $oldpassMsg,
|
||||
],
|
||||
'NewPassword' => [
|
||||
'type' => 'password',
|
||||
'label-message' => 'newpassword',
|
||||
],
|
||||
'Retype' => [
|
||||
'type' => 'password',
|
||||
'label-message' => 'retypenew',
|
||||
],
|
||||
];
|
||||
|
||||
if ( !$this->getUser()->isLoggedIn() ) {
|
||||
$fields['LoginOnChangeToken'] = [
|
||||
'type' => 'hidden',
|
||||
'label' => 'Change Password Token',
|
||||
'default' => LoginForm::getLoginToken()->toString(),
|
||||
];
|
||||
}
|
||||
|
||||
$extraFields = [];
|
||||
Hooks::run( 'ChangePasswordForm', [ &$extraFields ] );
|
||||
foreach ( $extraFields as $extra ) {
|
||||
list( $name, $label, $type, $default ) = $extra;
|
||||
$fields[$name] = [
|
||||
'type' => $type,
|
||||
'name' => $name,
|
||||
'label-message' => $label,
|
||||
'default' => $default,
|
||||
];
|
||||
}
|
||||
|
||||
if ( !$user->isLoggedIn() ) {
|
||||
$fields['Remember'] = [
|
||||
'type' => 'check',
|
||||
'label' => $this->msg( 'remembermypassword' )
|
||||
->numParams(
|
||||
ceil( $this->getConfig()->get( 'CookieExpiration' ) / ( 3600 * 24 ) )
|
||||
)->text(),
|
||||
'default' => $request->getVal( 'wpRemember' ),
|
||||
];
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
protected function alterForm( HTMLForm $form ) {
|
||||
$form->setId( 'mw-resetpass-form' );
|
||||
$form->setTableId( 'mw-resetpass-table' );
|
||||
$form->setWrapperLegendMsg( 'resetpass_header' );
|
||||
$form->setSubmitTextMsg(
|
||||
$this->getUser()->isLoggedIn()
|
||||
? 'resetpass-submit-loggedin'
|
||||
: 'resetpass_submit'
|
||||
);
|
||||
$form->addButton( [
|
||||
'name' => 'wpCancel',
|
||||
'value' => $this->msg( 'resetpass-submit-cancel' )->text()
|
||||
] );
|
||||
$form->setHeaderText( $this->msg( 'resetpass_text' )->parseAsBlock() );
|
||||
if ( $this->mPreTextMessage instanceof Message ) {
|
||||
$form->addPreText( $this->mPreTextMessage->parseAsBlock() );
|
||||
}
|
||||
$form->addHiddenFields(
|
||||
$this->getRequest()->getValues( 'wpName', 'wpDomain', 'returnto', 'returntoquery' ) );
|
||||
}
|
||||
|
||||
public function onSubmit( array $data ) {
|
||||
global $wgAuth;
|
||||
|
||||
$request = $this->getRequest();
|
||||
|
||||
if ( $request->getCheck( 'wpLoginToken' ) ) {
|
||||
// This comes from Special:Userlogin when logging in with a temporary password
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( !$this->getUser()->isLoggedIn()
|
||||
&& !LoginForm::getLoginToken()->match( $request->getVal( 'wpLoginOnChangeToken' ) )
|
||||
) {
|
||||
// Potential CSRF (bug 62497)
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $request->getCheck( 'wpCancel' ) ) {
|
||||
$returnto = $request->getVal( 'returnto' );
|
||||
$titleObj = $returnto !== null ? Title::newFromText( $returnto ) : null;
|
||||
if ( !$titleObj instanceof Title ) {
|
||||
$titleObj = Title::newMainPage();
|
||||
}
|
||||
$query = $request->getVal( 'returntoquery' );
|
||||
$this->getOutput()->redirect( $titleObj->getFullURL( $query ) );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->mUserName = $request->getVal( 'wpName', $this->getUser()->getName() );
|
||||
$this->mDomain = $wgAuth->getDomain();
|
||||
|
||||
if ( !$wgAuth->allowPasswordChange() ) {
|
||||
throw new ErrorPageError( 'changepassword', 'resetpass_forbidden' );
|
||||
}
|
||||
|
||||
$status = $this->attemptReset( $data['Password'], $data['NewPassword'], $data['Retype'] );
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
public function onSuccess() {
|
||||
if ( $this->getUser()->isLoggedIn() ) {
|
||||
$this->getOutput()->wrapWikiMsg(
|
||||
"<div class=\"successbox\">\n$1\n</div>",
|
||||
'changepassword-success'
|
||||
);
|
||||
$this->getOutput()->returnToMain();
|
||||
} else {
|
||||
$request = $this->getRequest();
|
||||
LoginForm::clearLoginToken();
|
||||
$token = LoginForm::getLoginToken()->toString();
|
||||
$data = [
|
||||
'action' => 'submitlogin',
|
||||
'wpName' => $this->mUserName,
|
||||
'wpDomain' => $this->mDomain,
|
||||
'wpLoginToken' => $token,
|
||||
'wpPassword' => $request->getVal( 'wpNewPassword' ),
|
||||
] + $request->getValues( 'wpRemember', 'returnto', 'returntoquery' );
|
||||
$login = new LoginForm( new DerivativeRequest( $request, $data, true ) );
|
||||
$login->setContext( $this->getContext() );
|
||||
$login->execute( null );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the new password if it meets the requirements for passwords and set
|
||||
* it as a current password, otherwise set the passed Status object to fatal
|
||||
* and doesn't change anything
|
||||
*
|
||||
* @param string $oldpass The current (temporary) password.
|
||||
* @param string $newpass The password to set.
|
||||
* @param string $retype The string of the retype password field to check with newpass
|
||||
* @return Status
|
||||
*/
|
||||
protected function attemptReset( $oldpass, $newpass, $retype ) {
|
||||
$isSelf = ( $this->mUserName === $this->getUser()->getName() );
|
||||
if ( $isSelf ) {
|
||||
$user = $this->getUser();
|
||||
} else {
|
||||
$user = User::newFromName( $this->mUserName );
|
||||
}
|
||||
|
||||
if ( !$user || $user->isAnon() ) {
|
||||
return Status::newFatal( $this->msg( 'nosuchusershort', $this->mUserName ) );
|
||||
}
|
||||
|
||||
if ( $newpass !== $retype ) {
|
||||
Hooks::run( 'PrefsPasswordAudit', [ $user, $newpass, 'badretype' ] );
|
||||
return Status::newFatal( $this->msg( 'badretype' ) );
|
||||
}
|
||||
|
||||
$throttleInfo = LoginForm::incrementLoginThrottle( $this->mUserName );
|
||||
if ( $throttleInfo ) {
|
||||
return Status::newFatal( $this->msg( 'changepassword-throttled' )
|
||||
->durationParams( $throttleInfo['wait'] )
|
||||
);
|
||||
}
|
||||
|
||||
// @todo Make these separate messages, since the message is written for both cases
|
||||
if ( !$user->checkTemporaryPassword( $oldpass ) && !$user->checkPassword( $oldpass ) ) {
|
||||
Hooks::run( 'PrefsPasswordAudit', [ $user, $newpass, 'wrongpassword' ] );
|
||||
return Status::newFatal( $this->msg( 'resetpass-wrong-oldpass' ) );
|
||||
}
|
||||
|
||||
// User is resetting their password to their old password
|
||||
if ( $oldpass === $newpass ) {
|
||||
return Status::newFatal( $this->msg( 'resetpass-recycled' ) );
|
||||
}
|
||||
|
||||
// Do AbortChangePassword after checking mOldpass, so we don't leak information
|
||||
// by possibly aborting a new password before verifying the old password.
|
||||
$abortMsg = 'resetpass-abort-generic';
|
||||
if ( !Hooks::run( 'AbortChangePassword', [ $user, $oldpass, $newpass, &$abortMsg ] ) ) {
|
||||
Hooks::run( 'PrefsPasswordAudit', [ $user, $newpass, 'abortreset' ] );
|
||||
return Status::newFatal( $this->msg( $abortMsg ) );
|
||||
}
|
||||
|
||||
// Please reset throttle for successful logins, thanks!
|
||||
LoginForm::clearLoginThrottle( $this->mUserName );
|
||||
|
||||
try {
|
||||
$user->setPassword( $newpass );
|
||||
Hooks::run( 'PrefsPasswordAudit', [ $user, $newpass, 'success' ] );
|
||||
} catch ( PasswordError $e ) {
|
||||
Hooks::run( 'PrefsPasswordAudit', [ $user, $newpass, 'error' ] );
|
||||
return Status::newFatal( new RawMessage( $e->getMessage() ) );
|
||||
}
|
||||
|
||||
if ( $isSelf ) {
|
||||
// This is needed to keep the user connected since
|
||||
// changing the password also modifies the user's token.
|
||||
$remember = $this->getRequest()->getCookie( 'Token' ) !== null;
|
||||
$user->setCookies( null, null, $remember );
|
||||
}
|
||||
$user->saveSettings();
|
||||
$this->resetPasswordExpiration( $user );
|
||||
return Status::newGood();
|
||||
}
|
||||
|
||||
public function requiresUnblock() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getGroupName() {
|
||||
return 'users';
|
||||
}
|
||||
|
||||
/**
|
||||
* For resetting user password expiration, until AuthManager comes along
|
||||
* @param User $user
|
||||
*/
|
||||
private function resetPasswordExpiration( User $user ) {
|
||||
global $wgPasswordExpirationDays;
|
||||
$newExpire = null;
|
||||
if ( $wgPasswordExpirationDays ) {
|
||||
$newExpire = wfTimestamp(
|
||||
TS_MW,
|
||||
time() + ( $wgPasswordExpirationDays * 24 * 3600 )
|
||||
);
|
||||
}
|
||||
// Give extensions a chance to force an expiration
|
||||
Hooks::run( 'ResetPasswordExpiration', [ $this, &$newExpire ] );
|
||||
$dbw = wfGetDB( DB_MASTER );
|
||||
$dbw->update(
|
||||
'user',
|
||||
[ 'user_password_expires' => $dbw->timestampOrNull( $newExpire ) ],
|
||||
[ 'user_id' => $user->getId() ],
|
||||
__METHOD__
|
||||
);
|
||||
}
|
||||
|
||||
protected function getDisplayFormat() {
|
||||
return 'ooui';
|
||||
parent::__construct( 'ChangePassword', 'ChangeCredentials',
|
||||
PasswordAuthenticationRequest::class, [ 'returnto', 'returntoquery' ] );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
/**
|
||||
* Redirect page: Special:CreateAccount --> Special:UserLogin/signup.
|
||||
* Implements Special:CreateAccount
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
|
@ -21,31 +21,35 @@
|
|||
* @ingroup SpecialPage
|
||||
*/
|
||||
|
||||
use MediaWiki\Auth\AuthManager;
|
||||
use MediaWiki\Logger\LoggerFactory;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* Redirect page: Special:CreateAccount --> Special:UserLogin/signup.
|
||||
* @todo FIXME: This (and the rest of the login frontend) needs to die a horrible painful death
|
||||
* Implements Special:CreateAccount
|
||||
*
|
||||
* @ingroup SpecialPage
|
||||
*/
|
||||
class SpecialCreateAccount extends SpecialRedirectToSpecial {
|
||||
function __construct() {
|
||||
parent::__construct(
|
||||
'CreateAccount',
|
||||
'Userlogin',
|
||||
'signup',
|
||||
[ 'returnto', 'returntoquery', 'uselang' ]
|
||||
);
|
||||
class SpecialCreateAccount extends LoginSignupSpecialPage {
|
||||
protected static $allowedActions = [
|
||||
AuthManager::ACTION_CREATE,
|
||||
AuthManager::ACTION_CREATE_CONTINUE
|
||||
];
|
||||
|
||||
protected static $messages = [
|
||||
'authform-newtoken' => 'nocookiesfornew',
|
||||
'authform-notoken' => 'sessionfailure',
|
||||
'authform-wrongtoken' => 'sessionfailure',
|
||||
];
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct( 'CreateAccount' );
|
||||
}
|
||||
|
||||
public function doesWrites() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// No reason to hide this link on Special:Specialpages
|
||||
public function isListed() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isRestricted() {
|
||||
return !User::groupHasPermission( '*', 'createaccount' );
|
||||
}
|
||||
|
|
@ -54,7 +58,112 @@ class SpecialCreateAccount extends SpecialRedirectToSpecial {
|
|||
return $user->isAllowed( 'createaccount' );
|
||||
}
|
||||
|
||||
public function checkPermissions() {
|
||||
parent::checkPermissions();
|
||||
|
||||
$user = $this->getUser();
|
||||
$status = AuthManager::singleton()->checkAccountCreatePermissions( $user );
|
||||
if ( !$status->isGood() ) {
|
||||
throw new ErrorPageError( 'createacct-error', $status->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
protected function getLoginSecurityLevel() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getDefaultAction( $subPage ) {
|
||||
return AuthManager::ACTION_CREATE;
|
||||
}
|
||||
|
||||
public function getDescription() {
|
||||
return $this->msg( 'createaccount' )->text();
|
||||
}
|
||||
|
||||
protected function isSignup() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run any hooks registered for logins, then display a message welcoming
|
||||
* the user.
|
||||
* @param bool $direct True if the action was successful just now; false if that happened
|
||||
* pre-redirection (so this handler was called already)
|
||||
* @param StatusValue|null $extraMessages
|
||||
*/
|
||||
protected function successfulAction( $direct = false, $extraMessages = null ) {
|
||||
$session = $this->getRequest()->getSession();
|
||||
$user = $this->targetUser ?: $this->getUser();
|
||||
|
||||
if ( $direct ) {
|
||||
# Only save preferences if the user is not creating an account for someone else.
|
||||
if ( !$this->proxyAccountCreation ) {
|
||||
Hooks::run( 'AddNewAccount', [ $user, false ] );
|
||||
|
||||
// If the user does not have a session cookie at this point, they probably need to
|
||||
// do something to their browser.
|
||||
if ( !$this->hasSessionCookie() ) {
|
||||
$this->mainLoginForm( [ /*?*/ ], $session->getProvider()->whyNoSession() );
|
||||
// TODO something more specific? This used to use nocookiesnew
|
||||
// FIXME should redirect to login page instead?
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$byEmail = false; // FIXME no way to set this
|
||||
|
||||
Hooks::run( 'AddNewAccount', [ $user, $byEmail ] );
|
||||
|
||||
$out = $this->getOutput();
|
||||
$out->setPageTitle( $this->msg( $byEmail ? 'accmailtitle' : 'accountcreated' ) );
|
||||
if ( $byEmail ) {
|
||||
$out->addWikiMsg( 'accmailtext', $user->getName(), $user->getEmail() );
|
||||
} else {
|
||||
$out->addWikiMsg( 'accountcreatedtext', $user->getName() );
|
||||
}
|
||||
$out->addReturnTo( $this->getPageTitle() );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->clearToken();
|
||||
|
||||
# Run any hooks; display injected HTML
|
||||
$injected_html = '';
|
||||
$welcome_creation_msg = 'welcomecreation-msg';
|
||||
Hooks::run( 'UserLoginComplete', [ &$user, &$injected_html ] );
|
||||
|
||||
/**
|
||||
* Let any extensions change what message is shown.
|
||||
* @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforeWelcomeCreation
|
||||
* @since 1.18
|
||||
*/
|
||||
Hooks::run( 'BeforeWelcomeCreation', [ &$welcome_creation_msg, &$injected_html ] );
|
||||
|
||||
$this->showSuccessPage( 'signup', $this->msg( 'welcomeuser', $this->getUser()->getName() ),
|
||||
$welcome_creation_msg, $injected_html, $extraMessages );
|
||||
}
|
||||
|
||||
protected function getToken() {
|
||||
return $this->getRequest()->getSession()->getToken( '', 'createaccount' );
|
||||
}
|
||||
|
||||
protected function clearToken() {
|
||||
return $this->getRequest()->getSession()->resetToken( 'createaccount' );
|
||||
}
|
||||
|
||||
protected function getTokenName() {
|
||||
return 'wpCreateaccountToken';
|
||||
}
|
||||
|
||||
protected function getGroupName() {
|
||||
return 'login';
|
||||
}
|
||||
|
||||
protected function logAuthResult( $success, $status = null ) {
|
||||
LoggerFactory::getInstance( 'authmanager-stats' )->info( 'Account creation attempt', [
|
||||
'event' => 'accountcreation',
|
||||
'successful' => $success,
|
||||
'status' => $status,
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
111
includes/specials/SpecialLinkAccounts.php
Normal file
111
includes/specials/SpecialLinkAccounts.php
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\Auth\AuthenticationRequest;
|
||||
use MediaWiki\Auth\AuthenticationResponse;
|
||||
use MediaWiki\Auth\AuthManager;
|
||||
|
||||
/**
|
||||
* Links/unlinks external accounts to the current user.
|
||||
*
|
||||
* To interact with this page, account providers need to register themselves with AuthManager.
|
||||
*/
|
||||
class SpecialLinkAccounts extends AuthManagerSpecialPage {
|
||||
protected static $allowedActions = [
|
||||
AuthManager::ACTION_LINK, AuthManager::ACTION_LINK_CONTINUE,
|
||||
];
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct( 'LinkAccounts' );
|
||||
}
|
||||
|
||||
protected function getGroupName() {
|
||||
return 'users';
|
||||
}
|
||||
|
||||
public function isListed() {
|
||||
return AuthManager::singleton()->canLinkAccounts();
|
||||
}
|
||||
|
||||
protected function getRequestBlacklist() {
|
||||
return $this->getConfig()->get( 'ChangeCredentialsBlacklist' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|string $subPage
|
||||
* @throws MWException
|
||||
* @throws PermissionsError
|
||||
*/
|
||||
public function execute( $subPage ) {
|
||||
$this->setHeaders();
|
||||
$this->loadAuth( $subPage );
|
||||
|
||||
if ( !$this->isActionAllowed( $this->authAction ) ) {
|
||||
if ( $this->authAction === AuthManager::ACTION_LINK ) {
|
||||
// looks like no linking provider is installed or willing to take this user
|
||||
$titleMessage = wfMessage( 'cannotlink-no-provider-title' );
|
||||
$errorMessage = wfMessage( 'cannotlink-no-provider' );
|
||||
throw new ErrorPageError( $titleMessage, $errorMessage );
|
||||
} else {
|
||||
// user probably back-button-navigated into an auth session that no longer exists
|
||||
// FIXME would be nice to show a message
|
||||
$this->getOutput()->redirect( $this->getPageTitle()->getFullURL( '', false,
|
||||
PROTO_HTTPS ) );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->outputHeader();
|
||||
|
||||
$status = $this->trySubmit();
|
||||
|
||||
if ( $status === false || !$status->isOK() ) {
|
||||
$this->displayForm( $status );
|
||||
return;
|
||||
}
|
||||
|
||||
$response = $status->getValue();
|
||||
|
||||
switch ( $response->status ) {
|
||||
case AuthenticationResponse::PASS:
|
||||
$this->success();
|
||||
break;
|
||||
case AuthenticationResponse::FAIL:
|
||||
$this->loadAuth( '', AuthManager::ACTION_LINK, true );
|
||||
$this->displayForm( StatusValue::newFatal( $response->message ) );
|
||||
break;
|
||||
case AuthenticationResponse::REDIRECT:
|
||||
$this->getOutput()->redirect( $response->redirectTarget );
|
||||
break;
|
||||
case AuthenticationResponse::UI:
|
||||
$this->authAction = AuthManager::ACTION_LINK_CONTINUE;
|
||||
$this->authRequests = $response->neededRequests;
|
||||
$this->displayForm( StatusValue::newFatal( $response->message ) );
|
||||
break;
|
||||
default:
|
||||
throw new LogicException( 'invalid AuthenticationResponse' );
|
||||
}
|
||||
}
|
||||
|
||||
protected function getDefaultAction( $subPage ) {
|
||||
return AuthManager::ACTION_LINK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AuthenticationRequest[] $requests
|
||||
* @param string $action AuthManager action name, should be ACTION_LINK or ACTION_LINK_CONTINUE
|
||||
* @return HTMLForm
|
||||
*/
|
||||
protected function getAuthForm( array $requests, $action ) {
|
||||
$form = parent::getAuthForm( $requests, $action );
|
||||
$form->setSubmitTextMsg( 'linkaccounts-submit' );
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a success message.
|
||||
*/
|
||||
protected function success() {
|
||||
$this->loadAuth( '', AuthManager::ACTION_LINK, true );
|
||||
$this->displayForm( StatusValue::newFatal( $this->msg( 'linkaccounts-success-text' ) ) );
|
||||
}
|
||||
}
|
||||
|
|
@ -21,21 +21,25 @@
|
|||
* @ingroup SpecialPage
|
||||
*/
|
||||
|
||||
use MediaWiki\Auth\AuthManager;
|
||||
|
||||
/**
|
||||
* Special page for requesting a password reset email
|
||||
* Special page for requesting a password reset email.
|
||||
*
|
||||
* Requires the TemporaryPasswordPrimaryAuthenticationProvider and the
|
||||
* EmailNotificationSecondaryAuthenticationProvider (or something providing equivalent
|
||||
* functionality) to be enabled.
|
||||
*
|
||||
* @ingroup SpecialPage
|
||||
*/
|
||||
class SpecialPasswordReset extends FormSpecialPage {
|
||||
/**
|
||||
* @var Message
|
||||
*/
|
||||
private $email;
|
||||
/** @var PasswordReset */
|
||||
private $passwordReset;
|
||||
|
||||
/**
|
||||
* @var User
|
||||
* @var string[] Temporary storage for the passwords which have been sent out, keyed by username.
|
||||
*/
|
||||
private $firstUser;
|
||||
private $passwords = [];
|
||||
|
||||
/**
|
||||
* @var Status
|
||||
|
|
@ -49,6 +53,7 @@ class SpecialPasswordReset extends FormSpecialPage {
|
|||
|
||||
public function __construct() {
|
||||
parent::__construct( 'PasswordReset', 'editmyprivateinfo' );
|
||||
$this->passwordReset = new PasswordReset( $this->getConfig(), AuthManager::singleton() );
|
||||
}
|
||||
|
||||
public function doesWrites() {
|
||||
|
|
@ -56,22 +61,19 @@ class SpecialPasswordReset extends FormSpecialPage {
|
|||
}
|
||||
|
||||
public function userCanExecute( User $user ) {
|
||||
return $this->canChangePassword( $user ) === true && parent::userCanExecute( $user );
|
||||
return $this->passwordReset->isAllowed( $user )->isGood();
|
||||
}
|
||||
|
||||
public function checkExecutePermissions( User $user ) {
|
||||
$error = $this->canChangePassword( $user );
|
||||
if ( is_string( $error ) ) {
|
||||
throw new ErrorPageError( 'internalerror', $error );
|
||||
} elseif ( !$error ) {
|
||||
throw new ErrorPageError( 'internalerror', 'resetpass_forbidden' );
|
||||
$status = Status::wrap( $this->passwordReset->isAllowed( $user ) );
|
||||
if ( !$status->isGood() ) {
|
||||
throw new ErrorPageError( 'internalerror', $status->getMessage() );
|
||||
}
|
||||
|
||||
parent::checkExecutePermissions( $user );
|
||||
}
|
||||
|
||||
protected function getFormFields() {
|
||||
global $wgAuth;
|
||||
$resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' );
|
||||
$a = [];
|
||||
if ( isset( $resetRoutes['username'] ) && $resetRoutes['username'] ) {
|
||||
|
|
@ -92,15 +94,6 @@ class SpecialPasswordReset extends FormSpecialPage {
|
|||
];
|
||||
}
|
||||
|
||||
if ( isset( $resetRoutes['domain'] ) && $resetRoutes['domain'] ) {
|
||||
$domains = $wgAuth->domainList();
|
||||
$a['Domain'] = [
|
||||
'type' => 'select',
|
||||
'options' => $domains,
|
||||
'label-message' => 'passwordreset-domain',
|
||||
];
|
||||
}
|
||||
|
||||
if ( $this->getUser()->isAllowed( 'passwordreset' ) ) {
|
||||
$a['Capture'] = [
|
||||
'type' => 'check',
|
||||
|
|
@ -128,9 +121,6 @@ class SpecialPasswordReset extends FormSpecialPage {
|
|||
if ( isset( $resetRoutes['email'] ) && $resetRoutes['email'] ) {
|
||||
$i++;
|
||||
}
|
||||
if ( isset( $resetRoutes['domain'] ) && $resetRoutes['domain'] ) {
|
||||
$i++;
|
||||
}
|
||||
|
||||
$message = ( $i > 1 ) ? 'passwordreset-text-many' : 'passwordreset-text-one';
|
||||
|
||||
|
|
@ -145,180 +135,54 @@ class SpecialPasswordReset extends FormSpecialPage {
|
|||
* @param array $data
|
||||
* @throws MWException
|
||||
* @throws ThrottledError|PermissionsError
|
||||
* @return bool|array
|
||||
* @return Status
|
||||
*/
|
||||
public function onSubmit( array $data ) {
|
||||
global $wgAuth, $wgMinimalPasswordLength;
|
||||
|
||||
if ( isset( $data['Domain'] ) ) {
|
||||
if ( $wgAuth->validDomain( $data['Domain'] ) ) {
|
||||
$wgAuth->setDomain( $data['Domain'] );
|
||||
} else {
|
||||
$wgAuth->setDomain( 'invaliddomain' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $data['Capture'] ) && !$this->getUser()->isAllowed( 'passwordreset' ) ) {
|
||||
// The user knows they don't have the passwordreset permission,
|
||||
// but they tried to spoof the form. That's naughty
|
||||
throw new PermissionsError( 'passwordreset' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @var $firstUser User
|
||||
* @var $users User[]
|
||||
*/
|
||||
$username = isset( $data['Username'] ) ? $data['Username'] : null;
|
||||
$email = isset( $data['Email'] ) ? $data['Email'] : null;
|
||||
$capture = !empty( $data['Capture'] );
|
||||
|
||||
if ( isset( $data['Username'] ) && $data['Username'] !== '' ) {
|
||||
$method = 'username';
|
||||
$users = [ User::newFromName( $data['Username'] ) ];
|
||||
} elseif ( isset( $data['Email'] )
|
||||
&& $data['Email'] !== ''
|
||||
&& Sanitizer::validateEmail( $data['Email'] )
|
||||
) {
|
||||
$method = 'email';
|
||||
$res = wfGetDB( DB_SLAVE )->select(
|
||||
'user',
|
||||
User::selectFields(),
|
||||
[ 'user_email' => $data['Email'] ],
|
||||
__METHOD__
|
||||
);
|
||||
|
||||
if ( $res ) {
|
||||
$users = [];
|
||||
|
||||
foreach ( $res as $row ) {
|
||||
$users[] = User::newFromRow( $row );
|
||||
}
|
||||
} else {
|
||||
// Some sort of database error, probably unreachable
|
||||
throw new MWException( 'Unknown database error in ' . __METHOD__ );
|
||||
}
|
||||
} else {
|
||||
// The user didn't supply any data
|
||||
return false;
|
||||
$this->method = $username ? 'username' : 'email';
|
||||
$this->result = Status::wrap(
|
||||
$this->passwordReset->execute( $this->getUser(), $username, $email, $capture ) );
|
||||
if ( $capture && $this->result->isOK() ) {
|
||||
$this->passwords = $this->result->getValue();
|
||||
}
|
||||
|
||||
// Check for hooks (captcha etc), and allow them to modify the users list
|
||||
$error = [];
|
||||
if ( !Hooks::run( 'SpecialPasswordResetOnSubmit', [ &$users, $data, &$error ] ) ) {
|
||||
return [ $error ];
|
||||
}
|
||||
|
||||
$this->method = $method;
|
||||
|
||||
if ( count( $users ) == 0 ) {
|
||||
if ( $method == 'email' ) {
|
||||
// Don't reveal whether or not an email address is in use
|
||||
return true;
|
||||
} else {
|
||||
return [ 'noname' ];
|
||||
}
|
||||
}
|
||||
|
||||
$firstUser = $users[0];
|
||||
|
||||
if ( !$firstUser instanceof User || !$firstUser->getId() ) {
|
||||
// Don't parse username as wikitext (bug 65501)
|
||||
return [ [ 'nosuchuser', wfEscapeWikiText( $data['Username'] ) ] ];
|
||||
}
|
||||
|
||||
// Check against the rate limiter
|
||||
if ( $this->getUser()->pingLimiter( 'mailpassword' ) ) {
|
||||
if ( $this->result->hasMessage( 'actionthrottledtext' ) ) {
|
||||
throw new ThrottledError;
|
||||
}
|
||||
|
||||
// Check against password throttle
|
||||
foreach ( $users as $user ) {
|
||||
if ( $user->isPasswordReminderThrottled() ) {
|
||||
|
||||
# Round the time in hours to 3 d.p., in case someone is specifying
|
||||
# minutes or seconds.
|
||||
return [ [
|
||||
'throttled-mailpassword',
|
||||
round( $this->getConfig()->get( 'PasswordReminderResendTime' ), 3 )
|
||||
] ];
|
||||
}
|
||||
}
|
||||
|
||||
// All the users will have the same email address
|
||||
if ( $firstUser->getEmail() == '' ) {
|
||||
// This won't be reachable from the email route, so safe to expose the username
|
||||
return [ [ 'noemail', wfEscapeWikiText( $firstUser->getName() ) ] ];
|
||||
}
|
||||
|
||||
// We need to have a valid IP address for the hook, but per bug 18347, we should
|
||||
// send the user's name if they're logged in.
|
||||
$ip = $this->getRequest()->getIP();
|
||||
if ( !$ip ) {
|
||||
return [ 'badipaddress' ];
|
||||
}
|
||||
$caller = $this->getUser();
|
||||
Hooks::run( 'User::mailPasswordInternal', [ &$caller, &$ip, &$firstUser ] );
|
||||
$username = $caller->getName();
|
||||
$msg = IP::isValid( $username )
|
||||
? 'passwordreset-emailtext-ip'
|
||||
: 'passwordreset-emailtext-user';
|
||||
|
||||
// Send in the user's language; which should hopefully be the same
|
||||
$userLanguage = $firstUser->getOption( 'language' );
|
||||
|
||||
$passwords = [];
|
||||
foreach ( $users as $user ) {
|
||||
$password = PasswordFactory::generateRandomPasswordString( $wgMinimalPasswordLength );
|
||||
$user->setNewpassword( $password );
|
||||
$user->saveSettings();
|
||||
$passwords[] = $this->msg( 'passwordreset-emailelement', $user->getName(), $password )
|
||||
->inLanguage( $userLanguage )->text(); // We'll escape the whole thing later
|
||||
}
|
||||
$passwordBlock = implode( "\n\n", $passwords );
|
||||
|
||||
$this->email = $this->msg( $msg )->inLanguage( $userLanguage );
|
||||
$this->email->params(
|
||||
$username,
|
||||
$passwordBlock,
|
||||
count( $passwords ),
|
||||
'<' . Title::newMainPage()->getCanonicalURL() . '>',
|
||||
round( $this->getConfig()->get( 'NewPasswordExpiry' ) / 86400 )
|
||||
);
|
||||
|
||||
$title = $this->msg( 'passwordreset-emailtitle' )->inLanguage( $userLanguage );
|
||||
|
||||
$this->result = $firstUser->sendMail( $title->text(), $this->email->text() );
|
||||
|
||||
if ( isset( $data['Capture'] ) && $data['Capture'] ) {
|
||||
// Save the user, will be used if an error occurs when sending the email
|
||||
$this->firstUser = $firstUser;
|
||||
} else {
|
||||
// Blank the email if the user is not supposed to see it
|
||||
$this->email = null;
|
||||
}
|
||||
|
||||
if ( $this->result->isGood() ) {
|
||||
return true;
|
||||
} elseif ( isset( $data['Capture'] ) && $data['Capture'] ) {
|
||||
// The email didn't send, but maybe they knew that and that's why they captured it
|
||||
return true;
|
||||
} else {
|
||||
// @todo FIXME: The email wasn't sent, but we have already set
|
||||
// the password throttle timestamp, so they won't be able to try
|
||||
// again until it expires... :(
|
||||
return [ [ 'mailerror', $this->result->getMessage() ] ];
|
||||
}
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
public function onSuccess() {
|
||||
if ( $this->getUser()->isAllowed( 'passwordreset' ) && $this->email != null ) {
|
||||
if ( $this->getUser()->isAllowed( 'passwordreset' ) && $this->passwords ) {
|
||||
// @todo Logging
|
||||
|
||||
if ( $this->result->isGood() ) {
|
||||
$this->getOutput()->addWikiMsg( 'passwordreset-emailsent-capture' );
|
||||
$this->getOutput()->addWikiMsg( 'passwordreset-emailsent-capture2',
|
||||
count( $this->passwords ) );
|
||||
} else {
|
||||
$this->getOutput()->addWikiMsg( 'passwordreset-emailerror-capture',
|
||||
$this->result->getMessage(), $this->firstUser->getName() );
|
||||
$this->getOutput()->addWikiMsg( 'passwordreset-emailerror-capture2',
|
||||
$this->result->getMessage(), key( $this->passwords ), count( $this->passwords ) );
|
||||
}
|
||||
|
||||
$this->getOutput()->addHTML( Html::rawElement( 'pre', [], $this->email->escaped() ) );
|
||||
$this->getOutput()->addHTML( Html::openElement( 'ul' ) );
|
||||
foreach ( $this->passwords as $username => $pwd ) {
|
||||
$this->getOutput()->addHTML( Html::rawElement( 'li', [],
|
||||
htmlspecialchars( $username, ENT_QUOTES )
|
||||
. $this->msg( 'colon-separator' )->text()
|
||||
. htmlspecialchars( $pwd, ENT_QUOTES )
|
||||
) );
|
||||
}
|
||||
$this->getOutput()->addHTML( Html::closeElement( 'ul' ) );
|
||||
}
|
||||
|
||||
if ( $this->method === 'email' ) {
|
||||
|
|
@ -330,42 +194,12 @@ class SpecialPasswordReset extends FormSpecialPage {
|
|||
$this->getOutput()->returnToMain();
|
||||
}
|
||||
|
||||
protected function canChangePassword( User $user ) {
|
||||
global $wgAuth;
|
||||
$resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' );
|
||||
|
||||
// Maybe password resets are disabled, or there are no allowable routes
|
||||
if ( !is_array( $resetRoutes ) ||
|
||||
!in_array( true, array_values( $resetRoutes ) )
|
||||
) {
|
||||
return 'passwordreset-disabled';
|
||||
}
|
||||
|
||||
// Maybe the external auth plugin won't allow local password changes
|
||||
if ( !$wgAuth->allowPasswordChange() ) {
|
||||
return 'resetpass_forbidden';
|
||||
}
|
||||
|
||||
// Maybe email features have been disabled
|
||||
if ( !$this->getConfig()->get( 'EnableEmail' ) ) {
|
||||
return 'passwordreset-emaildisabled';
|
||||
}
|
||||
|
||||
// Maybe the user is blocked (check this here rather than relying on the parent
|
||||
// method as we have a more specific error message to use here
|
||||
if ( $user->isBlocked() ) {
|
||||
return 'blocked-mailpassword';
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the password reset page if resets are disabled.
|
||||
* @return bool
|
||||
*/
|
||||
function isListed() {
|
||||
if ( $this->canChangePassword( $this->getUser() ) === true ) {
|
||||
public function isListed() {
|
||||
if ( $this->passwordReset->isAllowed( $this->getUser() )->isGood() ) {
|
||||
return parent::isListed();
|
||||
}
|
||||
|
||||
|
|
|
|||
26
includes/specials/SpecialRemoveCredentials.php
Normal file
26
includes/specials/SpecialRemoveCredentials.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\Auth\AuthManager;
|
||||
|
||||
/**
|
||||
* Special change to remove credentials (such as a two-factor token).
|
||||
*/
|
||||
class SpecialRemoveCredentials extends SpecialChangeCredentials {
|
||||
protected static $allowedActions = [ AuthManager::ACTION_REMOVE ];
|
||||
|
||||
protected static $messagePrefix = 'removecredentials';
|
||||
|
||||
protected static $loadUserData = false;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct( 'RemoveCredentials' );
|
||||
}
|
||||
|
||||
protected function getDefaultAction( $subPage ) {
|
||||
return AuthManager::ACTION_REMOVE;
|
||||
}
|
||||
|
||||
protected function getRequestBlacklist() {
|
||||
return $this->getConfig()->get( 'RemoveCredentialsBlacklist' );
|
||||
}
|
||||
}
|
||||
78
includes/specials/SpecialUnlinkAccounts.php
Normal file
78
includes/specials/SpecialUnlinkAccounts.php
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\Auth\AuthenticationResponse;
|
||||
use MediaWiki\Auth\AuthManager;
|
||||
use MediaWiki\Session\SessionManager;
|
||||
|
||||
class SpecialUnlinkAccounts extends AuthManagerSpecialPage {
|
||||
protected static $allowedActions = [ AuthManager::ACTION_UNLINK ];
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct( 'UnlinkAccounts' );
|
||||
}
|
||||
|
||||
protected function getLoginSecurityLevel() {
|
||||
return 'UnlinkAccount';
|
||||
}
|
||||
|
||||
protected function getDefaultAction( $subPage ) {
|
||||
return AuthManager::ACTION_UNLINK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Under which header this special page is listed in Special:SpecialPages.
|
||||
*/
|
||||
protected function getGroupName() {
|
||||
return 'users';
|
||||
}
|
||||
|
||||
public function isListed() {
|
||||
return AuthManager::singleton()->canLinkAccounts();
|
||||
}
|
||||
|
||||
protected function getRequestBlacklist() {
|
||||
return $this->getConfig()->get( 'RemoveCredentialsBlacklist' );
|
||||
}
|
||||
|
||||
public function execute( $subPage ) {
|
||||
$this->setHeaders();
|
||||
$this->loadAuth( $subPage );
|
||||
$this->outputHeader();
|
||||
|
||||
$status = $this->trySubmit();
|
||||
|
||||
if ( $status === false || !$status->isOK() ) {
|
||||
$this->displayForm( $status );
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var AuthenticationResponse $response */
|
||||
$response = $status->getValue();
|
||||
|
||||
if ( $response->status === AuthenticationResponse::FAIL ) {
|
||||
$this->displayForm( StatusValue::newFatal( $response->message ) );
|
||||
return;
|
||||
}
|
||||
|
||||
$status = StatusValue::newGood();
|
||||
$status->warning( wfMessage( 'unlinkaccounts-success' ) );
|
||||
$this->loadAuth( $subPage, null, true ); // update requests so the unlinked one doesn't show up
|
||||
|
||||
// Reset sessions - if the user unlinked an account because it was compromised,
|
||||
// log attackers out from sessions obtained via that account.
|
||||
$session = $this->getRequest()->getSession();
|
||||
$user = $this->getUser();
|
||||
SessionManager::singleton()->invalidateSessionsForUser( $user );
|
||||
$session->setUser( $user );
|
||||
$session->resetId();
|
||||
|
||||
$this->displayForm( $status );
|
||||
}
|
||||
|
||||
public function handleFormSubmit( $data ) {
|
||||
// unlink requests do not accept user input so repeat parent code but skip call to
|
||||
// AuthenticationRequest::loadRequestsFromSubmission
|
||||
$response = $this->performAuthenticationStep( $this->authAction, $this->authRequests );
|
||||
return Status::newGood( $response );
|
||||
}
|
||||
}
|
||||
163
includes/specials/SpecialUserLogin.php
Normal file
163
includes/specials/SpecialUserLogin.php
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
<?php
|
||||
/**
|
||||
* Implements Special:UserLogin
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @ingroup SpecialPage
|
||||
*/
|
||||
|
||||
use MediaWiki\Auth\AuthManager;
|
||||
use MediaWiki\Logger\LoggerFactory;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
/**
|
||||
* Implements Special:UserLogin
|
||||
*
|
||||
* @ingroup SpecialPage
|
||||
*/
|
||||
class SpecialUserLogin extends LoginSignupSpecialPage {
|
||||
protected static $allowedActions = [
|
||||
AuthManager::ACTION_LOGIN,
|
||||
AuthManager::ACTION_LOGIN_CONTINUE
|
||||
];
|
||||
|
||||
protected static $messages = [
|
||||
'authform-newtoken' => 'nocookiesforlogin',
|
||||
'authform-notoken' => 'sessionfailure',
|
||||
'authform-wrongtoken' => 'sessionfailure',
|
||||
];
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct( 'Userlogin' );
|
||||
}
|
||||
|
||||
public function doesWrites() {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getLoginSecurityLevel() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getDefaultAction( $subPage ) {
|
||||
return AuthManager::ACTION_LOGIN;
|
||||
}
|
||||
|
||||
public function getDescription() {
|
||||
return $this->msg( 'login' )->text();
|
||||
}
|
||||
|
||||
public function setHeaders() {
|
||||
// override the page title if we are doing a forced reauthentication
|
||||
parent::setHeaders();
|
||||
if ( $this->securityLevel && $this->getUser()->isLoggedIn() ) {
|
||||
$this->getOutput()->setPageTitle( $this->msg( 'login-security' ) );
|
||||
}
|
||||
}
|
||||
|
||||
protected function isSignup() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function beforeExecute( $subPage ) {
|
||||
if ( $subPage === 'signup' || $this->getRequest()->getText( 'type' ) === 'signup' ) {
|
||||
// B/C for old account creation URLs
|
||||
$title = SpecialPage::getTitleFor( 'CreateAccount' );
|
||||
$query = array_diff_key( $this->getRequest()->getValues(),
|
||||
array_fill_keys( [ 'type', 'title' ], true ) );
|
||||
$url = $title->getFullURL( $query, false, PROTO_CURRENT );
|
||||
$this->getOutput()->redirect( $url );
|
||||
return false;
|
||||
}
|
||||
return parent::beforeExecute( $subPage );
|
||||
}
|
||||
|
||||
/**
|
||||
* Run any hooks registered for logins, then HTTP redirect to
|
||||
* $this->mReturnTo (or Main Page if that's undefined). Formerly we had a
|
||||
* nice message here, but that's really not as useful as just being sent to
|
||||
* wherever you logged in from. It should be clear that the action was
|
||||
* successful, given the lack of error messages plus the appearance of your
|
||||
* name in the upper right.
|
||||
* @param bool $direct True if the action was successful just now; false if that happened
|
||||
* pre-redirection (so this handler was called already)
|
||||
* @param StatusValue|null $extraMessages
|
||||
*/
|
||||
protected function successfulAction( $direct = false, $extraMessages = null ) {
|
||||
global $wgSecureLogin;
|
||||
|
||||
$user = $this->targetUser ?: $this->getUser();
|
||||
$session = $this->getRequest()->getSession();
|
||||
|
||||
if ( $direct ) {
|
||||
$user->touch();
|
||||
|
||||
$this->clearToken();
|
||||
|
||||
if ( $user->requiresHTTPS() ) {
|
||||
$this->mStickHTTPS = true;
|
||||
}
|
||||
$session->setForceHTTPS( $wgSecureLogin && $this->mStickHTTPS );
|
||||
|
||||
// If the user does not have a session cookie at this point, they probably need to
|
||||
// do something to their browser.
|
||||
if ( !$this->hasSessionCookie() ) {
|
||||
$this->mainLoginForm( [ /*?*/ ], $session->getProvider()->whyNoSession() );
|
||||
// TODO something more specific? This used to use nocookieslogin
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
# Run any hooks; display injected HTML if any, else redirect
|
||||
$injected_html = '';
|
||||
Hooks::run( 'UserLoginComplete', [ &$user, &$injected_html ] );
|
||||
|
||||
if ( $injected_html !== '' || $extraMessages ) {
|
||||
$this->showSuccessPage( 'success', $this->msg( 'loginsuccesstitle' ),
|
||||
'loginsuccess', $injected_html, $extraMessages );
|
||||
} else {
|
||||
$helper = new LoginHelper( $this->getContext() );
|
||||
$helper->showReturnToPage( 'successredirect', $this->mReturnTo, $this->mReturnToQuery,
|
||||
$this->mStickHTTPS );
|
||||
}
|
||||
}
|
||||
|
||||
protected function getToken() {
|
||||
return $this->getRequest()->getSession()->getToken( '', 'login' );
|
||||
}
|
||||
|
||||
protected function clearToken() {
|
||||
return $this->getRequest()->getSession()->resetToken( 'login' );
|
||||
}
|
||||
|
||||
protected function getTokenName() {
|
||||
return 'wpLoginToken';
|
||||
}
|
||||
|
||||
protected function getGroupName() {
|
||||
return 'login';
|
||||
}
|
||||
|
||||
protected function logAuthResult( $success, $status = null ) {
|
||||
LoggerFactory::getInstance( 'authmanager-stats' )->info( 'Login attempt', [
|
||||
'event' => 'login',
|
||||
'successful' => $success,
|
||||
'status' => $status,
|
||||
] );
|
||||
}
|
||||
}
|
||||
85
includes/specials/SpecialUserLogout.php
Normal file
85
includes/specials/SpecialUserLogout.php
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
/**
|
||||
* Implements Special:Userlogout
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @ingroup SpecialPage
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements Special:Userlogout
|
||||
*
|
||||
* @ingroup SpecialPage
|
||||
*/
|
||||
class SpecialUserLogout extends UnlistedSpecialPage {
|
||||
function __construct() {
|
||||
parent::__construct( 'Userlogout' );
|
||||
}
|
||||
|
||||
public function doesWrites() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function execute( $par ) {
|
||||
/**
|
||||
* Some satellite ISPs use broken precaching schemes that log people out straight after
|
||||
* they're logged in (bug 17790). Luckily, there's a way to detect such requests.
|
||||
*/
|
||||
if ( isset( $_SERVER['REQUEST_URI'] ) && strpos( $_SERVER['REQUEST_URI'], '&' ) !== false ) {
|
||||
wfDebug( "Special:UserLogout request {$_SERVER['REQUEST_URI']} looks suspicious, denying.\n" );
|
||||
throw new HttpError( 400, $this->msg( 'suspicious-userlogout' ), $this->msg( 'loginerror' ) );
|
||||
}
|
||||
|
||||
$this->setHeaders();
|
||||
$this->outputHeader();
|
||||
|
||||
// Make sure it's possible to log out
|
||||
$session = MediaWiki\Session\SessionManager::getGlobalSession();
|
||||
if ( !$session->canSetUser() ) {
|
||||
throw new ErrorPageError(
|
||||
'cannotlogoutnow-title',
|
||||
'cannotlogoutnow-text',
|
||||
[
|
||||
$session->getProvider()->describe( RequestContext::getMain()->getLanguage() )
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$user = $this->getUser();
|
||||
$oldName = $user->getName();
|
||||
|
||||
$user->logout();
|
||||
|
||||
$loginURL = SpecialPage::getTitleFor( 'Userlogin' )->getFullURL(
|
||||
$this->getRequest()->getValues( 'returnto', 'returntoquery' ) );
|
||||
|
||||
$out = $this->getOutput();
|
||||
$out->addWikiMsg( 'logouttext', $loginURL );
|
||||
|
||||
// Hook.
|
||||
$injected_html = '';
|
||||
Hooks::run( 'UserLogoutComplete', [ &$user, &$injected_html, $oldName ] );
|
||||
$out->addHTML( $injected_html );
|
||||
|
||||
$out->returnToMain();
|
||||
}
|
||||
|
||||
protected function getGroupName() {
|
||||
return 'login';
|
||||
}
|
||||
}
|
||||
98
includes/specials/helpers/LoginHelper.php
Normal file
98
includes/specials/helpers/LoginHelper.php
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Helper functions for the login form that need to be shared with other special pages
|
||||
* (such as CentralAuth's SpecialCentralLogin).
|
||||
* @since 1.27
|
||||
*/
|
||||
class LoginHelper extends ContextSource {
|
||||
/**
|
||||
* Valid error and warning messages
|
||||
*
|
||||
* Special:Userlogin can show an error or warning message on the form when
|
||||
* coming from another page. This is done via the ?error= or ?warning= GET
|
||||
* parameters.
|
||||
*
|
||||
* This array is the list of valid message keys. Further keys can be added by the
|
||||
* LoginFormValidErrorMessages hook. All other values will be ignored.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public static $validErrorMessages = [
|
||||
'exception-nologin-text',
|
||||
'watchlistanontext',
|
||||
'changeemail-no-info',
|
||||
'resetpass-no-info',
|
||||
'confirmemail_needlogin',
|
||||
'prefsnologintext2',
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns an array of all valid error messages.
|
||||
*
|
||||
* @return array
|
||||
* @see LoginHelper::$validErrorMessages
|
||||
*/
|
||||
public static function getValidErrorMessages() {
|
||||
static $messages = null;
|
||||
if ( !$messages ) {
|
||||
$messages = self::$validErrorMessages;
|
||||
Hooks::run( 'LoginFormValidErrorMessages', [ &$messages ] );
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
public function __construct( IContextSource $context ) {
|
||||
$this->setContext( $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a return link or redirect to it.
|
||||
* Extensions can change where the link should point or inject content into the page
|
||||
* (which will change it from redirect to link mode).
|
||||
*
|
||||
* @param string $type One of the following:
|
||||
* - error: display a return to link ignoring $wgRedirectOnLogin
|
||||
* - success: display a return to link using $wgRedirectOnLogin if needed
|
||||
* - successredirect: send an HTTP redirect using $wgRedirectOnLogin if needed
|
||||
* @param string $returnTo
|
||||
* @param array|string $returnToQuery
|
||||
* @param bool $stickHTTPS Keep redirect link on HTTPS
|
||||
*/
|
||||
public function showReturnToPage(
|
||||
$type, $returnTo = '', $returnToQuery = '', $stickHTTPS = false
|
||||
) {
|
||||
global $wgRedirectOnLogin, $wgSecureLogin;
|
||||
|
||||
if ( $type !== 'error' && $wgRedirectOnLogin !== null ) {
|
||||
$returnTo = $wgRedirectOnLogin;
|
||||
$returnToQuery = [];
|
||||
} elseif ( is_string( $returnToQuery ) ) {
|
||||
$returnToQuery = wfCgiToArray( $returnToQuery );
|
||||
}
|
||||
|
||||
// Allow modification of redirect behavior
|
||||
Hooks::run( 'PostLoginRedirect', [ &$returnTo, &$returnToQuery, &$type ] );
|
||||
|
||||
$returnToTitle = Title::newFromText( $returnTo ) ?: Title::newMainPage();
|
||||
|
||||
if ( $wgSecureLogin && !$stickHTTPS ) {
|
||||
$options = [ 'http' ];
|
||||
$proto = PROTO_HTTP;
|
||||
} elseif ( $wgSecureLogin ) {
|
||||
$options = [ 'https' ];
|
||||
$proto = PROTO_HTTPS;
|
||||
} else {
|
||||
$options = [];
|
||||
$proto = PROTO_RELATIVE;
|
||||
}
|
||||
|
||||
if ( $type === 'successredirect' ) {
|
||||
$redirectUrl = $returnToTitle->getFullURL( $returnToQuery, false, $proto );
|
||||
$this->getOutput()->redirect( $redirectUrl );
|
||||
} else {
|
||||
$this->getOutput()->addReturnTo( $returnToTitle, $returnToQuery, null, $options );
|
||||
}
|
||||
}
|
||||
}
|
||||
10
includes/specials/pre-authmanager/README
Normal file
10
includes/specials/pre-authmanager/README
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
This directory temporarily hosts pre-AuthManager code as a way of feature-flagging.
|
||||
Class names are postfixed with 'PreAuthManager' and SpecialPageFactory adds/removes
|
||||
that postfix based on the feature flag.
|
||||
|
||||
This is a horrible hack that will only be in place for a few weeks, to allow instant
|
||||
rollback while AuthManager is tested in WMF production and major problems are ironed
|
||||
out. In the past such issues have been handled via deployment branches, but that
|
||||
meant blocking the work of all WMF developers from being deployed. This is hoped
|
||||
to be a less disruptive method.
|
||||
|
||||
216
includes/specials/pre-authmanager/SpecialChangeEmail.php
Normal file
216
includes/specials/pre-authmanager/SpecialChangeEmail.php
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
<?php
|
||||
/**
|
||||
* Implements Special:ChangeEmail
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @ingroup SpecialPage
|
||||
*/
|
||||
|
||||
/**
|
||||
* Let users change their email address.
|
||||
*
|
||||
* @ingroup SpecialPage
|
||||
*/
|
||||
class SpecialChangeEmailPreAuthManager extends FormSpecialPage {
|
||||
/**
|
||||
* @var Status
|
||||
*/
|
||||
private $status;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct( 'ChangeEmail', 'editmyprivateinfo' );
|
||||
}
|
||||
|
||||
public function doesWrites() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isListed() {
|
||||
global $wgAuth;
|
||||
|
||||
return $wgAuth->allowPropChange( 'emailaddress' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execution point
|
||||
* @param string $par
|
||||
*/
|
||||
function execute( $par ) {
|
||||
$out = $this->getOutput();
|
||||
$out->disallowUserJs();
|
||||
|
||||
parent::execute( $par );
|
||||
}
|
||||
|
||||
protected function checkExecutePermissions( User $user ) {
|
||||
global $wgAuth;
|
||||
|
||||
if ( !$wgAuth->allowPropChange( 'emailaddress' ) ) {
|
||||
throw new ErrorPageError( 'changeemail', 'cannotchangeemail' );
|
||||
}
|
||||
|
||||
$this->requireLogin( 'changeemail-no-info' );
|
||||
|
||||
// This could also let someone check the current email address, so
|
||||
// require both permissions.
|
||||
if ( !$this->getUser()->isAllowed( 'viewmyprivateinfo' ) ) {
|
||||
throw new PermissionsError( 'viewmyprivateinfo' );
|
||||
}
|
||||
|
||||
parent::checkExecutePermissions( $user );
|
||||
}
|
||||
|
||||
protected function getFormFields() {
|
||||
$user = $this->getUser();
|
||||
|
||||
$fields = [
|
||||
'Name' => [
|
||||
'type' => 'info',
|
||||
'label-message' => 'username',
|
||||
'default' => $user->getName(),
|
||||
],
|
||||
'OldEmail' => [
|
||||
'type' => 'info',
|
||||
'label-message' => 'changeemail-oldemail',
|
||||
'default' => $user->getEmail() ?: $this->msg( 'changeemail-none' )->text(),
|
||||
],
|
||||
'NewEmail' => [
|
||||
'type' => 'email',
|
||||
'label-message' => 'changeemail-newemail',
|
||||
'autofocus' => true,
|
||||
'help-message' => 'changeemail-newemail-help',
|
||||
],
|
||||
];
|
||||
|
||||
if ( $this->getConfig()->get( 'RequirePasswordforEmailChange' ) ) {
|
||||
$fields['Password'] = [
|
||||
'type' => 'password',
|
||||
'label-message' => 'changeemail-password'
|
||||
];
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
protected function getDisplayFormat() {
|
||||
return 'ooui';
|
||||
}
|
||||
|
||||
protected function alterForm( HTMLForm $form ) {
|
||||
$form->setId( 'mw-changeemail-form' );
|
||||
$form->setTableId( 'mw-changeemail-table' );
|
||||
$form->setSubmitTextMsg( 'changeemail-submit' );
|
||||
$form->addHiddenFields( $this->getRequest()->getValues( 'returnto', 'returntoquery' ) );
|
||||
|
||||
$form->addHeaderText( $this->msg( 'changeemail-header' )->parseAsBlock() );
|
||||
if ( $this->getConfig()->get( 'RequirePasswordforEmailChange' ) ) {
|
||||
$form->addHeaderText( $this->msg( 'changeemail-passwordrequired' )->parseAsBlock() );
|
||||
}
|
||||
}
|
||||
|
||||
public function onSubmit( array $data ) {
|
||||
$password = isset( $data['Password'] ) ? $data['Password'] : null;
|
||||
$status = $this->attemptChange( $this->getUser(), $password, $data['NewEmail'] );
|
||||
|
||||
$this->status = $status;
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
public function onSuccess() {
|
||||
$request = $this->getRequest();
|
||||
|
||||
$returnto = $request->getVal( 'returnto' );
|
||||
$titleObj = $returnto !== null ? Title::newFromText( $returnto ) : null;
|
||||
if ( !$titleObj instanceof Title ) {
|
||||
$titleObj = Title::newMainPage();
|
||||
}
|
||||
$query = $request->getVal( 'returntoquery' );
|
||||
|
||||
if ( $this->status->value === true ) {
|
||||
$this->getOutput()->redirect( $titleObj->getFullURL( $query ) );
|
||||
} elseif ( $this->status->value === 'eauth' ) {
|
||||
# Notify user that a confirmation email has been sent...
|
||||
$this->getOutput()->wrapWikiMsg( "<div class='error' style='clear: both;'>\n$1\n</div>",
|
||||
'eauthentsent', $this->getUser()->getName() );
|
||||
// just show the link to go back
|
||||
$this->getOutput()->addReturnTo( $titleObj, wfCgiToArray( $query ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param string $pass
|
||||
* @param string $newaddr
|
||||
* @return Status
|
||||
*/
|
||||
private function attemptChange( User $user, $pass, $newaddr ) {
|
||||
global $wgAuth;
|
||||
|
||||
if ( $newaddr != '' && !Sanitizer::validateEmail( $newaddr ) ) {
|
||||
return Status::newFatal( 'invalidemailaddress' );
|
||||
}
|
||||
|
||||
if ( $newaddr === $user->getEmail() ) {
|
||||
return Status::newFatal( 'changeemail-nochange' );
|
||||
}
|
||||
|
||||
$throttleInfo = LoginForm::incrementLoginThrottle( $user->getName() );
|
||||
if ( $throttleInfo ) {
|
||||
$lang = $this->getLanguage();
|
||||
return Status::newFatal(
|
||||
'changeemail-throttled',
|
||||
$lang->formatDuration( $throttleInfo['wait'] )
|
||||
);
|
||||
}
|
||||
|
||||
if ( $this->getConfig()->get( 'RequirePasswordforEmailChange' )
|
||||
&& !$user->checkTemporaryPassword( $pass )
|
||||
&& !$user->checkPassword( $pass )
|
||||
) {
|
||||
return Status::newFatal( 'wrongpassword' );
|
||||
}
|
||||
|
||||
LoginForm::clearLoginThrottle( $user->getName() );
|
||||
|
||||
$oldaddr = $user->getEmail();
|
||||
$status = $user->setEmailWithConfirmation( $newaddr );
|
||||
if ( !$status->isGood() ) {
|
||||
return $status;
|
||||
}
|
||||
|
||||
Hooks::run( 'PrefsEmailAudit', [ $user, $oldaddr, $newaddr ] );
|
||||
|
||||
$user->saveSettings();
|
||||
|
||||
$wgAuth->updateExternalDB( $user );
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
public function requiresUnblock() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getGroupName() {
|
||||
return 'users';
|
||||
}
|
||||
}
|
||||
343
includes/specials/pre-authmanager/SpecialChangePassword.php
Normal file
343
includes/specials/pre-authmanager/SpecialChangePassword.php
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
<?php
|
||||
/**
|
||||
* Implements Special:ChangePassword
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @ingroup SpecialPage
|
||||
*/
|
||||
|
||||
/**
|
||||
* Let users recover their password.
|
||||
*
|
||||
* @ingroup SpecialPage
|
||||
*/
|
||||
class SpecialChangePasswordPreAuthManager extends FormSpecialPage {
|
||||
protected $mUserName;
|
||||
protected $mDomain;
|
||||
|
||||
// Optional Wikitext Message to show above the password change form
|
||||
protected $mPreTextMessage = null;
|
||||
|
||||
// label for old password input
|
||||
protected $mOldPassMsg = null;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct( 'ChangePassword', 'editmyprivateinfo' );
|
||||
$this->listed( false );
|
||||
}
|
||||
|
||||
public function doesWrites() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execution point
|
||||
* @param string|null $par
|
||||
*/
|
||||
function execute( $par ) {
|
||||
$this->getOutput()->disallowUserJs();
|
||||
|
||||
parent::execute( $par );
|
||||
}
|
||||
|
||||
protected function checkExecutePermissions( User $user ) {
|
||||
parent::checkExecutePermissions( $user );
|
||||
|
||||
if ( !$this->getRequest()->wasPosted() ) {
|
||||
$this->requireLogin( 'resetpass-no-info' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a message at the top of the Change Password form
|
||||
* @since 1.23
|
||||
* @param Message $msg Message to parse and add to the form header
|
||||
*/
|
||||
public function setChangeMessage( Message $msg ) {
|
||||
$this->mPreTextMessage = $msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a message at the top of the Change Password form
|
||||
* @since 1.23
|
||||
* @param string $msg Message label for old/temp password field
|
||||
*/
|
||||
public function setOldPasswordMessage( $msg ) {
|
||||
$this->mOldPassMsg = $msg;
|
||||
}
|
||||
|
||||
protected function getFormFields() {
|
||||
$user = $this->getUser();
|
||||
$request = $this->getRequest();
|
||||
|
||||
$oldpassMsg = $this->mOldPassMsg;
|
||||
if ( $oldpassMsg === null ) {
|
||||
$oldpassMsg = $user->isLoggedIn() ? 'oldpassword' : 'resetpass-temp-password';
|
||||
}
|
||||
|
||||
$fields = [
|
||||
'Name' => [
|
||||
'type' => 'info',
|
||||
'label-message' => 'username',
|
||||
'default' => $request->getVal( 'wpName', $user->getName() ),
|
||||
],
|
||||
'Password' => [
|
||||
'type' => 'password',
|
||||
'label-message' => $oldpassMsg,
|
||||
],
|
||||
'NewPassword' => [
|
||||
'type' => 'password',
|
||||
'label-message' => 'newpassword',
|
||||
],
|
||||
'Retype' => [
|
||||
'type' => 'password',
|
||||
'label-message' => 'retypenew',
|
||||
],
|
||||
];
|
||||
|
||||
if ( !$this->getUser()->isLoggedIn() ) {
|
||||
$fields['LoginOnChangeToken'] = [
|
||||
'type' => 'hidden',
|
||||
'label' => 'Change Password Token',
|
||||
'default' => LoginForm::getLoginToken()->toString(),
|
||||
];
|
||||
}
|
||||
|
||||
$extraFields = [];
|
||||
Hooks::run( 'ChangePasswordForm', [ &$extraFields ] );
|
||||
foreach ( $extraFields as $extra ) {
|
||||
list( $name, $label, $type, $default ) = $extra;
|
||||
$fields[$name] = [
|
||||
'type' => $type,
|
||||
'name' => $name,
|
||||
'label-message' => $label,
|
||||
'default' => $default,
|
||||
];
|
||||
}
|
||||
|
||||
if ( !$user->isLoggedIn() ) {
|
||||
$fields['Remember'] = [
|
||||
'type' => 'check',
|
||||
'label' => $this->msg( 'remembermypassword' )
|
||||
->numParams(
|
||||
ceil( $this->getConfig()->get( 'CookieExpiration' ) / ( 3600 * 24 ) )
|
||||
)->text(),
|
||||
'default' => $request->getVal( 'wpRemember' ),
|
||||
];
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
protected function alterForm( HTMLForm $form ) {
|
||||
$form->setId( 'mw-resetpass-form' );
|
||||
$form->setTableId( 'mw-resetpass-table' );
|
||||
$form->setWrapperLegendMsg( 'resetpass_header' );
|
||||
$form->setSubmitTextMsg(
|
||||
$this->getUser()->isLoggedIn()
|
||||
? 'resetpass-submit-loggedin'
|
||||
: 'resetpass_submit'
|
||||
);
|
||||
$form->addButton( [
|
||||
'name' => 'wpCancel',
|
||||
'value' => $this->msg( 'resetpass-submit-cancel' )->text()
|
||||
] );
|
||||
$form->setHeaderText( $this->msg( 'resetpass_text' )->parseAsBlock() );
|
||||
if ( $this->mPreTextMessage instanceof Message ) {
|
||||
$form->addPreText( $this->mPreTextMessage->parseAsBlock() );
|
||||
}
|
||||
$form->addHiddenFields(
|
||||
$this->getRequest()->getValues( 'wpName', 'wpDomain', 'returnto', 'returntoquery' ) );
|
||||
}
|
||||
|
||||
public function onSubmit( array $data ) {
|
||||
global $wgAuth;
|
||||
|
||||
$request = $this->getRequest();
|
||||
|
||||
if ( $request->getCheck( 'wpLoginToken' ) ) {
|
||||
// This comes from Special:Userlogin when logging in with a temporary password
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( !$this->getUser()->isLoggedIn()
|
||||
&& !LoginForm::getLoginToken()->match( $request->getVal( 'wpLoginOnChangeToken' ) )
|
||||
) {
|
||||
// Potential CSRF (bug 62497)
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $request->getCheck( 'wpCancel' ) ) {
|
||||
$returnto = $request->getVal( 'returnto' );
|
||||
$titleObj = $returnto !== null ? Title::newFromText( $returnto ) : null;
|
||||
if ( !$titleObj instanceof Title ) {
|
||||
$titleObj = Title::newMainPage();
|
||||
}
|
||||
$query = $request->getVal( 'returntoquery' );
|
||||
$this->getOutput()->redirect( $titleObj->getFullURL( $query ) );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->mUserName = $request->getVal( 'wpName', $this->getUser()->getName() );
|
||||
$this->mDomain = $wgAuth->getDomain();
|
||||
|
||||
if ( !$wgAuth->allowPasswordChange() ) {
|
||||
throw new ErrorPageError( 'changepassword', 'resetpass_forbidden' );
|
||||
}
|
||||
|
||||
$status = $this->attemptReset( $data['Password'], $data['NewPassword'], $data['Retype'] );
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
public function onSuccess() {
|
||||
if ( $this->getUser()->isLoggedIn() ) {
|
||||
$this->getOutput()->wrapWikiMsg(
|
||||
"<div class=\"successbox\">\n$1\n</div>",
|
||||
'changepassword-success'
|
||||
);
|
||||
$this->getOutput()->returnToMain();
|
||||
} else {
|
||||
$request = $this->getRequest();
|
||||
LoginForm::clearLoginToken();
|
||||
$token = LoginForm::getLoginToken()->toString();
|
||||
$data = [
|
||||
'action' => 'submitlogin',
|
||||
'wpName' => $this->mUserName,
|
||||
'wpDomain' => $this->mDomain,
|
||||
'wpLoginToken' => $token,
|
||||
'wpPassword' => $request->getVal( 'wpNewPassword' ),
|
||||
] + $request->getValues( 'wpRemember', 'returnto', 'returntoquery' );
|
||||
$login = new LoginForm( new DerivativeRequest( $request, $data, true ) );
|
||||
$login->setContext( $this->getContext() );
|
||||
$login->execute( null );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the new password if it meets the requirements for passwords and set
|
||||
* it as a current password, otherwise set the passed Status object to fatal
|
||||
* and doesn't change anything
|
||||
*
|
||||
* @param string $oldpass The current (temporary) password.
|
||||
* @param string $newpass The password to set.
|
||||
* @param string $retype The string of the retype password field to check with newpass
|
||||
* @return Status
|
||||
*/
|
||||
protected function attemptReset( $oldpass, $newpass, $retype ) {
|
||||
$isSelf = ( $this->mUserName === $this->getUser()->getName() );
|
||||
if ( $isSelf ) {
|
||||
$user = $this->getUser();
|
||||
} else {
|
||||
$user = User::newFromName( $this->mUserName );
|
||||
}
|
||||
|
||||
if ( !$user || $user->isAnon() ) {
|
||||
return Status::newFatal( $this->msg( 'nosuchusershort', $this->mUserName ) );
|
||||
}
|
||||
|
||||
if ( $newpass !== $retype ) {
|
||||
Hooks::run( 'PrefsPasswordAudit', [ $user, $newpass, 'badretype' ] );
|
||||
return Status::newFatal( $this->msg( 'badretype' ) );
|
||||
}
|
||||
|
||||
$throttleInfo = LoginForm::incrementLoginThrottle( $this->mUserName );
|
||||
if ( $throttleInfo ) {
|
||||
return Status::newFatal( $this->msg( 'changepassword-throttled' )
|
||||
->durationParams( $throttleInfo['wait'] )
|
||||
);
|
||||
}
|
||||
|
||||
// @todo Make these separate messages, since the message is written for both cases
|
||||
if ( !$user->checkTemporaryPassword( $oldpass ) && !$user->checkPassword( $oldpass ) ) {
|
||||
Hooks::run( 'PrefsPasswordAudit', [ $user, $newpass, 'wrongpassword' ] );
|
||||
return Status::newFatal( $this->msg( 'resetpass-wrong-oldpass' ) );
|
||||
}
|
||||
|
||||
// User is resetting their password to their old password
|
||||
if ( $oldpass === $newpass ) {
|
||||
return Status::newFatal( $this->msg( 'resetpass-recycled' ) );
|
||||
}
|
||||
|
||||
// Do AbortChangePassword after checking mOldpass, so we don't leak information
|
||||
// by possibly aborting a new password before verifying the old password.
|
||||
$abortMsg = 'resetpass-abort-generic';
|
||||
if ( !Hooks::run( 'AbortChangePassword', [ $user, $oldpass, $newpass, &$abortMsg ] ) ) {
|
||||
Hooks::run( 'PrefsPasswordAudit', [ $user, $newpass, 'abortreset' ] );
|
||||
return Status::newFatal( $this->msg( $abortMsg ) );
|
||||
}
|
||||
|
||||
// Please reset throttle for successful logins, thanks!
|
||||
LoginForm::clearLoginThrottle( $this->mUserName );
|
||||
|
||||
try {
|
||||
$user->setPassword( $newpass );
|
||||
Hooks::run( 'PrefsPasswordAudit', [ $user, $newpass, 'success' ] );
|
||||
} catch ( PasswordError $e ) {
|
||||
Hooks::run( 'PrefsPasswordAudit', [ $user, $newpass, 'error' ] );
|
||||
return Status::newFatal( new RawMessage( $e->getMessage() ) );
|
||||
}
|
||||
|
||||
if ( $isSelf ) {
|
||||
// This is needed to keep the user connected since
|
||||
// changing the password also modifies the user's token.
|
||||
$remember = $this->getRequest()->getCookie( 'Token' ) !== null;
|
||||
$user->setCookies( null, null, $remember );
|
||||
}
|
||||
$user->saveSettings();
|
||||
$this->resetPasswordExpiration( $user );
|
||||
return Status::newGood();
|
||||
}
|
||||
|
||||
public function requiresUnblock() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getGroupName() {
|
||||
return 'users';
|
||||
}
|
||||
|
||||
/**
|
||||
* For resetting user password expiration, until AuthManager comes along
|
||||
* @param User $user
|
||||
*/
|
||||
private function resetPasswordExpiration( User $user ) {
|
||||
global $wgPasswordExpirationDays;
|
||||
$newExpire = null;
|
||||
if ( $wgPasswordExpirationDays ) {
|
||||
$newExpire = wfTimestamp(
|
||||
TS_MW,
|
||||
time() + ( $wgPasswordExpirationDays * 24 * 3600 )
|
||||
);
|
||||
}
|
||||
// Give extensions a chance to force an expiration
|
||||
Hooks::run( 'ResetPasswordExpiration', [ $this, &$newExpire ] );
|
||||
$dbw = wfGetDB( DB_MASTER );
|
||||
$dbw->update(
|
||||
'user',
|
||||
[ 'user_password_expires' => $dbw->timestampOrNull( $newExpire ) ],
|
||||
[ 'user_id' => $user->getId() ],
|
||||
__METHOD__
|
||||
);
|
||||
}
|
||||
|
||||
protected function getDisplayFormat() {
|
||||
return 'ooui';
|
||||
}
|
||||
}
|
||||
60
includes/specials/pre-authmanager/SpecialCreateAccount.php
Normal file
60
includes/specials/pre-authmanager/SpecialCreateAccount.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
/**
|
||||
* Redirect page: Special:CreateAccount --> Special:UserLogin/signup.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @ingroup SpecialPage
|
||||
*/
|
||||
|
||||
/**
|
||||
* Redirect page: Special:CreateAccount --> Special:UserLogin/signup.
|
||||
* @todo FIXME: This (and the rest of the login frontend) needs to die a horrible painful death
|
||||
*
|
||||
* @ingroup SpecialPage
|
||||
*/
|
||||
class SpecialCreateAccountPreAuthManager extends SpecialRedirectToSpecial {
|
||||
function __construct() {
|
||||
parent::__construct(
|
||||
'CreateAccount',
|
||||
'Userlogin',
|
||||
'signup',
|
||||
[ 'returnto', 'returntoquery', 'uselang' ]
|
||||
);
|
||||
}
|
||||
|
||||
public function doesWrites() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// No reason to hide this link on Special:Specialpages
|
||||
public function isListed() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isRestricted() {
|
||||
return !User::groupHasPermission( '*', 'createaccount' );
|
||||
}
|
||||
|
||||
public function userCanExecute( User $user ) {
|
||||
return $user->isAllowed( 'createaccount' );
|
||||
}
|
||||
|
||||
protected function getGroupName() {
|
||||
return 'login';
|
||||
}
|
||||
}
|
||||
378
includes/specials/pre-authmanager/SpecialPasswordReset.php
Normal file
378
includes/specials/pre-authmanager/SpecialPasswordReset.php
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
<?php
|
||||
/**
|
||||
* Implements Special:PasswordReset
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @ingroup SpecialPage
|
||||
*/
|
||||
|
||||
/**
|
||||
* Special page for requesting a password reset email
|
||||
*
|
||||
* @ingroup SpecialPage
|
||||
*/
|
||||
class SpecialPasswordResetPreAuthManager extends FormSpecialPage {
|
||||
/**
|
||||
* @var Message
|
||||
*/
|
||||
private $email;
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $firstUser;
|
||||
|
||||
/**
|
||||
* @var Status
|
||||
*/
|
||||
private $result;
|
||||
|
||||
/**
|
||||
* @var string $method Identifies which password reset field was specified by the user.
|
||||
*/
|
||||
private $method;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct( 'PasswordReset', 'editmyprivateinfo' );
|
||||
}
|
||||
|
||||
public function doesWrites() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function userCanExecute( User $user ) {
|
||||
return $this->canChangePassword( $user ) === true && parent::userCanExecute( $user );
|
||||
}
|
||||
|
||||
public function checkExecutePermissions( User $user ) {
|
||||
$error = $this->canChangePassword( $user );
|
||||
if ( is_string( $error ) ) {
|
||||
throw new ErrorPageError( 'internalerror', $error );
|
||||
} elseif ( !$error ) {
|
||||
throw new ErrorPageError( 'internalerror', 'resetpass_forbidden' );
|
||||
}
|
||||
|
||||
parent::checkExecutePermissions( $user );
|
||||
}
|
||||
|
||||
protected function getFormFields() {
|
||||
global $wgAuth;
|
||||
$resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' );
|
||||
$a = [];
|
||||
if ( isset( $resetRoutes['username'] ) && $resetRoutes['username'] ) {
|
||||
$a['Username'] = [
|
||||
'type' => 'text',
|
||||
'label-message' => 'passwordreset-username',
|
||||
];
|
||||
|
||||
if ( $this->getUser()->isLoggedIn() ) {
|
||||
$a['Username']['default'] = $this->getUser()->getName();
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $resetRoutes['email'] ) && $resetRoutes['email'] ) {
|
||||
$a['Email'] = [
|
||||
'type' => 'email',
|
||||
'label-message' => 'passwordreset-email',
|
||||
];
|
||||
}
|
||||
|
||||
if ( isset( $resetRoutes['domain'] ) && $resetRoutes['domain'] ) {
|
||||
$domains = $wgAuth->domainList();
|
||||
$a['Domain'] = [
|
||||
'type' => 'select',
|
||||
'options' => $domains,
|
||||
'label-message' => 'passwordreset-domain',
|
||||
];
|
||||
}
|
||||
|
||||
if ( $this->getUser()->isAllowed( 'passwordreset' ) ) {
|
||||
$a['Capture'] = [
|
||||
'type' => 'check',
|
||||
'label-message' => 'passwordreset-capture',
|
||||
'help-message' => 'passwordreset-capture-help',
|
||||
];
|
||||
}
|
||||
|
||||
return $a;
|
||||
}
|
||||
|
||||
protected function getDisplayFormat() {
|
||||
return 'ooui';
|
||||
}
|
||||
|
||||
public function alterForm( HTMLForm $form ) {
|
||||
$resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' );
|
||||
|
||||
$form->addHiddenFields( $this->getRequest()->getValues( 'returnto', 'returntoquery' ) );
|
||||
|
||||
$i = 0;
|
||||
if ( isset( $resetRoutes['username'] ) && $resetRoutes['username'] ) {
|
||||
$i++;
|
||||
}
|
||||
if ( isset( $resetRoutes['email'] ) && $resetRoutes['email'] ) {
|
||||
$i++;
|
||||
}
|
||||
if ( isset( $resetRoutes['domain'] ) && $resetRoutes['domain'] ) {
|
||||
$i++;
|
||||
}
|
||||
|
||||
$message = ( $i > 1 ) ? 'passwordreset-text-many' : 'passwordreset-text-one';
|
||||
|
||||
$form->setHeaderText( $this->msg( $message, $i )->parseAsBlock() );
|
||||
$form->setSubmitTextMsg( 'mailmypassword' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the form. At this point we know that the user passes all the criteria in
|
||||
* userCanExecute(), and if the data array contains 'Username', etc, then Username
|
||||
* resets are allowed.
|
||||
* @param array $data
|
||||
* @throws MWException
|
||||
* @throws ThrottledError|PermissionsError
|
||||
* @return bool|array
|
||||
*/
|
||||
public function onSubmit( array $data ) {
|
||||
global $wgAuth, $wgMinimalPasswordLength;
|
||||
|
||||
if ( isset( $data['Domain'] ) ) {
|
||||
if ( $wgAuth->validDomain( $data['Domain'] ) ) {
|
||||
$wgAuth->setDomain( $data['Domain'] );
|
||||
} else {
|
||||
$wgAuth->setDomain( 'invaliddomain' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $data['Capture'] ) && !$this->getUser()->isAllowed( 'passwordreset' ) ) {
|
||||
// The user knows they don't have the passwordreset permission,
|
||||
// but they tried to spoof the form. That's naughty
|
||||
throw new PermissionsError( 'passwordreset' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @var $firstUser User
|
||||
* @var $users User[]
|
||||
*/
|
||||
|
||||
if ( isset( $data['Username'] ) && $data['Username'] !== '' ) {
|
||||
$method = 'username';
|
||||
$users = [ User::newFromName( $data['Username'] ) ];
|
||||
} elseif ( isset( $data['Email'] )
|
||||
&& $data['Email'] !== ''
|
||||
&& Sanitizer::validateEmail( $data['Email'] )
|
||||
) {
|
||||
$method = 'email';
|
||||
$res = wfGetDB( DB_SLAVE )->select(
|
||||
'user',
|
||||
User::selectFields(),
|
||||
[ 'user_email' => $data['Email'] ],
|
||||
__METHOD__
|
||||
);
|
||||
|
||||
if ( $res ) {
|
||||
$users = [];
|
||||
|
||||
foreach ( $res as $row ) {
|
||||
$users[] = User::newFromRow( $row );
|
||||
}
|
||||
} else {
|
||||
// Some sort of database error, probably unreachable
|
||||
throw new MWException( 'Unknown database error in ' . __METHOD__ );
|
||||
}
|
||||
} else {
|
||||
// The user didn't supply any data
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for hooks (captcha etc), and allow them to modify the users list
|
||||
$error = [];
|
||||
if ( !Hooks::run( 'SpecialPasswordResetOnSubmit', [ &$users, $data, &$error ] ) ) {
|
||||
return [ $error ];
|
||||
}
|
||||
|
||||
$this->method = $method;
|
||||
|
||||
if ( count( $users ) == 0 ) {
|
||||
if ( $method == 'email' ) {
|
||||
// Don't reveal whether or not an email address is in use
|
||||
return true;
|
||||
} else {
|
||||
return [ 'noname' ];
|
||||
}
|
||||
}
|
||||
|
||||
$firstUser = $users[0];
|
||||
|
||||
if ( !$firstUser instanceof User || !$firstUser->getId() ) {
|
||||
// Don't parse username as wikitext (bug 65501)
|
||||
return [ [ 'nosuchuser', wfEscapeWikiText( $data['Username'] ) ] ];
|
||||
}
|
||||
|
||||
// Check against the rate limiter
|
||||
if ( $this->getUser()->pingLimiter( 'mailpassword' ) ) {
|
||||
throw new ThrottledError;
|
||||
}
|
||||
|
||||
// Check against password throttle
|
||||
foreach ( $users as $user ) {
|
||||
if ( $user->isPasswordReminderThrottled() ) {
|
||||
|
||||
# Round the time in hours to 3 d.p., in case someone is specifying
|
||||
# minutes or seconds.
|
||||
return [ [
|
||||
'throttled-mailpassword',
|
||||
round( $this->getConfig()->get( 'PasswordReminderResendTime' ), 3 )
|
||||
] ];
|
||||
}
|
||||
}
|
||||
|
||||
// All the users will have the same email address
|
||||
if ( $firstUser->getEmail() == '' ) {
|
||||
// This won't be reachable from the email route, so safe to expose the username
|
||||
return [ [ 'noemail', wfEscapeWikiText( $firstUser->getName() ) ] ];
|
||||
}
|
||||
|
||||
// We need to have a valid IP address for the hook, but per bug 18347, we should
|
||||
// send the user's name if they're logged in.
|
||||
$ip = $this->getRequest()->getIP();
|
||||
if ( !$ip ) {
|
||||
return [ 'badipaddress' ];
|
||||
}
|
||||
$caller = $this->getUser();
|
||||
Hooks::run( 'User::mailPasswordInternal', [ &$caller, &$ip, &$firstUser ] );
|
||||
$username = $caller->getName();
|
||||
$msg = IP::isValid( $username )
|
||||
? 'passwordreset-emailtext-ip'
|
||||
: 'passwordreset-emailtext-user';
|
||||
|
||||
// Send in the user's language; which should hopefully be the same
|
||||
$userLanguage = $firstUser->getOption( 'language' );
|
||||
|
||||
$passwords = [];
|
||||
foreach ( $users as $user ) {
|
||||
$password = PasswordFactory::generateRandomPasswordString( $wgMinimalPasswordLength );
|
||||
$user->setNewpassword( $password );
|
||||
$user->saveSettings();
|
||||
$passwords[] = $this->msg( 'passwordreset-emailelement', $user->getName(), $password )
|
||||
->inLanguage( $userLanguage )->text(); // We'll escape the whole thing later
|
||||
}
|
||||
$passwordBlock = implode( "\n\n", $passwords );
|
||||
|
||||
$this->email = $this->msg( $msg )->inLanguage( $userLanguage );
|
||||
$this->email->params(
|
||||
$username,
|
||||
$passwordBlock,
|
||||
count( $passwords ),
|
||||
'<' . Title::newMainPage()->getCanonicalURL() . '>',
|
||||
round( $this->getConfig()->get( 'NewPasswordExpiry' ) / 86400 )
|
||||
);
|
||||
|
||||
$title = $this->msg( 'passwordreset-emailtitle' )->inLanguage( $userLanguage );
|
||||
|
||||
$this->result = $firstUser->sendMail( $title->text(), $this->email->text() );
|
||||
|
||||
if ( isset( $data['Capture'] ) && $data['Capture'] ) {
|
||||
// Save the user, will be used if an error occurs when sending the email
|
||||
$this->firstUser = $firstUser;
|
||||
} else {
|
||||
// Blank the email if the user is not supposed to see it
|
||||
$this->email = null;
|
||||
}
|
||||
|
||||
if ( $this->result->isGood() ) {
|
||||
return true;
|
||||
} elseif ( isset( $data['Capture'] ) && $data['Capture'] ) {
|
||||
// The email didn't send, but maybe they knew that and that's why they captured it
|
||||
return true;
|
||||
} else {
|
||||
// @todo FIXME: The email wasn't sent, but we have already set
|
||||
// the password throttle timestamp, so they won't be able to try
|
||||
// again until it expires... :(
|
||||
return [ [ 'mailerror', $this->result->getMessage() ] ];
|
||||
}
|
||||
}
|
||||
|
||||
public function onSuccess() {
|
||||
if ( $this->getUser()->isAllowed( 'passwordreset' ) && $this->email != null ) {
|
||||
// @todo Logging
|
||||
|
||||
if ( $this->result->isGood() ) {
|
||||
$this->getOutput()->addWikiMsg( 'passwordreset-emailsent-capture' );
|
||||
} else {
|
||||
$this->getOutput()->addWikiMsg( 'passwordreset-emailerror-capture',
|
||||
$this->result->getMessage(), $this->firstUser->getName() );
|
||||
}
|
||||
|
||||
$this->getOutput()->addHTML( Html::rawElement( 'pre', [], $this->email->escaped() ) );
|
||||
}
|
||||
|
||||
if ( $this->method === 'email' ) {
|
||||
$this->getOutput()->addWikiMsg( 'passwordreset-emailsentemail' );
|
||||
} else {
|
||||
$this->getOutput()->addWikiMsg( 'passwordreset-emailsentusername' );
|
||||
}
|
||||
|
||||
$this->getOutput()->returnToMain();
|
||||
}
|
||||
|
||||
protected function canChangePassword( User $user ) {
|
||||
global $wgAuth;
|
||||
$resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' );
|
||||
|
||||
// Maybe password resets are disabled, or there are no allowable routes
|
||||
if ( !is_array( $resetRoutes ) ||
|
||||
!in_array( true, array_values( $resetRoutes ) )
|
||||
) {
|
||||
return 'passwordreset-disabled';
|
||||
}
|
||||
|
||||
// Maybe the external auth plugin won't allow local password changes
|
||||
if ( !$wgAuth->allowPasswordChange() ) {
|
||||
return 'resetpass_forbidden';
|
||||
}
|
||||
|
||||
// Maybe email features have been disabled
|
||||
if ( !$this->getConfig()->get( 'EnableEmail' ) ) {
|
||||
return 'passwordreset-emaildisabled';
|
||||
}
|
||||
|
||||
// Maybe the user is blocked (check this here rather than relying on the parent
|
||||
// method as we have a more specific error message to use here
|
||||
if ( $user->isBlocked() ) {
|
||||
return 'blocked-mailpassword';
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the password reset page if resets are disabled.
|
||||
* @return bool
|
||||
*/
|
||||
function isListed() {
|
||||
if ( $this->canChangePassword( $this->getUser() ) === true ) {
|
||||
return parent::isListed();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getGroupName() {
|
||||
return 'users';
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ use MediaWiki\Session\SessionManager;
|
|||
*
|
||||
* @ingroup SpecialPage
|
||||
*/
|
||||
class LoginForm extends SpecialPage {
|
||||
class LoginFormPreAuthManager extends SpecialPage {
|
||||
const SUCCESS = 0;
|
||||
const NO_NAME = 1;
|
||||
const ILLEGAL = 2;
|
||||
|
|
@ -690,12 +690,7 @@ class LoginForm extends SpecialPage {
|
|||
|
||||
$status = $u->addToDatabase();
|
||||
if ( !$status->isOK() ) {
|
||||
if ( $status->hasMessage( 'userexists' ) ) {
|
||||
// AuthManager probably just added the user.
|
||||
$u->saveSettings();
|
||||
} else {
|
||||
return $status;
|
||||
}
|
||||
return $status;
|
||||
}
|
||||
|
||||
if ( $wgAuth->allowPasswordChange() ) {
|
||||
|
|
@ -707,12 +702,10 @@ class LoginForm extends SpecialPage {
|
|||
SessionManager::singleton()->invalidateSessionsForUser( $u );
|
||||
|
||||
Hooks::run( 'LocalUserCreated', [ $u, $autocreate ] );
|
||||
if ( $wgAuth && !$wgAuth instanceof MediaWiki\Auth\AuthManagerAuthPlugin ) {
|
||||
$oldUser = $u;
|
||||
$wgAuth->initUser( $u, $autocreate );
|
||||
if ( $oldUser !== $u ) {
|
||||
wfWarn( get_class( $wgAuth ) . '::initUser() replaced the user object' );
|
||||
}
|
||||
$oldUser = $u;
|
||||
$wgAuth->initUser( $u, $autocreate );
|
||||
if ( $oldUser !== $u ) {
|
||||
wfWarn( get_class( $wgAuth ) . '::initUser() replaced the user object' );
|
||||
}
|
||||
|
||||
$u->saveSettings();
|
||||
|
|
@ -864,12 +857,10 @@ class LoginForm extends SpecialPage {
|
|||
$this->mAbortLoginErrorMsg = 'resetpass-expired';
|
||||
} else {
|
||||
Hooks::run( 'UserLoggedIn', [ $u ] );
|
||||
if ( $wgAuth && !$wgAuth instanceof MediaWiki\Auth\AuthManagerAuthPlugin ) {
|
||||
$oldUser = $u;
|
||||
$wgAuth->updateUser( $u );
|
||||
if ( $oldUser !== $u ) {
|
||||
wfWarn( get_class( $wgAuth ) . '::updateUser() replaced the user object' );
|
||||
}
|
||||
$oldUser = $u;
|
||||
$wgAuth->updateUser( $u );
|
||||
if ( $oldUser !== $u ) {
|
||||
wfWarn( get_class( $wgAuth ) . '::updateUser() replaced the user object' );
|
||||
}
|
||||
$wgUser = $u;
|
||||
// This should set it for OutputPage and the Skin
|
||||
|
|
@ -1826,7 +1817,8 @@ class LoginForm extends SpecialPage {
|
|||
}
|
||||
|
||||
/**
|
||||
* Private function to check password expiration, until this is rewritten for AuthManager.
|
||||
* Private function to check password expiration, until AuthManager comes
|
||||
* along to handle that.
|
||||
* @param User $user
|
||||
* @return string|bool
|
||||
*/
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
*
|
||||
* @ingroup SpecialPage
|
||||
*/
|
||||
class SpecialUserlogout extends UnlistedSpecialPage {
|
||||
class SpecialUserlogoutPreAuthManager extends UnlistedSpecialPage {
|
||||
function __construct() {
|
||||
parent::__construct( 'Userlogout' );
|
||||
}
|
||||
|
|
@ -20,6 +20,8 @@
|
|||
*
|
||||
* @file
|
||||
* @ingroup Templates
|
||||
* @deprecated Will be removed when AuthManager lands.
|
||||
* The signup form will be generated via HTMLForm.
|
||||
*/
|
||||
|
||||
class UsercreateTemplate extends BaseTemplate {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@
|
|||
*
|
||||
* @file
|
||||
* @ingroup Templates
|
||||
* @deprecated Will be removed when AuthManager lands.
|
||||
* The login form will be generated via HTMLForm.
|
||||
*/
|
||||
|
||||
class UserloginTemplate extends BaseTemplate {
|
||||
|
|
|
|||
257
includes/user/PasswordReset.php
Normal file
257
includes/user/PasswordReset.php
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
<?php
|
||||
/**
|
||||
* User password reset helper for MediaWiki.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
use MediaWiki\Auth\AuthManager;
|
||||
use MediaWiki\Auth\TemporaryPasswordAuthenticationRequest;
|
||||
|
||||
/**
|
||||
* Helper class for the password reset functionality shared by the web UI and the API.
|
||||
*
|
||||
* Requires the TemporaryPasswordPrimaryAuthenticationProvider and the
|
||||
* EmailNotificationSecondaryAuthenticationProvider (or something providing equivalent
|
||||
* functionality) to be enabled.
|
||||
*/
|
||||
class PasswordReset {
|
||||
/** @var Config */
|
||||
protected $config;
|
||||
|
||||
/** @var AuthManager */
|
||||
protected $authManager;
|
||||
|
||||
/**
|
||||
* In-process cache for isAllowed lookups, by username. Contains pairs of StatusValue objects
|
||||
* (for false and true value of $displayPassword, respectively).
|
||||
* @var HashBagOStuff
|
||||
*/
|
||||
private $permissionCache;
|
||||
|
||||
public function __construct( Config $config, AuthManager $authManager ) {
|
||||
$this->config = $config;
|
||||
$this->authManager = $authManager;
|
||||
$this->permissionCache = new HashBagOStuff( [ 'maxKeys' => 1 ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given user has permission to use this functionality.
|
||||
* @param User $user
|
||||
* @param bool $displayPassword If set, also check whether the user is allowed to reset the
|
||||
* password of another user and see the temporary password.
|
||||
* @return StatusValue
|
||||
*/
|
||||
public function isAllowed( User $user, $displayPassword = false ) {
|
||||
$statuses = $this->permissionCache->get( $user->getName() );
|
||||
if ( $statuses ) {
|
||||
list ( $status, $status2 ) = $statuses;
|
||||
} else {
|
||||
$resetRoutes = $this->config->get( 'PasswordResetRoutes' );
|
||||
$status = StatusValue::newGood();
|
||||
|
||||
if ( !is_array( $resetRoutes ) ||
|
||||
!in_array( true, array_values( $resetRoutes ), true )
|
||||
) {
|
||||
// Maybe password resets are disabled, or there are no allowable routes
|
||||
$status = StatusValue::newFatal( 'passwordreset-disabled' );
|
||||
} elseif (
|
||||
( $providerStatus = $this->authManager->allowsAuthenticationDataChange(
|
||||
new TemporaryPasswordAuthenticationRequest(), false ) )
|
||||
&& !$providerStatus->isGood()
|
||||
) {
|
||||
// Maybe the external auth plugin won't allow local password changes
|
||||
$status = StatusValue::newFatal( 'resetpass_forbidden-reason',
|
||||
$providerStatus->getMessage() );
|
||||
} elseif ( !$this->config->get( 'EnableEmail' ) ) {
|
||||
// Maybe email features have been disabled
|
||||
$status = StatusValue::newFatal( 'passwordreset-emaildisabled' );
|
||||
} elseif ( !$user->isAllowed( 'editmyprivateinfo' ) ) {
|
||||
// Maybe not all users have permission to change private data
|
||||
$status = StatusValue::newFatal( 'badaccess' );
|
||||
} elseif ( $user->isBlocked() ) {
|
||||
// Maybe the user is blocked (check this here rather than relying on the parent
|
||||
// method as we have a more specific error message to use here
|
||||
$status = StatusValue::newFatal( 'blocked-mailpassword' );
|
||||
}
|
||||
|
||||
$status2 = StatusValue::newGood();
|
||||
if ( !$user->isAllowed( 'passwordreset' ) ) {
|
||||
$status2 = StatusValue::newFatal( 'badaccess' );
|
||||
}
|
||||
|
||||
$this->permissionCache->set( $user->getName(), [ $status, $status2 ] );
|
||||
}
|
||||
|
||||
if ( !$displayPassword || !$status->isGood() ) {
|
||||
return $status;
|
||||
} else {
|
||||
return $status2;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a password reset. Authorization is the caller's responsibility.
|
||||
*
|
||||
* Process the form. At this point we know that the user passes all the criteria in
|
||||
* userCanExecute(), and if the data array contains 'Username', etc, then Username
|
||||
* resets are allowed.
|
||||
* @param User $performingUser The user that does the password reset
|
||||
* @param string $username The user whose password is reset
|
||||
* @param string $email Alternative way to specify the user
|
||||
* @param bool $displayPassword Whether to display the password
|
||||
* @return StatusValue Will contain the passwords as a username => password array if the
|
||||
* $displayPassword flag was set
|
||||
* @throws LogicException When the user is not allowed to perform the action
|
||||
* @throws MWException On unexpected DB errors
|
||||
*/
|
||||
public function execute(
|
||||
User $performingUser, $username = null, $email = null, $displayPassword = false
|
||||
) {
|
||||
if ( !$this->isAllowed( $performingUser, $displayPassword )->isGood() ) {
|
||||
$action = $this->isAllowed( $performingUser )->isGood() ? 'display' : 'reset';
|
||||
throw new LogicException( 'User ' . $performingUser->getName()
|
||||
. ' is not allowed to ' . $action . ' passwords' );
|
||||
}
|
||||
|
||||
$resetRoutes = $this->config->get( 'PasswordResetRoutes' )
|
||||
+ [ 'username' => false, 'email' => false ];
|
||||
if ( $resetRoutes['username'] && $username ) {
|
||||
$method = 'username';
|
||||
$users = [ User::newFromName( $username ) ];
|
||||
} elseif ( $resetRoutes['email'] && $email ) {
|
||||
if ( !Sanitizer::validateEmail( $email ) ) {
|
||||
return StatusValue::newFatal( 'passwordreset-invalidemail' );
|
||||
}
|
||||
$method = 'email';
|
||||
$users = $this->getUsersByEmail( $email );
|
||||
} else {
|
||||
// The user didn't supply any data
|
||||
return StatusValue::newFatal( 'passwordreset-nodata' );
|
||||
}
|
||||
|
||||
// Check for hooks (captcha etc), and allow them to modify the users list
|
||||
$error = [];
|
||||
$data = [
|
||||
'Username' => $username,
|
||||
'Email' => $email,
|
||||
'Capture' => $displayPassword ? '1' : null,
|
||||
];
|
||||
if ( !Hooks::run( 'SpecialPasswordResetOnSubmit', [ &$users, $data, &$error ] ) ) {
|
||||
return StatusValue::newFatal( wfMessage( $error ) );
|
||||
}
|
||||
|
||||
if ( !$users ) {
|
||||
if ( $method === 'email' ) {
|
||||
// Don't reveal whether or not an email address is in use
|
||||
return StatusValue::newGood( [] );
|
||||
} else {
|
||||
return StatusValue::newFatal( 'noname' );
|
||||
}
|
||||
}
|
||||
|
||||
$firstUser = $users[0];
|
||||
|
||||
if ( !$firstUser instanceof User || !$firstUser->getId() ) {
|
||||
// Don't parse username as wikitext (bug 65501)
|
||||
return StatusValue::newFatal( wfMessage( 'nosuchuser', wfEscapeWikiText( $username ) ) );
|
||||
}
|
||||
|
||||
// Check against the rate limiter
|
||||
if ( $performingUser->pingLimiter( 'mailpassword' ) ) {
|
||||
return StatusValue::newFatal( 'actionthrottledtext' );
|
||||
}
|
||||
|
||||
// All the users will have the same email address
|
||||
if ( !$firstUser->getEmail() ) {
|
||||
// This won't be reachable from the email route, so safe to expose the username
|
||||
return StatusValue::newFatal( wfMessage( 'noemail',
|
||||
wfEscapeWikiText( $firstUser->getName() ) ) );
|
||||
}
|
||||
|
||||
// We need to have a valid IP address for the hook, but per bug 18347, we should
|
||||
// send the user's name if they're logged in.
|
||||
$ip = $performingUser->getRequest()->getIP();
|
||||
if ( !$ip ) {
|
||||
return StatusValue::newFatal( 'badipaddress' );
|
||||
}
|
||||
|
||||
Hooks::run( 'User::mailPasswordInternal', [ &$performingUser, &$ip, &$firstUser ] );
|
||||
|
||||
$result = StatusValue::newGood();
|
||||
$reqs = [];
|
||||
foreach ( $users as $user ) {
|
||||
$req = TemporaryPasswordAuthenticationRequest::newRandom();
|
||||
$req->username = $user->getName();
|
||||
$req->mailpassword = true;
|
||||
$req->hasBackchannel = $displayPassword;
|
||||
$req->caller = $performingUser->getName();
|
||||
$status = $this->authManager->allowsAuthenticationDataChange( $req, true );
|
||||
if ( $status->isGood() && $status->getValue() !== 'ignored' ) {
|
||||
$reqs[] = $req;
|
||||
} elseif ( $result->isGood() ) {
|
||||
// only record the first error, to avoid exposing the number of users having the
|
||||
// same email address
|
||||
if ( $status->getValue() === 'ignored' ) {
|
||||
$status = StatusValue::newFatal( 'passwordreset-ignored' );
|
||||
}
|
||||
$result->merge( $status );
|
||||
}
|
||||
}
|
||||
|
||||
if ( !$result->isGood() ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$passwords = [];
|
||||
foreach ( $reqs as $req ) {
|
||||
$this->authManager->changeAuthenticationData( $req );
|
||||
// TODO record mail sending errors
|
||||
if ( $displayPassword ) {
|
||||
$passwords[$req->username] = $req->password;
|
||||
}
|
||||
}
|
||||
|
||||
return StatusValue::newGood( $passwords );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $email
|
||||
* @return User[]
|
||||
* @throws MWException On unexpected database errors
|
||||
*/
|
||||
protected function getUsersByEmail( $email ) {
|
||||
$res = wfGetDB( DB_SLAVE )->select(
|
||||
'user',
|
||||
User::selectFields(),
|
||||
[ 'user_email' => $email ],
|
||||
__METHOD__
|
||||
);
|
||||
|
||||
if ( !$res ) {
|
||||
// Some sort of database error, probably unreachable
|
||||
throw new MWException( 'Unknown database error in ' . __METHOD__ );
|
||||
}
|
||||
|
||||
$users = [];
|
||||
foreach ( $res as $row ) {
|
||||
$users[] = User::newFromRow( $row );
|
||||
}
|
||||
return $users;
|
||||
}
|
||||
}
|
||||
|
|
@ -2667,9 +2667,7 @@ class User implements IDBAccessObject {
|
|||
/**
|
||||
* Set the password for a password reminder or new account email
|
||||
*
|
||||
* @deprecated since 1.27. Some way to do this via AuthManager (probably
|
||||
* involving TemporaryPasswordAuthenticationRequest) has yet to be
|
||||
* designed.
|
||||
* @deprecated Removed in 1.27. Use PasswordReset instead.
|
||||
* @param string $str New password to set or null to set an invalid
|
||||
* password hash meaning that the user will not be able to use it
|
||||
* @param bool $throttle If true, reset the throttle timestamp to the present
|
||||
|
|
|
|||
|
|
@ -415,6 +415,7 @@
|
|||
"password-change-forbidden": "You cannot change passwords on this wiki.",
|
||||
"externaldberror": "There was either an authentication database error or you are not allowed to update your external account.",
|
||||
"login": "Log in",
|
||||
"login-security": "Verify your identity",
|
||||
"nav-login-createaccount": "Log in / create account",
|
||||
"loginprompt": "",
|
||||
"userlogin": "Log in / create account",
|
||||
|
|
@ -435,19 +436,24 @@
|
|||
"helplogin-url": "https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Logging_in",
|
||||
"userlogin-helplink2": "Help with logging in",
|
||||
"userlogin-loggedin": "You are already logged in as {{GENDER:$1|$1}}.\nUse the form below to log in as another user.",
|
||||
"userlogin-reauth": "You must log in again to verify that you are {{GENDER:$1|$1}}.",
|
||||
"userlogin-createanother": "Create another account",
|
||||
"createacct-emailrequired": "Email address",
|
||||
"createacct-emailoptional": "Email address (optional)",
|
||||
"createacct-email-ph": "Enter your email address",
|
||||
"createacct-another-email-ph": "Enter email address",
|
||||
"createaccountmail": "Use a temporary random password and send it to the specified email address",
|
||||
"createaccountmail-help": "Can be used to create account for another person without learning the password.",
|
||||
"createacct-realname": "Real name (optional)",
|
||||
"createaccountreason": "Reason:",
|
||||
"createacct-reason": "Reason",
|
||||
"createacct-reason-ph": "Why you are creating another account",
|
||||
"createacct-reason-help": "Message shown in the account creation log",
|
||||
"createacct-imgcaptcha-help": "",
|
||||
"createacct-submit": "Create your account",
|
||||
"createacct-another-submit": "Create account",
|
||||
"createacct-continue-submit": "Continue account creation",
|
||||
"createacct-another-continue-submit": "Continue account creation",
|
||||
"createacct-benefit-heading": "{{SITENAME}} is made by people like you.",
|
||||
"createacct-benefit-icon1": "icon-edits",
|
||||
"createacct-benefit-head1": "{{NUMBEROFEDITS}}",
|
||||
|
|
@ -468,10 +474,11 @@
|
|||
"nocookieslogin": "{{SITENAME}} uses cookies to log in users.\nYou have cookies disabled.\nPlease enable them and try again.",
|
||||
"nocookiesfornew": "The user account was not created, as we could not confirm its source.\nEnsure you have cookies enabled, reload this page and try again.",
|
||||
"nocookiesforlogin": "{{int:nocookieslogin}}",
|
||||
"createacct-loginerror": "The account was successfully created but you could not be logged in automatically. Please proceed to [[Special:UserLogin|manual login]].",
|
||||
"noname": "You have not specified a valid username.",
|
||||
"loginsuccesstitle": "Logged in",
|
||||
"loginsuccess": "<strong>You are now logged in to {{SITENAME}} as \"$1\".</strong>",
|
||||
"nosuchuser": "There is no user by the name \"$1\".\nUsernames are case sensitive.\nCheck your spelling, or [[Special:UserLogin/signup|create a new account]].",
|
||||
"nosuchuser": "There is no user by the name \"$1\".\nUsernames are case sensitive.\nCheck your spelling, or [[Special:CreateAccount|create a new account]].",
|
||||
"nosuchusershort": "There is no user by the name \"$1\".\nCheck your spelling.",
|
||||
"nouserspecified": "You have to specify a username.",
|
||||
"login-userblocked": "This user is blocked. Login not allowed.",
|
||||
|
|
@ -517,6 +524,7 @@
|
|||
"createacct-another-realname-tip": "Real name is optional.\nIf you choose to provide it, this will be used for giving the user attribution for their work.",
|
||||
"pt-login": "Log in",
|
||||
"pt-login-button": "Log in",
|
||||
"pt-login-continue-button": "Continue login",
|
||||
"pt-createaccount": "Create account",
|
||||
"pt-userlogout": "Log out",
|
||||
"pear-mail-error": "$1",
|
||||
|
|
@ -567,6 +575,7 @@
|
|||
"botpasswords-invalid-name": "The username specified does not contain the bot password separator (\"$1\").",
|
||||
"botpasswords-not-exist": "User \"$1\" does not have a bot password named \"$2\".",
|
||||
"resetpass_forbidden": "Passwords cannot be changed",
|
||||
"resetpass_forbidden-reason": "Passwords cannot be changed: $1",
|
||||
"resetpass-no-info": "You must be logged in to access this page directly.",
|
||||
"resetpass-submit-loggedin": "Change password",
|
||||
"resetpass-submit-cancel": "Cancel",
|
||||
|
|
@ -596,6 +605,13 @@
|
|||
"passwordreset-emailsentusername": "If there is an email address associated with this username, then a password reset email will be sent.",
|
||||
"passwordreset-emailsent-capture": "A password reset email has been sent, which is shown below.",
|
||||
"passwordreset-emailerror-capture": "A password reset email was generated, which is shown below, but sending it to the {{GENDER:$2|user}} failed: $1",
|
||||
"passwordreset-emailsent-capture2": "The password reset {{PLURAL:$1|email has|emails have}} been sent. The {{PLURAL:$1|username and password|list of usernames and passwords}} is shown below.",
|
||||
"passwordreset-emailerror-capture2": "Emailing the {{GENDER:$2|user}} failed: $1 The {{PLURAL:$3|username and password|list of usernames and passwords}} is shown below.",
|
||||
"passwordreset-nocaller": "A caller must be provided",
|
||||
"passwordreset-nosuchcaller": "Caller does not exist: $1",
|
||||
"passwordreset-ignored": "The password reset was not handled. Maybe no provider was configured?",
|
||||
"passwordreset-invalideamil": "Invalid email address",
|
||||
"passwordreset-nodata": "Neither a username nor an email address was supplied",
|
||||
"changeemail": "Change or remove email address",
|
||||
"changeemail-summary": "",
|
||||
"changeemail-header": "Complete this form to change your email address. If you would like to remove the association of any email address from your account, leave the new email address blank when submitting the form.",
|
||||
|
|
@ -673,7 +689,7 @@
|
|||
"newarticletext": "You have followed a link to a page that does not exist yet.\nTo create the page, start typing in the box below (see the [$1 help page] for more info).\nIf you are here by mistake, click your browser's <strong>back</strong> button.",
|
||||
"newarticletextanon": "{{int:newarticletext|$1}}",
|
||||
"talkpagetext": "<!-- MediaWiki:talkpagetext -->",
|
||||
"anontalkpagetext": "----\n<em>This is the discussion page for an anonymous user who has not created an account yet, or who does not use it.</em>\nWe therefore have to use the numerical IP address to identify him/her.\nSuch an IP address can be shared by several users.\nIf you are an anonymous user and feel that irrelevant comments have been directed at you, please [[Special:UserLogin/signup|create an account]] or [[Special:UserLogin|log in]] to avoid future confusion with other anonymous users.",
|
||||
"anontalkpagetext": "----\n<em>This is the discussion page for an anonymous user who has not created an account yet, or who does not use it.</em>\nWe therefore have to use the numerical IP address to identify him/her.\nSuch an IP address can be shared by several users.\nIf you are an anonymous user and feel that irrelevant comments have been directed at you, please [[Special:CreateAccount|create an account]] or [[Special:UserLogin|log in]] to avoid future confusion with other anonymous users.",
|
||||
"noarticletext": "There is currently no text in this page.\nYou can [[Special:Search/{{PAGENAME}}|search for this page title]] in other pages,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} search the related logs],\nor [{{fullurl:{{FULLPAGENAME}}|action=edit}} create this page]</span>.",
|
||||
"noarticletext-nopermission": "There is currently no text in this page.\nYou can [[Special:Search/{{PAGENAME}}|search for this page title]] in other pages, or <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} search the related logs]</span>, but you do not have permission to create this page.",
|
||||
"noarticletextanon": "{{int:noarticletext}}",
|
||||
|
|
@ -4132,5 +4148,39 @@
|
|||
"authprovider-confirmlink-failed": "Account linking did not fully succeed: $1",
|
||||
"authprovider-confirmlink-ok-help": "Continue after displaying linking failure messages.",
|
||||
"authprovider-resetpass-skip-label": "Skip",
|
||||
"authprovider-resetpass-skip-help": "Skip resetting the password."
|
||||
"authprovider-resetpass-skip-help": "Skip resetting the password.",
|
||||
"authform-nosession-login": "The authentication was successful, but your browser cannot \"remember\" being logged in.\n\n$1",
|
||||
"authform-nosession-signup": "The account was created, but your browser cannot \"remember\" being logged in.\n\n$1",
|
||||
"authform-newtoken": "Missing token. $1",
|
||||
"authform-notoken": "Missing token",
|
||||
"authform-wrongtoken": "Wrong token",
|
||||
"specialpage-securitylevel-not-allowed-title": "Not allowed",
|
||||
"specialpage-securitylevel-not-allowed": "Sorry, you are not allowed to use this page because your identity could not be verified.",
|
||||
"authpage-cannot-login": "Unable to start login.",
|
||||
"authpage-cannot-login-continue": "Unable to continue login. Your session most likely timed out.",
|
||||
"authpage-cannot-create": "Unable to start account creation.",
|
||||
"authpage-cannot-create-continue": "Unable to continue account creation. Your session most likely timed out.",
|
||||
"authpage-cannot-link": "Unable to start account linking.",
|
||||
"authpage-cannot-link-continue": "Unable to continue account linking. Your session most likely timed out.",
|
||||
"cannotauth-not-allowed-title": "Permission denied",
|
||||
"cannotauth-not-allowed": "You are not allowed to use this page",
|
||||
"changecredentials" : "Change credentials",
|
||||
"changecredentials-submit": "Change",
|
||||
"changecredentials-submit-cancel": "Cancel",
|
||||
"changecredentials-invalidsubpage": "$1 is not a valid credential type.",
|
||||
"changecredentials-success": "Your credentials have been changed.",
|
||||
"removecredentials" : "Remove credentials",
|
||||
"removecredentials-submit": "Remove",
|
||||
"removecredentials-submit-cancel": "Cancel",
|
||||
"removecredentials-invalidsubpage": "$1 is not a valid credential type.",
|
||||
"removecredentials-success": "Your credentials have been removed.",
|
||||
"credentialsform-provider": "Credentials type:",
|
||||
"credentialsform-account": "Account name:",
|
||||
"cannotlink-no-provider-title": "There are no linkable accounts",
|
||||
"cannotlink-no-provider": "There are no linkable accounts.",
|
||||
"linkaccounts": "Link accounts",
|
||||
"linkaccounts-success-text": "The account was linked.",
|
||||
"linkaccounts-submit": "Link accounts",
|
||||
"unlinkaccounts": "Unlink accounts",
|
||||
"unlinkaccounts-success": "The account was unlinked."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -594,6 +594,7 @@
|
|||
"password-change-forbidden": "Error message shown when an external authentication source does not allow the password to be changed.",
|
||||
"externaldberror": "This message is thrown when a valid attempt to change the wiki password for a user fails because of a database error or an error from an external system.",
|
||||
"login": "{{Doc-special|UserLogin|unlisted=1}}\n{{Identical|Log in}}",
|
||||
"login-security": "Used as the title of the login page when the user is already logged in but sent to reauthenticate before getting access to a feature with elevated security.",
|
||||
"nav-login-createaccount": "Shown to anonymous users in the upper right corner of the page. When you can't create an account, the message {{msg-mw|login}} is shown.\n{{Identical|Log in / create account}}",
|
||||
"loginprompt": "{{ignored}}",
|
||||
"userlogin": "Since 1.22 no longer used in core, but may still be used by extensions. DEPRECATED\n\n{{Identical|Log in / create account}}",
|
||||
|
|
@ -614,19 +615,24 @@
|
|||
"helplogin-url": "{{doc-important|Do not translate the namespace name <code>Help</code>.}}\nUsed as name of the page that provides information about logging into the wiki.\n\nUsed as a link target in the message {{msg-mw|Userlogin-helplink}}.",
|
||||
"userlogin-helplink2": "Label for a link to login help.\n\nSee example: [[Special:UserLogin]]\n\nSee also:\n* {{msg-mw|Helplogin-url}}",
|
||||
"userlogin-loggedin": "Used as warning on [[Special:UserLogin]] when the current user is already logged in.\n\nFollowed by the Login form.\n\nSee example: [[Special:UserLogin]].\n\nParameters:\n* $1 - user name (used for display and for gender support)\nSee also:\n* {{msg-mw|Mobile-frontend-userlogin-loggedin-register}}",
|
||||
"userlogin-reauth": "Used as an explanatory message on [[Special:UserLogin]] when the user is redirected there to log in again when trying to use a security-sensitive page.\n\nParameters:\n* $1 - user name (used for display and for gender support)",
|
||||
"userlogin-createanother": "Used as label for the button on [[Special:UserLogin]] shown when the current user is already logged in.\n{{Identical|Create another account}}",
|
||||
"createacct-emailrequired": "Label in create account form for email field when it is required.\n\nSee also:\n* {{msg-mw|Createacct-emailoptional}}\n{{Identical|E-mail address}}",
|
||||
"createacct-emailoptional": "Label in vertical-layout create account form for email field when it is optional.\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]\n\nSee also:\n* {{msg-mw|Createacct-emailrequired}}",
|
||||
"createacct-email-ph": "Placeholder in vertical-layout create account form for email field.\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]",
|
||||
"createacct-another-email-ph": "Placeholder in create account form for email field when one user creates an account for another.",
|
||||
"createaccountmail": "The label for the checkbox for creating a new account and sending the new password to the specified email address directly, as used on [[Special:UserLogin/signup]] when one user creates an account for another (if creating accounts by email is allowed).\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]",
|
||||
"createaccountmail": "The label for the checkbox for creating a new account and sending the new password to the specified email address directly, as used on [[Special:CreateAccount]] when one user creates an account for another (if creating accounts by email is allowed).\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]",
|
||||
"createaccountmail-help": "Account creation API help message for the <code>mailpassword</code> parameter.",
|
||||
"createacct-realname": "In vertical-layout create account form, label for field to enter optional real name.",
|
||||
"createaccountreason": "Since 1.22 no longer used in core, but may be used by some extensions. DEPRECATED\n\n{{Identical|Reason}}",
|
||||
"createacct-reason": "In create account form, label for field to enter reason to create an account when already logged-in.\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]\n{{Identical|Reason}}",
|
||||
"createacct-reason-ph": "Placeholder in vertical-layout create account form for reason field.\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]",
|
||||
"createacct-reason-help": "Account creation API help message for the <code>reason</code> parameter.",
|
||||
"createacct-imgcaptcha-help": "{{Optional}} Optional help text in vertical-layout create account form for image CAPTCHA input field when repositioned by JavaScript.\n\nBlank by default.",
|
||||
"createacct-submit": "Submit button on vertical-layout create account form.\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]",
|
||||
"createacct-another-submit": "Submit button of [[Special:UserLogin/signup]] ([[Special:CreateAccount]]) when accessed by a registered user.\n\nThe original means \"create an account in addition to the one you already have\"; sometimes, but not always, it means you are going to \"Create the account on behalf of somebody else\" or \"Create account for another\".\n{{Identical|Create another account}}",
|
||||
"createacct-another-submit": "Submit button of [[Special:CreateAccount]] when accessed by a registered user.\n\nThe original means \"create an account in addition to the one you already have\"; sometimes, but not always, it means you are going to \"Create the account on behalf of somebody else\" or \"Create account for another\".\n{{Identical|Create another account}}",
|
||||
"createacct-continue-submit": "Submit button on create account form in second and later steps of a multistep account creation.",
|
||||
"createacct-another-continue-submit": "Submit button on create account form in second and later steps of a multistep account creation, when done by a registered user.",
|
||||
"createacct-benefit-heading": "In vertical-layout create account form, the heading for the section describing the benefits of creating an account. See example: [{{canonicalurl:Special:UserLogin|type=signup}} Special:UserLogin?type=signup]\n\nIf in your language you need to know the gender of the name for the wiki (which is the subject of the English sentence), please adapt the sentence as much as you need for your translation to fit.",
|
||||
"createacct-benefit-icon1": "In vertical-layout create account form, the CSS style for the div next to the first benefit. If you replace this you will need probably need to adjust CSS.\n\nUsed as a CSS class name.\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup&useNew=1}} Special:UserLogin?type=signup&useNew=1]",
|
||||
"createacct-benefit-head1": "In vertical-layout create account form, the text in the heading for the first benefit. Do not edit the magic word; if you replace it you will probably need to adjust CSS.\n\nFollowed by the message {{msg-mw|Createacct-benefit-body1}}.\n\nSee example: [{{canonicalurl:Special:UserLogin|type=signup&useNew=1}} Special:UserLogin?type=signup&useNew=1]",
|
||||
|
|
@ -647,6 +653,7 @@
|
|||
"nocookieslogin": "This message is displayed when someone tried to login, but the browser doesn't accept cookies.",
|
||||
"nocookiesfornew": "This message is displayed when the user tried to create a new account, but it failed the cross-site request forgery (CSRF) check. It could be blocking an attack, but most likely, the browser isn't accepting cookies.",
|
||||
"nocookiesforlogin": "{{optional}}\nThis message is displayed when someone tried to login and the CSRF failed (most likely, the browser doesn't accept cookies).\n\nDefault:\n* {{msg-mw|Nocookieslogin}}",
|
||||
"createacct-loginerror": "This message is displayed after a successful registration when there is a server-side error with logging the user in. This is not expected to happen.",
|
||||
"noname": "Error message.",
|
||||
"loginsuccesstitle": "The title of the page saying that you are logged in. The content of the page is the message {{msg-mw|Loginsuccess}}.\n{{Identical|Log in}}",
|
||||
"loginsuccess": "The content of the page saying that you are logged in. The title of the page is {{msg-mw|Loginsuccesstitle}}.\n\nParameters:\n* $1 - the name of the logged in user\n{{Gender}}",
|
||||
|
|
@ -696,6 +703,7 @@
|
|||
"createacct-another-realname-tip": "{{doc-singularthey}}\nUsed on the account creation form when creating another user's account. Similar to {{msg-mw|prefs-help-realname}}.\n{{Identical|Real name attribution}}",
|
||||
"pt-login": "Shown to anonymous users in the upper right corner of the page when they can't create an account (otherwise the message {{msg-mw|nav-login-createaccount}} is shown there).\n{{Identical|Log in}}",
|
||||
"pt-login-button": "Shown as the caption of the button at [[Special:UserLogin]].\n{{Identical|Log in}}",
|
||||
"pt-login-continue-button": "Shown as the caption of the button at [[Special:UserLogin]] in second and later steps of a multipage login.",
|
||||
"pt-createaccount": "Used on the top of the page for logged out users, where it appears next to {{msg-mw|login}}, so consider making them similar.\n{{Identical|Create account}}",
|
||||
"pt-userlogout": "{{Doc-actionlink}}\n{{Identical|Log out}}",
|
||||
"pear-mail-error": "{{notranslate}}\nParameters:\n* $1 - error message which is returned by PEAR mailer.",
|
||||
|
|
@ -746,6 +754,7 @@
|
|||
"botpasswords-invalid-name": "Error message when a username lacking the separator character is passed to BotPassword. Parameters:\n* $1 - The separator character.",
|
||||
"botpasswords-not-exist": "Error message when a username exists but does not a bot password for the given \"bot name\". Parameters:\n* $1 - username\n* $2 - bot name",
|
||||
"resetpass_forbidden": "Used as error message in changing password. Maybe the external auth plugin won't allow local password changes.",
|
||||
"resetpass_forbidden-reason": "Like {{msg-mw|resetpass_forbidden}} but the auth provider gave a reason.\n\nParameters:\n* $1 - reason given by auth provider",
|
||||
"resetpass-no-info": "Error message for [[Special:ChangePassword]].\n\nParameters:\n* $1 (unused) - a link to [[Special:UserLogin]] with {{msg-mw|loginreqlink}} as link description",
|
||||
"resetpass-submit-loggedin": "Button on [[Special:ResetPass]] to submit new password.\n\n{{Identical|Change password}}",
|
||||
"resetpass-submit-cancel": "Used on [[Special:ResetPass]].\n{{Identical|Cancel}}",
|
||||
|
|
@ -775,6 +784,13 @@
|
|||
"passwordreset-emailsentusername": "Used in [[Special:PasswordReset]].\n\nSee also:\n* {{msg-mw|Passwordreset-emailsent-capture}}\n* {{msg-mw|Passwordreset-emailerror-capture}}",
|
||||
"passwordreset-emailsent-capture": "Used in [[Special:PasswordReset]].\n\nSee also:\n* {{msg-mw|Passwordreset-emailsentemail}}\n* {msg-mw|Passwordreset-emailsentusername}}\n* {{msg-mw|Passwordreset-emailerror-capture}}",
|
||||
"passwordreset-emailerror-capture": "Error message displayed in [[Special:PasswordReset]] when sending an email fails. Parameters:\n* $1 - error message\n* $2 - username, used for GENDER\nSee also:\n* {{msg-mw|Passwordreset-emailsentemail}}\n* {msg-mw|Passwordreset-emailsentusername}}\n* {{msg-mw|Passwordreset-emailsent-capture}}",
|
||||
"passwordreset-emailsent-capture2": "Used in [[Special:PasswordReset]].\n\nParameters:\n* $1 - number of accounts notified\n\nSee also:\n* {{msg-mw|Passwordreset-emailsentemail}}\n* {msg-mw|Passwordreset-emailsentusername}}\n* {{msg-mw|Passwordreset-emailerror-capture}}",
|
||||
"passwordreset-emailerror-capture2": "Error message displayed in [[Special:PasswordReset]] when sending an email fails. Parameters:\n* $1 - error message\n* $2 - username, used for GENDER\n* $3 - number of accounts notified\n\nSee also:\n* {{msg-mw|Passwordreset-emailsentemail}}\n* {msg-mw|Passwordreset-emailsentusername}}\n* {{msg-mw|Passwordreset-emailsent-capture}}",
|
||||
"passwordreset-nocaller": "Shown when a password reset was requested but the caller was not provided. This is an internal error.",
|
||||
"passwordreset-nosuchcaller": "Shown when a password reset was requested but the username of the caller could not be resolved to a user. This is an internal error.\n\nParameters:\n* $1 - username of the caller",
|
||||
"passwordreset-ignored": "Shown when password reset was unsuccessful due to configuration problems.",
|
||||
"passwordreset-invalideamil": "Returned when the email address is syntatically invalid.",
|
||||
"passwordreset-nodata": "Returned when no data was provided.",
|
||||
"changeemail": "Title of [[Special:ChangeEmail|special page]]. This page also allows removing the user's email address.",
|
||||
"changeemail-summary": "{{ignored}}",
|
||||
"changeemail-header": "Text of [[Special:ChangeEmail]].",
|
||||
|
|
@ -826,7 +842,7 @@
|
|||
"showpreview": "The text of the button to preview the page you are editing. See also {{msg-mw|showdiff}} and {{msg-mw|savearticle}} for the other buttons.\n\nSee also:\n* {{msg-mw|Showpreview}}\n* {{msg-mw|Accesskey-preview}}\n* {{msg-mw|Tooltip-preview}}\n{{Identical|Show preview}}",
|
||||
"showdiff": "Button below the edit page. See also {{msg-mw|Showpreview}} and {{msg-mw|Savearticle}} for the other buttons.\n\nSee also:\n* {{msg-mw|Showdiff}}\n* {{msg-mw|Accesskey-diff}}\n* {{msg-mw|Tooltip-diff}}\n{{Identical|Show change}}",
|
||||
"blankarticle": "Notice displayed once after the user tries to save an empty page.",
|
||||
"anoneditwarning": "Shown when editing a page anonymously.\n\nParameters:\n* $1 – A link to log in, <nowiki>{{fullurl:Special:UserLogin|returnto={{FULLPAGENAMEE}}}}</nowiki>\n* $2 – A link to sign up, <nowiki>{{fullurl:Special:UserLogin/signup|returnto={{FULLPAGENAMEE}}}}</nowiki>\n\nSee also:\n* {{msg-mw|Mobile-frontend-editor-anonwarning}}",
|
||||
"anoneditwarning": "Shown when editing a page anonymously.\n\nParameters:\n* $1 – A link to log in, <nowiki>{{fullurl:Special:UserLogin|returnto={{FULLPAGENAMEE}}}}</nowiki>\n* $2 – A link to sign up, <nowiki>{{fullurl:Special:CreateAccount|returnto={{FULLPAGENAMEE}}}}</nowiki>\n\nSee also:\n* {{msg-mw|Mobile-frontend-editor-anonwarning}}",
|
||||
"anonpreviewwarning": "See also:\n* {{msg-mw|Anoneditwarning}}",
|
||||
"missingsummary": "The text \"edit summary\" is in {{msg-mw|Summary}}.\n\nSee also:\n* {{msg-mw|Missingcommentheader}}\n* {{msg-mw|Savearticle}}",
|
||||
"selfredirect": "Notice displayed once after the user tries to create a redirect to the same article.",
|
||||
|
|
@ -974,7 +990,7 @@
|
|||
"undo-summary-username-hidden": "Edit summary for an undo action where the username of the old revision is hidden.\n\nParameters:\n* $1 - the revision ID being undone\nSee also:\n* {{msg-mw|Undo-summary}}",
|
||||
"cantcreateaccounttitle": "Used as title of the error message {{msg-mw|Cantcreateaccount-text}}.",
|
||||
"cantcreateaccount-text": "Used as error message, with the title {{msg-mw|Cantcreateaccounttitle}}.\n* $1 - target IP address\n* $2 - reason or {{msg-mw|Blockednoreason}}\n* $3 - username\nSee also:\n* {{msg-mw|Cantcreateaccount-range-text}}",
|
||||
"cantcreateaccount-range-text": "Used as more detailed version of the {{msg-mw|Cantcreateaccount-text}} error message, with the title {{msg-mw|Cantcreateaccounttitle}}.\n* $1 - target IP address range\n* $2 - reason or {{msg-mw|Blockednoreason}}\n* $3 - username\n* $4 - current user's IP address",
|
||||
"cantcreateaccount-range-text": "Used instead of the {{msg-mw|Cantcreateaccount-text}} when the block is a range block.\n* $1 - target IP address range\n* $2 - reason or {{msg-mw|Blockednoreason}}\n* $3 - username\n* $4 - current user's IP address",
|
||||
"createaccount-hook-aborted": "Placeholder message to return with API errors on account create; passes through the message from a hook {{notranslate}}",
|
||||
"viewpagelogs": "Link displayed in history of pages",
|
||||
"nohistory": "Message shown when there are no history to list. See [{{canonicalurl:x|action=history}} example history].\n----\nAlso used as title of error message when the feed is empty. See [{{canonicalurl:x|action=history&feed=atom}} example feed].\n\nSee the error message:\n* {{msg-mw|history-feed-empty}}",
|
||||
|
|
@ -4311,5 +4327,39 @@
|
|||
"authprovider-confirmlink-failed": "Used to prefix the list of individual link statuses when some did not succeed. Parameters:\n* $1 - Failure message, or a wikitext bulleted list of failure messages.\n\nSee also:\n* {{msg-mw|authprovider-confirmlink-success-line}}\n* {{msg-mw|authprovider-confirmlink-failed-line}}",
|
||||
"authprovider-confirmlink-ok-help": "Description of the \"ok\" field when ConfirmLinkSecondaryAuthenticationProvider needs to display link failure messages to the user.",
|
||||
"authprovider-resetpass-skip-label": "Label for the \"Skip\" button when it's possible to skip resetting a password in ResetPasswordSecondaryAuthenticationProvider.",
|
||||
"authprovider-resetpass-skip-help": "Description of the option to skip resetting a password in ResetPasswordSecondaryAuthenticationProvider."
|
||||
"authprovider-resetpass-skip-help": "Description of the option to skip resetting a password in ResetPasswordSecondaryAuthenticationProvider.",
|
||||
"authform-nosession-login": "Error message shown when the login was successful, but the session could not be reestablished on the next request. $1 is an explanation which depends on what session handler is being used, such as {{msg-mw|sessionprovider-nocookies}}.",
|
||||
"authform-nosession-signup": "Error message shown when the account creation was successful, but the session could not be reestablished on the next request. $1 is an explanation which depends on what session handler is being used, such as {{msg-mw|sessionprovider-nocookies}}.",
|
||||
"authform-newtoken": "Error message shown on the auth form when the session has no CSRF token. This can be caused by session expiry but it is more likely that the client does not support sessions for some reason (e.g. a browser with all cookies diabled). $1 is an explanation (in the form of full sentences) given by the session provider of why sessions might not work (usually this will be {{msg-mw|sessionprovider-nocookies}}).",
|
||||
"authform-notoken": "Error message shown on the auth form when the submitted data has no CSRF token.",
|
||||
"authform-wrongtoken": "Error message shown on the auth form when the submitted CSRF token value is invalid.",
|
||||
"specialpage-securitylevel-not-allowed-title": "Error page title shown when the user visits a special page but the authentication security check fails.",
|
||||
"specialpage-securitylevel-not-allowed": "Error message shown when the user visits a special page but the authentication security check fails.",
|
||||
"authpage-cannot-login": "Error message shown on authentication-related special pages when login cannot start. This is not supposed to happen unless the site is misconfigured.",
|
||||
"authpage-cannot-login-continue": "Error message shown on authentication-related special pages when login cannot continue. This most likely means a session timeout.",
|
||||
"authpage-cannot-create": "Error message shown on authentication-related special pages when account creation cannot start. This is not supposed to happen unless the site is misconfigured.",
|
||||
"authpage-cannot-create-continue": "Error message shown on authentication-related special pages when account creation cannot continue. This most likely means a session timeout.",
|
||||
"authpage-cannot-link": "Error message shown on authentication-related special pages when account linking cannot start. This is not supposed to happen unless the site is misconfigured.",
|
||||
"authpage-cannot-link-continue": "Error message shown on authentication-related special pages when account linking cannot continue. This most likely means a session timeout.",
|
||||
"cannotauth-not-allowed-title": "Title of the error page shown when the user tries t use an authentication-related page they should not have access to.",
|
||||
"cannotauth-not-allowed": "Text of the error page shown when the user tries t use an authentication-related page they should not have access to.",
|
||||
"changecredentials" : "Title of the special page [[Special:ChangeCredentials]] which allows changing authentication credentials (such as the password).",
|
||||
"changecredentials-submit": "Used on [[Special:ChangeCredentials]].",
|
||||
"changecredentials-submit-cancel": "Used on [[Special:ChangeCredentials]].\n{{Identical|Cancel}}",
|
||||
"changecredentials-invalidsubpage": "Error message shown when using [[Special:ChangeCredentials]] with an invalid type.\n\nParameters:\n* $1 - subpage name.",
|
||||
"changecredentials-success": "Success message after using [[Special:ChangeCredentials]].",
|
||||
"removecredentials" : "Title of the special page [[Special:RemoveCredentials]] which allows removing authentication credentials (such as a two-factor token).",
|
||||
"removecredentials-submit": "Used on [[Special:RemoveCredentials]].",
|
||||
"removecredentials-submit-cancel": "Used on [[Special:RemoveCredentials]].\n{{Identical|Cancel}}",
|
||||
"removecredentials-invalidsubpage": "Error message shown when using [[Special:RemoveCredentials]] with an invalid type.\n\nParameters:\n* $1 - subpage name.",
|
||||
"removecredentials-success": "Success message after using [[Special:RemoveCredentials]].",
|
||||
"credentialsform-provider": "Shown on [[Special:ChangeCredentials]]/[[Special:RemoveCredentials]] as the label for the authentication type (e.g. \"password\", \"English Wikipedia via OAuth\")",
|
||||
"credentialsform-account": "Shown on [[Special:ChangeCredentials]]/[[Special:RemoveCredentials]] as the label for the account name",
|
||||
"cannotlink-no-provider-title": "Error page title shown when the user visits [[Special:LinkAccounts]] but there is no external account provider that could be linked.",
|
||||
"cannotlink-no-provider": "Error message shown when the user visits [[Special:LinkAccounts]] but there is no external account provider that could be linked.",
|
||||
"linkaccounts": "Title of the special page [[Special:LinkAccounts]] which allows the user to connect the local user accounts with external ones such as Google or Facebook.",
|
||||
"linkaccounts-success-text": "Text shown on top of the form after a successful action.",
|
||||
"linkaccounts-submit": "Text of the main submit button on [[Special:LinkAccounts]] (when there is one)",
|
||||
"unlinkaccounts": "Title of the special page [[Special:UnlinkAccounts]] which allows the user to remove linked remote accounts.",
|
||||
"unlinkaccounts-success": "Account unlinking form success message"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -407,6 +407,7 @@ $specialPageAliases = [
|
|||
'BrokenRedirects' => [ 'BrokenRedirects' ],
|
||||
'Categories' => [ 'Categories' ],
|
||||
'ChangeContentModel' => [ 'ChangeContentModel' ],
|
||||
'ChangeCredentials' => [ 'ChangeCredentials' ],
|
||||
'ChangeEmail' => [ 'ChangeEmail' ],
|
||||
'ChangePassword' => [ 'ChangePassword', 'ResetPass', 'ResetPassword' ],
|
||||
'ComparePages' => [ 'ComparePages' ],
|
||||
|
|
@ -430,6 +431,7 @@ $specialPageAliases = [
|
|||
'JavaScriptTest' => [ 'JavaScriptTest' ],
|
||||
'BlockList' => [ 'BlockList', 'ListBlocks', 'IPBlockList' ],
|
||||
'LinkSearch' => [ 'LinkSearch' ],
|
||||
'LinkAccounts' => [ 'LinkAccounts' ],
|
||||
'Listadmins' => [ 'ListAdmins' ],
|
||||
'Listbots' => [ 'ListBots' ],
|
||||
'Listfiles' => [ 'ListFiles', 'FileList', 'ImageList' ],
|
||||
|
|
@ -475,6 +477,7 @@ $specialPageAliases = [
|
|||
'Recentchanges' => [ 'RecentChanges' ],
|
||||
'Recentchangeslinked' => [ 'RecentChangesLinked', 'RelatedChanges' ],
|
||||
'Redirect' => [ 'Redirect' ],
|
||||
'RemoveCredentials' => [ 'RemoveCredentials' ],
|
||||
'ResetTokens' => [ 'ResetTokens' ],
|
||||
'Revisiondelete' => [ 'RevisionDelete' ],
|
||||
'RunJobs' => [ 'RunJobs' ],
|
||||
|
|
@ -490,6 +493,7 @@ $specialPageAliases = [
|
|||
'Uncategorizedpages' => [ 'UncategorizedPages' ],
|
||||
'Uncategorizedtemplates' => [ 'UncategorizedTemplates' ],
|
||||
'Undelete' => [ 'Undelete' ],
|
||||
'UnlinkAccounts' => [ 'UnlinkAccounts' ],
|
||||
'Unlockdb' => [ 'UnlockDB' ],
|
||||
'Unusedcategories' => [ 'UnusedCategories' ],
|
||||
'Unusedimages' => [ 'UnusedFiles', 'UnusedImages' ],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Auth;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class EmailNotificationSecondaryAuthenticationProviderTest extends \PHPUnit_Framework_TestCase {
|
||||
public function testConstructor() {
|
||||
$config = new \HashConfig( [
|
||||
'EnableEmail' => true,
|
||||
'EmailAuthentication' => true,
|
||||
] );
|
||||
|
||||
$provider = new EmailNotificationSecondaryAuthenticationProvider();
|
||||
$provider->setConfig( $config );
|
||||
$providerPriv = \TestingAccessWrapper::newFromObject( $provider );
|
||||
$this->assertTrue( $providerPriv->sendConfirmationEmail );
|
||||
|
||||
$provider = new EmailNotificationSecondaryAuthenticationProvider( [
|
||||
'sendConfirmationEmail' => false,
|
||||
] );
|
||||
$provider->setConfig( $config );
|
||||
$providerPriv = \TestingAccessWrapper::newFromObject( $provider );
|
||||
$this->assertFalse( $providerPriv->sendConfirmationEmail );
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideGetAuthenticationRequests
|
||||
* @param string $action
|
||||
* @param AuthenticationRequest[] $expected
|
||||
*/
|
||||
public function testGetAuthenticationRequests( $action, $expected ) {
|
||||
$provider = new EmailNotificationSecondaryAuthenticationProvider( [
|
||||
'sendConfirmationEmail' => true,
|
||||
] );
|
||||
$this->assertSame( $expected, $provider->getAuthenticationRequests( $action, [] ) );
|
||||
}
|
||||
|
||||
public function provideGetAuthenticationRequests() {
|
||||
return [
|
||||
[ AuthManager::ACTION_LOGIN, [] ],
|
||||
[ AuthManager::ACTION_CREATE, [] ],
|
||||
[ AuthManager::ACTION_LINK, [] ],
|
||||
[ AuthManager::ACTION_CHANGE, [] ],
|
||||
[ AuthManager::ACTION_REMOVE, [] ],
|
||||
];
|
||||
}
|
||||
|
||||
public function testBeginSecondaryAuthentication() {
|
||||
$provider = new EmailNotificationSecondaryAuthenticationProvider( [
|
||||
'sendConfirmationEmail' => true,
|
||||
] );
|
||||
$this->assertEquals( AuthenticationResponse::newAbstain(),
|
||||
$provider->beginSecondaryAuthentication( \User::newFromName( 'Foo' ), [] ) );
|
||||
}
|
||||
|
||||
public function testBeginSecondaryAccountCreation() {
|
||||
$authManager = new AuthManager( new \FauxRequest(), new \HashConfig() );
|
||||
|
||||
$creator = $this->getMock( 'User' );
|
||||
$userWithoutEmail = $this->getMock( 'User' );
|
||||
$userWithoutEmail->expects( $this->any() )->method( 'getEmail' )->willReturn( '' );
|
||||
$userWithoutEmail->expects( $this->never() )->method( 'sendConfirmationMail' );
|
||||
$userWithEmailError = $this->getMock( 'User' );
|
||||
$userWithEmailError->expects( $this->any() )->method( 'getEmail' )->willReturn( 'foo@bar.baz' );
|
||||
$userWithEmailError->expects( $this->any() )->method( 'sendConfirmationMail' )
|
||||
->willReturn( \Status::newFatal( 'fail' ) );
|
||||
$userExpectsConfirmation = $this->getMock( 'User' );
|
||||
$userExpectsConfirmation->expects( $this->any() )->method( 'getEmail' )
|
||||
->willReturn( 'foo@bar.baz' );
|
||||
$userExpectsConfirmation->expects( $this->once() )->method( 'sendConfirmationMail' )
|
||||
->willReturn( \Status::newGood() );
|
||||
$userNotExpectsConfirmation = $this->getMock( 'User' );
|
||||
$userNotExpectsConfirmation->expects( $this->any() )->method( 'getEmail' )
|
||||
->willReturn( 'foo@bar.baz' );
|
||||
$userNotExpectsConfirmation->expects( $this->never() )->method( 'sendConfirmationMail' );
|
||||
|
||||
$provider = new EmailNotificationSecondaryAuthenticationProvider( [
|
||||
'sendConfirmationEmail' => false,
|
||||
] );
|
||||
$provider->setManager( $authManager );
|
||||
$provider->beginSecondaryAccountCreation( $userNotExpectsConfirmation, $creator, [] );
|
||||
|
||||
$provider = new EmailNotificationSecondaryAuthenticationProvider( [
|
||||
'sendConfirmationEmail' => true,
|
||||
] );
|
||||
$provider->setManager( $authManager );
|
||||
$provider->beginSecondaryAccountCreation( $userWithoutEmail, $creator, [] );
|
||||
$provider->beginSecondaryAccountCreation( $userExpectsConfirmation, $creator, [] );
|
||||
|
||||
// test logging of email errors
|
||||
$logger = $this->getMockForAbstractClass( LoggerInterface::class );
|
||||
$logger->expects( $this->once() )->method( 'warning' );
|
||||
$provider->setLogger( $logger );
|
||||
$provider->beginSecondaryAccountCreation( $userWithEmailError, $creator, [] );
|
||||
|
||||
// test disable flag used by other providers
|
||||
$authManager->setAuthenticationSessionData( 'no-email', true );
|
||||
$provider->setManager( $authManager );
|
||||
$provider->beginSecondaryAccountCreation( $userNotExpectsConfirmation, $creator, [] );
|
||||
|
||||
}
|
||||
}
|
||||
161
tests/phpunit/includes/user/PasswordResetTest.php
Normal file
161
tests/phpunit/includes/user/PasswordResetTest.php
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\Auth\AuthManager;
|
||||
|
||||
/**
|
||||
* @group Database
|
||||
*/
|
||||
class PasswordResetTest extends PHPUnit_Framework_TestCase {
|
||||
/**
|
||||
* @dataProvider provideIsAllowed
|
||||
*/
|
||||
public function testIsAllowed( $passwordResetRoutes, $enableEmail,
|
||||
$allowsAuthenticationDataChange, $canEditPrivate, $canSeePassword,
|
||||
$userIsBlocked, $isAllowed, $isAllowedToDisplayPassword
|
||||
) {
|
||||
$config = new HashConfig( [
|
||||
'PasswordResetRoutes' => $passwordResetRoutes,
|
||||
'EnableEmail' => $enableEmail,
|
||||
] );
|
||||
|
||||
$authManager = $this->getMockBuilder( AuthManager::class )->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$authManager->expects( $this->any() )->method( 'allowsAuthenticationDataChange' )
|
||||
->willReturn( $allowsAuthenticationDataChange ? Status::newGood() : Status::newFatal( 'foo' ) );
|
||||
|
||||
$user = $this->getMock( User::class );
|
||||
$user->expects( $this->any() )->method( 'getName' )->willReturn( 'Foo' );
|
||||
$user->expects( $this->any() )->method( 'isBlocked' )->willReturn( $userIsBlocked );
|
||||
$user->expects( $this->any() )->method( 'isAllowed' )
|
||||
->will( $this->returnCallback( function ( $perm ) use ( $canEditPrivate, $canSeePassword ) {
|
||||
if ( $perm === 'editmyprivateinfo' ) {
|
||||
return $canEditPrivate;
|
||||
} elseif ( $perm === 'passwordreset' ) {
|
||||
return $canSeePassword;
|
||||
} else {
|
||||
$this->fail( 'Unexpected permission check' );
|
||||
}
|
||||
} ) );
|
||||
|
||||
$passwordReset = new PasswordReset( $config, $authManager );
|
||||
|
||||
$this->assertSame( $isAllowed, $passwordReset->isAllowed( $user )->isGood() );
|
||||
$this->assertSame( $isAllowedToDisplayPassword,
|
||||
$passwordReset->isAllowed( $user, true )->isGood() );
|
||||
}
|
||||
|
||||
public function provideIsAllowed() {
|
||||
return [
|
||||
[
|
||||
'passwordResetRoutes' => [],
|
||||
'enableEmail' => true,
|
||||
'allowsAuthenticationDataChange' => true,
|
||||
'canEditPrivate' => true,
|
||||
'canSeePassword' => true,
|
||||
'userIsBlocked' => false,
|
||||
'isAllowed' => false,
|
||||
'isAllowedToDisplayPassword' => false,
|
||||
],
|
||||
[
|
||||
'passwordResetRoutes' => [ 'username' => true ],
|
||||
'enableEmail' => false,
|
||||
'allowsAuthenticationDataChange' => true,
|
||||
'canEditPrivate' => true,
|
||||
'canSeePassword' => true,
|
||||
'userIsBlocked' => false,
|
||||
'isAllowed' => false,
|
||||
'isAllowedToDisplayPassword' => false,
|
||||
],
|
||||
[
|
||||
'passwordResetRoutes' => [ 'username' => true ],
|
||||
'enableEmail' => true,
|
||||
'allowsAuthenticationDataChange' => false,
|
||||
'canEditPrivate' => true,
|
||||
'canSeePassword' => true,
|
||||
'userIsBlocked' => false,
|
||||
'isAllowed' => false,
|
||||
'isAllowedToDisplayPassword' => false,
|
||||
],
|
||||
[
|
||||
'passwordResetRoutes' => [ 'username' => true ],
|
||||
'enableEmail' => true,
|
||||
'allowsAuthenticationDataChange' => true,
|
||||
'canEditPrivate' => false,
|
||||
'canSeePassword' => true,
|
||||
'userIsBlocked' => false,
|
||||
'isAllowed' => false,
|
||||
'isAllowedToDisplayPassword' => false,
|
||||
],
|
||||
[
|
||||
'passwordResetRoutes' => [ 'username' => true ],
|
||||
'enableEmail' => true,
|
||||
'allowsAuthenticationDataChange' => true,
|
||||
'canEditPrivate' => true,
|
||||
'canSeePassword' => true,
|
||||
'userIsBlocked' => true,
|
||||
'isAllowed' => false,
|
||||
'isAllowedToDisplayPassword' => false,
|
||||
],
|
||||
[
|
||||
'passwordResetRoutes' => [ 'username' => true ],
|
||||
'enableEmail' => true,
|
||||
'allowsAuthenticationDataChange' => true,
|
||||
'canEditPrivate' => true,
|
||||
'canSeePassword' => false,
|
||||
'userIsBlocked' => false,
|
||||
'isAllowed' => true,
|
||||
'isAllowedToDisplayPassword' => false,
|
||||
],
|
||||
[
|
||||
'passwordResetRoutes' => [ 'username' => true ],
|
||||
'enableEmail' => true,
|
||||
'allowsAuthenticationDataChange' => true,
|
||||
'canEditPrivate' => true,
|
||||
'canSeePassword' => true,
|
||||
'userIsBlocked' => false,
|
||||
'isAllowed' => true,
|
||||
'isAllowedToDisplayPassword' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testExecute_email() {
|
||||
$config = new HashConfig( [
|
||||
'PasswordResetRoutes' => [ 'username' => true, 'email' => true ],
|
||||
'EnableEmail' => true,
|
||||
] );
|
||||
|
||||
$authManager = $this->getMockBuilder( AuthManager::class )->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$authManager->expects( $this->any() )->method( 'allowsAuthenticationDataChange' )
|
||||
->willReturn( Status::newGood() );
|
||||
$authManager->expects( $this->exactly( 2 ) )->method( 'changeAuthenticationData' );
|
||||
|
||||
$request = new FauxRequest();
|
||||
$request->setIP( '1.2.3.4' );
|
||||
$performingUser = $this->getMock( User::class );
|
||||
$performingUser->expects( $this->any() )->method( 'getRequest' )->willReturn( $request );
|
||||
$performingUser->expects( $this->any() )->method( 'isAllowed' )->willReturn( true );
|
||||
|
||||
$targetUser1 = $this->getMock( User::class );
|
||||
$targetUser2 = $this->getMock( User::class );
|
||||
$targetUser1->expects( $this->any() )->method( 'getName' )->willReturn( 'User1' );
|
||||
$targetUser2->expects( $this->any() )->method( 'getName' )->willReturn( 'User2' );
|
||||
$targetUser1->expects( $this->any() )->method( 'getId' )->willReturn( 1 );
|
||||
$targetUser2->expects( $this->any() )->method( 'getId' )->willReturn( 2 );
|
||||
$targetUser1->expects( $this->any() )->method( 'getEmail' )->willReturn( 'foo@bar.baz' );
|
||||
$targetUser2->expects( $this->any() )->method( 'getEmail' )->willReturn( 'foo@bar.baz' );
|
||||
|
||||
$passwordReset = $this->getMockBuilder( PasswordReset::class )
|
||||
->setMethods( [ 'getUsersByEmail' ] )->setConstructorArgs( [ $config, $authManager ] )
|
||||
->getMock();
|
||||
$passwordReset->expects( $this->any() )->method( 'getUsersByEmail' )->with( 'foo@bar.baz' )
|
||||
->willReturn( [ $targetUser1, $targetUser2 ] );
|
||||
|
||||
$status = $passwordReset->isAllowed( $performingUser );
|
||||
$this->assertTrue( $status->isGood() );
|
||||
|
||||
$status = $passwordReset->execute( $performingUser, null, 'foo@bar.baz' );
|
||||
$this->assertTrue( $status->isGood() );
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue