jenkins-bot has submitted this change and it was merged.

Change subject: Update for AuthManager
......................................................................


Update for AuthManager

Creates a PrimaryAuthenticationProvider for actual authentication, and a
SecondaryAuthenticationProvider for checking renames and central locks
on all logins.

The loginwiki handshake is still done post-login via the existing hooks,
in no small part because trying to stick it into the login flow would
break every existing API client.

Bug: T110283
Change-Id: Ia60d92f9e08740e1f76a3744147b93a7636ae09e
---
M AntiSpoof/CentralAuthAntiSpoofHooks.php
M CentralAuth.php
M extension.json
M i18n/en.json
M i18n/qqq.json
M includes/CentralAuthHooks.php
M includes/CentralAuthPlugin.php
A includes/CentralAuthPreAuthManagerHooks.php
A includes/CentralAuthPrimaryAuthenticationProvider.php
A includes/CentralAuthSecondaryAuthenticationProvider.php
M includes/CentralAuthUser.php
M includes/CentralAuthUtils.php
M includes/LocalRenameJob/LocalUserMergeJob.php
M includes/specials/SpecialCentralLogin.php
M maintenance/createLocalAccount.php
M tests/phpunit/CentralAuthHooksUsingDatabaseTest.php
A tests/phpunit/CentralAuthPreAuthManagerHooksUsingDatabaseTest.php
17 files changed, 1,022 insertions(+), 340 deletions(-)

Approvals:
  Gergő Tisza: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/AntiSpoof/CentralAuthAntiSpoofHooks.php 
b/AntiSpoof/CentralAuthAntiSpoofHooks.php
index d4fa369..1a54f29 100644
--- a/AntiSpoof/CentralAuthAntiSpoofHooks.php
+++ b/AntiSpoof/CentralAuthAntiSpoofHooks.php
@@ -12,11 +12,26 @@
        public static function asAbortNewAccountHook( $user, &$message ) {
                global $wgAntiSpoofAccounts, $wgUser, $wgRequest;
 
-               if ( !$wgAntiSpoofAccounts ) {
+               $status = self::testNewAccount( $user, $wgUser, 
$wgAntiSpoofAccounts, $wgRequest->getCheck( 'wpIgnoreAntiSpoof' ) );
+               if ( !$status->isGood() ) {
+                       $message = Status::wrap( $status 
)->getMessage()->escaped();
+               }
+               return $status->isGood();
+       }
+
+       /**
+        * Test if an account is acceptable
+        * @param User $user
+        * @param User $creator
+        * @param bool $enable
+        * @param bool $override
+        * @return StatusValue
+        */
+       public static function testNewAccount( $user, $creator, $enable, 
$override ) {
+               if ( !$enable ) {
                        $mode = 'LOGGING ';
                        $active = false;
-               } elseif ( $wgRequest->getCheck( 'wpIgnoreAntiSpoof' ) &&
-                               $wgUser->isAllowed( 'override-antispoof' ) ) {
+               } elseif ( $override && $creator->isAllowed( 
'override-antispoof' ) ) {
                        $mode = 'OVERRIDE ';
                        $active = false;
                } else {
@@ -35,24 +50,25 @@
                                wfDebugLog( 'antispoof', "{$mode}CONFLICT new 
account '$name' [$normalized] spoofs " . implode( ',', $conflicts ) );
                                if ( $active ) {
                                        $numConflicts = count( $conflicts );
+
+                                       // This message pasting-together sucks.
                                        $message = wfMessage( 
'antispoof-conflict-top', $name )->numParams( $numConflicts )->escaped();
                                        $message .= '<ul>';
                                        foreach ( $conflicts as $simUser ) {
                                                $message .= '<li>' . wfMessage( 
'antispoof-conflict-item', $simUser )->escaped() . '</li>';
                                        }
                                        $message .= '</ul>' . wfMessage( 
'antispoof-conflict-bottom' )->escaped();
-                                       return false;
+                                       return StatusValue::newFatal( new 
RawMessage( '$1', Message::rawParam( $message ) ) );
                                }
                        }
                } else {
                        $error = $spoof->getError();
                        wfDebugLog( 'antispoof', "{$mode}ILLEGAL new account 
'$name' $error" );
                        if ( $active ) {
-                               $message = wfMessage( 'antispoof-name-illegal', 
$name, $error )->escaped();
-                               return false;
+                               return StatusValue::newFatal( 
'antispoof-name-illegal', $name, $error );
                        }
                }
-               return true;
+               return StatusValue::newGood();
        }
 
        /**
diff --git a/CentralAuth.php b/CentralAuth.php
index 6e3ffe6..8b6dde6 100644
--- a/CentralAuth.php
+++ b/CentralAuth.php
@@ -33,12 +33,6 @@
 $wgCentralAuthDatabase = 'centralauth';
 
 /**
- * If true, new account registrations will be registered globally if
- * the username hasn't been used elsewhere.
- */
-$wgCentralAuthAutoNew = false;
-
-/**
  * If true, existing unattached accounts will be automatically migrated
  * if possible at first login.
  *
diff --git a/extension.json b/extension.json
index e8b2e73..c059f78 100644
--- a/extension.json
+++ b/extension.json
@@ -6,7 +6,7 @@
        "license-name": "GPL-2.0",
        "type": "specialpage",
        "requires": {
-               "MediaWiki": ">= 1.28.0"
+               "MediaWiki": ">= 1.27.0"
        },
        "GroupPermissions": {
                "steward": {
@@ -170,8 +170,11 @@
                "SpecialSulRenameWarning": 
"includes/specials/SpecialSulRenameWarning.php",
                "CentralAuthUser": "includes/CentralAuthUser.php",
                "CentralAuthUtils": "includes/CentralAuthUtils.php",
+               "CentralAuthPrimaryAuthenticationProvider": 
"includes/CentralAuthPrimaryAuthenticationProvider.php",
+               "CentralAuthSecondaryAuthenticationProvider": 
"includes/CentralAuthSecondaryAuthenticationProvider.php",
                "CentralAuthPlugin": "includes/CentralAuthPlugin.php",
                "CentralAuthHooks": "includes/CentralAuthHooks.php",
+               "CentralAuthPreAuthManagerHooks": 
"includes/CentralAuthPreAuthManagerHooks.php",
                "CentralAuthSuppressUserJob": "includes/SuppressUserJob.php",
                "CentralAuthCreateLocalAccountJob": 
"includes/CreateLocalAccountJob.php",
                "WikiSet": "includes/WikiSet.php",
@@ -319,12 +322,8 @@
        },
        "callback": "CentralAuthHooks::onRegistration",
        "Hooks": {
-               "AuthPluginSetup": "CentralAuthHooks::onAuthPluginSetup",
-               "AddNewAccount": "CentralAuthHooks::onAddNewAccount",
+               "LocalUserCreated": "CentralAuthHooks::onLocalUserCreated",
                "GetPreferences": "CentralAuthHooks::onGetPreferences",
-               "AbortLogin": "CentralAuthHooks::onAbortLogin",
-               "AbortNewAccount": "CentralAuthHooks::onAbortNewAccount",
-               "AbortAutoAccount": "CentralAuthHooks::onAbortAutoAccount",
                "UserLoginComplete": "CentralAuthHooks::onUserLoginComplete",
                "UserLogout": "CentralAuthHooks::onUserLogout",
                "UserLogoutComplete": "CentralAuthHooks::onUserLogoutComplete",
@@ -373,7 +372,6 @@
        "config": {
                "@doc": "see CentralAuth.php",
                "CentralAuthDatabase": "centralauth",
-               "CentralAuthAutoNew": false,
                "CentralAuthAutoMigrate": false,
                "CentralAuthAutoMigrateNonGlobalAccounts": false,
                "CentralAuthStrict": false,
diff --git a/i18n/en.json b/i18n/en.json
index b760439..02904bd 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -495,6 +495,7 @@
        "sulrenamewarning-title" : "{{GENDER:$1|Your}} account has been 
renamed",
        "sulrenamewarning-renamed" : "{{GENDER:$2|Your}} account '''$1''' has 
been renamed to '''$2''' during automatic migration to the unified login system 
of {{int:Centralauth-groupname}}.",
        "sulrenamewarning-usenew" : "Please use {{GENDER:$1|your}} new username 
'''$1''' when logging in in the future.",
+       "sulrenamewarning-authmanager-ok-help": "Continue after displaying the 
rename notification.",
        "globalrenamequeue-request-reason-label": "Public log entry comment (if 
accepting)",
        "apihelp-centralauthtoken-description": "Fetch a centralauthtoken for 
making an authenticated request to an attached wiki.",
        "apihelp-centralauthtoken-example-1": "Fetch a centralauthtoken",
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 2d2476b..ce3ec17 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -511,6 +511,7 @@
        "sulrenamewarning-title": "[[Special:SulRenameWarning]] title\n\n* $1 
is the user's new name for GENDER",
        "sulrenamewarning-renamed": "Notice to user that their account has been 
renamed.\n\n* $1 is the user's old name\n* $2 is the user's new name",
        "sulrenamewarning-usenew": "Instructions to user to login with new name 
in the future\n\n* $1 is the user's new name",
+       "sulrenamewarning-authmanager-ok-help": "Description of the \"ok\" 
field when displaying the rename notification during AuthManager 
authentication.",
        "globalrenamequeue-request-reason-label": "Label for field that will be 
used as the log comment in the public rename log",
        "apihelp-centralauthtoken-description": 
"{{doc-apihelp-description|centralauthtoken}}",
        "apihelp-centralauthtoken-example-1": 
"{{doc-apihelp-example|centralauthtoken}}",
diff --git a/includes/CentralAuthHooks.php b/includes/CentralAuthHooks.php
index 76e3352..27f5708 100644
--- a/includes/CentralAuthHooks.php
+++ b/includes/CentralAuthHooks.php
@@ -9,7 +9,8 @@
                global $wgWikimediaJenkinsCI, $wgCentralAuthDatabase, $wgDBname,
                        $wgHooks, $wgSpecialPages, $wgSessionProviders,
                        $wgCentralIdLookupProvider, 
$wgOverrideCentralIdLookupProvider,
-                       $wgCentralAuthCheckSULMigration;
+                       $wgCentralAuthCheckSULMigration, $wgDisableAuthManager,
+                       $wgAuthManagerAutoConfig;
 
                // Override $wgCentralAuthDatabase for Wikimedia Jenkins.
                if( isset( $wgWikimediaJenkinsCI ) && $wgWikimediaJenkinsCI ) {
@@ -30,11 +31,35 @@
                        $wgCentralIdLookupProvider = 'CentralAuth';
                }
 
-               if ( $wgCentralAuthCheckSULMigration ) {
-                       // Install hidden special page for renamed users
-                       $wgSpecialPages['SulRenameWarning'] = 
'SpecialSulRenameWarning';
-                       $wgHooks['PostLoginRedirect'][] = 
'CentralAuthHooks::onPostLoginRedirect';
-                       $wgHooks['LoginUserMigrated'][] = 
'CentralAuthHooks::onLoginUserMigrated';
+               // AuthManager
+               if ( class_exists( MediaWiki\Auth\AuthManager::class ) && 
empty( $wgDisableAuthManager ) ) {
+                       $wgAuthManagerAutoConfig['primaryauth'] += [
+                               CentralAuthPrimaryAuthenticationProvider::class 
=> [
+                                       'class' => 
CentralAuthPrimaryAuthenticationProvider::class,
+                                       'sort' => 10, // After 
TemporaryPassword, before LocalPassword.
+                               ]
+                       ];
+                       $wgAuthManagerAutoConfig['secondaryauth'] += [
+                               
CentralAuthSecondaryAuthenticationProvider::class => [
+                                       'class' => 
CentralAuthSecondaryAuthenticationProvider::class,
+                                       'sort' => 0, // non-UI secondaries 
should run early
+                               ]
+                       ];
+               } else {
+                       $wgHooks['AuthPluginSetup'][] = 
'CentralAuthPreAuthManagerHooks::onAuthPluginSetup';
+                       $wgHooks['AbortLogin'][] = 
'CentralAuthPreAuthManagerHooks::onAbortLogin';
+                       $wgHooks['AbortNewAccount'][] = 
'CentralAuthPreAuthManagerHooks::onAbortNewAccount';
+                       $wgHooks['AbortAutoAccount'][] = 
'CentralAuthPreAuthManagerHooks::onAbortAutoAccount';
+                       $wgHooks['UserLoginComplete'][] = 
'CentralAuthPreAuthManagerHooks::onUserLoginComplete';
+                       $wgHooks['UserLogout'][] = 
'CentralAuthPreAuthManagerHooks::onUserLogout';
+                       $wgHooks['SpecialPage_initList'][] = 
'CentralAuthPreAuthManagerHooks::onSpecialPage_initList';
+
+                       if ( $wgCentralAuthCheckSULMigration ) {
+                               // Install hidden special page for renamed users
+                               $wgSpecialPages['SulRenameWarning'] = 
'SpecialSulRenameWarning';
+                               $wgHooks['PostLoginRedirect'][] = 
'CentralAuthHooks::onPostLoginRedirect';
+                               $wgHooks['LoginUserMigrated'][] = 
'CentralAuthHooks::onLoginUserMigrated';
+                       }
                }
        }
 
@@ -48,7 +73,7 @@
                global $wgAutoloadClasses, $wgExtensionCredits, $wgHooks;
                global $wgSpecialPages, $wgResourceModules;
                global $wgCentralAuthEnableGlobalRenameRequest;
-               global $wgCentralAuthCheckSULMigration;
+               global $wgDisableAuthManager;
                $caBase = __DIR__ . '/..';
 
                if ( class_exists( 'RenameuserSQL' ) ) {
@@ -79,9 +104,11 @@
                        $wgAutoloadClasses['CentralAuthAntiSpoofHooks'] =
                                
"$caBase/AntiSpoof/CentralAuthAntiSpoofHooks.php";
 
-                       $wgHooks['AbortNewAccount'][] =
-                               
'CentralAuthAntiSpoofHooks::asAbortNewAccountHook';
-                       $wgHooks['AddNewAccount'][] =
+                       if ( !class_exists( MediaWiki\Auth\AuthManager::class ) 
|| !empty( $wgDisableAuthManager ) ) {
+                               $wgHooks['AbortNewAccount'][] =
+                                       
'CentralAuthAntiSpoofHooks::asAbortNewAccountHook';
+                       }
+                       $wgHooks['LocalUserCreated'][] =
                                
'CentralAuthAntiSpoofHooks::asAddNewAccountHook';
                        $wgHooks['RenameUserComplete'][] =
                                
'CentralAuthAntiSpoofHooks::asAddRenameUserHook';
@@ -227,25 +254,12 @@
         * @return bool
         */
        public static function onSpecialPage_initList( &$list ) {
-               global $wgCentralAuthEnableGlobalRenameRequest, 
$wgCentralAuthCheckSULMigration;
+               global $wgCentralAuthEnableGlobalRenameRequest;
                if ( $wgCentralAuthEnableGlobalRenameRequest ) {
                        $list['GlobalRenameRequest'] = 
'SpecialGlobalRenameRequest';
                        $list['GlobalRenameQueue'] = 'SpecialGlobalRenameQueue';
                }
 
-               if ( $wgCentralAuthCheckSULMigration ) {
-                       $list['SulRenameWarning'] = 'SpecialSulRenameWarning';
-               }
-
-               return true;
-       }
-
-       /**
-        * @param $auth
-        * @return bool
-        */
-       static function onAuthPluginSetup( &$auth ) {
-               $auth = new CentralAuthPlugin;
                return true;
        }
 
@@ -253,12 +267,24 @@
         * Make sure migration information in localuser table is populated
         * on local account creation
         * @param $user User
-        * @param $byEmail bool
+        * @param $autocreated bool
         * @return bool
         */
-       static function onAddNewAccount( $user, $byEmail ) {
-               $central = CentralAuthUser::getMasterInstance( $user );
-               $central->addLocalName( wfWikiID() );
+       static function onLocalUserCreated( $user, $autocreated ) {
+               $centralUser = CentralAuthUser::getMasterInstance( $user );
+
+               // If some other AuthManager PrimaryAuthenticationProvider is 
creating
+               // the user, we should still create a central user for them so
+               // CentralAuthIdLookup can have an ID for this new user right 
away.
+               if ( !$centralUser->exists() && !$centralUser->listUnattached() 
) {
+                       if ( $centralUser->register( null, $user->getEmail() ) 
) {
+                               $centralUser->attach( wfWikiID(), 'new' );
+                               CentralAuthUtils::scheduleCreationJobs( 
$centralUser );
+                       }
+               }
+
+               $centralUser->addLocalName( wfWikiID() );
+
                return true;
        }
 
@@ -323,144 +349,6 @@
        }
 
        /**
-        * @param $user User
-        * @param $abortError
-        * @return bool
-        */
-       static function onAbortNewAccount( User $user, &$abortError ) {
-               global $wgCentralAuthPreventUnattached;
-               global $wgCentralAuthEnableGlobalRenameRequest;
-
-               $centralUser = CentralAuthUser::getMasterInstance( $user );
-               if ( $centralUser->exists() || 
$centralUser->renameInProgressOn( wfWikiID() ) ) {
-                       $abortError = wfMessage( 'centralauth-account-exists' 
)->text();
-                       return false;
-               }
-
-               if ( $wgCentralAuthPreventUnattached && 
$centralUser->listUnattached() ) {
-                       // If no global account exists and there are unattached 
accounts,
-                       // don't let a new unattached account get created. 
We'll pretend
-                       // that the name is already taken, because someone will 
eventually
-                       // get it. See bug 67901.
-                       $abortError = wfMessage( 
'centralauth-account-unattached-exists' )->text();
-                       return false;
-               }
-
-               // Block account creation if name is a pending rename request
-               if ( $wgCentralAuthEnableGlobalRenameRequest &&
-                       GlobalRenameRequest::nameHasPendingRequest( 
$user->getName() )
-               ) {
-                       $abortError = wfMessage( 
'centralauth-account-rename-exists' )->text();
-                       return false;
-               }
-
-               return true;
-       }
-
-       /**
-        * Prevent accounts from being autocreated if a rename is in progress
-        * @param User $user
-        * @param $msg
-        * @return bool
-        */
-       static function onAbortAutoAccount( User $user, &$msg ) {
-               $caUser = CentralAuthUser::getInstance( $user );
-               if ( $caUser->renameInProgress() ) {
-                       $msg = 'centralauth-rename-abortlogin';
-                       return false;
-               }
-
-               return true;
-       }
-
-       /**
-        * @param User $user
-        * @param string $pass
-        * @param integer &$retval
-        * @param string &$msg
-        * @return bool
-        * @throws Exception
-        */
-       static function onAbortLogin( User $user, $pass, &$retval, &$msg ) {
-               $centralUser = CentralAuthUser::getMasterInstance( $user );
-
-               // Since logins are rare, check the actual DB
-               $rename = $centralUser->renameInProgressOn( wfWikiID() );
-               if ( $rename ) {
-                       // This wiki specifically has a rename in progress, so 
always abort
-                       $retval = LoginForm::NOT_EXISTS;
-                       $msg = 'centralauth-rename-abortlogin';
-                       return false;
-               }
-
-               // Now check if the user is the target of a rename anywhere
-               $rename = $centralUser->renameInProgress();
-               if ( $rename ) {
-                       // It's possible a user is being renamed but someone 
else with
-                       // an unattached account is trying to login. Since 
we've already
-                       // moved everything over to the new account name, we 
only need
-                       // to check one username.
-                       $newCAUser = $rename[1] === $user->getName()
-                               ? $centralUser
-                               : CentralAuthUser::getInstanceByName( 
$rename[0] );
-
-                       if ( $newCAUser->isAttached() ) {
-                               // If there is an account using that name that 
exists on this wiki
-                               // reject the login.
-                               $retval = LoginForm::NOT_EXISTS;
-                               // This is an icky hack so the message is 
->parse()d and
-                               // gets the username as a parameter
-                               $msg = 'centralauth-rename-abortlogin';
-                               return false;
-                       }
-               }
-
-               switch ( (string)$centralUser->canAuthenticate() ) {
-                       case '1': // boolean true
-                               return true;
-
-                       case 'no user':
-                               // If they're local, we still want to let them 
log in. And if they
-                               // don't exist, this hook wouldn't have even 
been called.
-                               return true;
-
-                       case 'locked':
-                               $msg = 'centralauth-login-error-locked';
-                               $retval = LoginForm::USER_BLOCKED;
-                               return false;
-
-                       default:
-                               throw new Exception( "Unexpected result from 
CentralAuthUser::canAuthenticate()" );
-               }
-       }
-
-       /**
-        * Inform a user that their username was renamed as part of SUL
-        * Finalization, if their previous username doesn't exist any more 
(winner
-        * was renamed).
-        * @param User $user
-        * @param string &$msg return error key, or return an array with key 
and params
-        * @return bool
-        */
-       public static function onLoginUserMigrated( $user, &$msg ) {
-               global $wgCentralAuthCheckSULMigration;
-               if ( $wgCentralAuthCheckSULMigration ) {
-                       $centralUser = CentralAuthUser::getInstance( $user );
-                       if ( $user->getID() === 0 && !$centralUser->exists() ) {
-                               // If the local and global accounts don't exist,
-                               // otherwise wgAuth will handle those.
-                               $testName = $user->getName() . '~' . wfWikiID();
-                               $test = CentralAuthUser::getInstanceByName( 
$testName );
-                               if ( $test->exists() && $test->isAttached() ) {
-                                       $msg = array( 
'centralauth-abortlogin-renamed', $testName );
-                                       return false;
-                               }
-                       }
-               }
-               return true;
-       }
-
-       /**
         * Show a nicer error when the user account does not exist on the local 
wiki, but
         * does exist globally
         * @param $users User[]
@@ -493,17 +381,6 @@
         */
        static function onUserLoginComplete( &$user, &$inject_html ) {
                global $wgCentralAuthCookies;
-               global $wgCentralAuthCheckSULMigration;
-
-               if ( $wgCentralAuthCheckSULMigration &&
-                       isset( $user->sulRenamed ) &&
-                       $user->sulRenamed === true
-               ) {
-                       // Add a flag in the user's session to track that they 
authenticated
-                       // with a pre-migration username.
-                       $request = $user->getRequest();
-                       $request->setSessionData( 'CentralAuthForcedRename', 
true );
-               }
 
                if ( !$wgCentralAuthCookies ) {
                        // Use local sessions only.
@@ -713,9 +590,6 @@
                                $centralUser->resetAuthToken();
                        }
                } );
-
-               // Clean up any possible forced rename markers
-               $user->getRequest()->setSessionData( 'CentralAuthForcedRename', 
null );
 
                return true;
        }
@@ -1413,35 +1287,6 @@
                        'setglobalaccountstatus' => 'setglobalaccountstatus',
                        'deleteglobalaccount' => 'deleteglobalaccount',
                );
-               return true;
-       }
-
-       /**
-        * Handler for PostLoginRedirect
-        * @param string $returnTo The page to return to
-        * @param array $returnToQuery Url parameters
-        * @param string $type Type of login redirect
-        * @return bool
-        */
-       public static function onPostLoginRedirect(
-               &$returnTo, &$returnToQuery, &$type
-       ) {
-               global $wgCentralAuthCheckSULMigration, $wgUser;
-               if ( $wgCentralAuthCheckSULMigration &&
-                       $wgUser->getRequest()->getSessionData( 
'CentralAuthForcedRename' ) === true &&
-                       ( $type == 'success' || $type == 'successredirect' )
-               ) {
-                       wfDebugLog( 'SUL', 'Redirecting user to 
Special:SulRenameWarning' );
-                       // Store current redirect target in session so we can 
provide a link
-                       // later.
-                       $wgUser->getRequest()->setSessionData( 
'SulRenameWarning', array(
-                               'returnTo' => $returnTo,
-                               'returnToQuery' => $returnToQuery,
-                       ) );
-                       $returnTo = SpecialPageFactory::getLocalNameFor( 
'Special:SulRenameWarning' );
-                       $returnToQuery = array();
-                       return false;
-               }
                return true;
        }
 
diff --git a/includes/CentralAuthPlugin.php b/includes/CentralAuthPlugin.php
index 37dff76..7f9e9e0 100644
--- a/includes/CentralAuthPlugin.php
+++ b/includes/CentralAuthPlugin.php
@@ -7,6 +7,9 @@
  *   central password or settings.
  */
 
+/**
+ * @deprecated
+ */
 class CentralAuthPlugin extends AuthPlugin {
 
        /**
@@ -297,25 +300,22 @@
         * @return bool
         */
        public function addUser( $user, $password, $email = '', $realname = '' 
) {
-               global $wgCentralAuthAutoNew;
-               if ( $wgCentralAuthAutoNew ) {
-                       $central = CentralAuthUser::getMasterInstance( $user );
-                       if ( !$central->exists() && !$central->listUnattached() 
) {
-                               // Username is unused; set up as a global 
account
-                               // @fixme is this even vaguely reliable? pah
-                               $ok = $central->register( $password, $email );
-                               if ( $ok ) {
-                                       $central->attach( wfWikiID(), 'new' );
-                                       $this->autoCreateAccounts( $central );
-                               } else {
-                                       return false;
-                               }
+               $central = CentralAuthUser::getMasterInstance( $user );
+               if ( !$central->exists() && !$central->listUnattached() ) {
+                       // Username is unused; set up as a global account
+                       // @fixme is this even vaguely reliable? pah
+                       $ok = $central->register( $password, $email );
+                       if ( $ok ) {
+                               $central->attach( wfWikiID(), 'new' );
+                               CentralAuthUtils::scheduleCreationJobs( 
$central );
+                       } else {
+                               return false;
                        }
-                       // Note: If $wgCentralAuthPreventUnattached is enabled,
-                       // accounts where a global does not exist, but there are
-                       // unattached accounts will have been denied creation in
-                       // the AbortNewAccount hook.
                }
+               // Note: If $wgCentralAuthPreventUnattached is enabled,
+               // accounts where a global does not exist, but there are
+               // unattached accounts will have been denied creation in
+               // the AbortNewAccount hook.
                return true;
        }
 
@@ -367,27 +367,4 @@
                return CentralAuthUser::getInstance( $user );
        }
 
-       /**
-        * Sets up jobs to create and attach a local account for the given user 
on every wiki listed in
-        * $wgCentralAuthAutoCreateWikis.
-        * @param CentralAuthUser $centralUser
-        */
-       private function autoCreateAccounts( CentralAuthUser $centralUser ) {
-               global $wgCentralAuthAutoCreateWikis;
-
-               $name = $centralUser->getName();
-               $thisWiki = wfWikiID();
-               $session = RequestContext::getMain()->exportSession();
-               foreach ( $wgCentralAuthAutoCreateWikis as $wiki ) {
-                       if ( $wiki === $thisWiki ) {
-                               continue;
-                       }
-                       $job = Job::factory(
-                               'CentralAuthCreateLocalAccountJob',
-                               Title::makeTitleSafe( NS_USER, $name ),
-                               array( 'name' => $name, 'from' => $thisWiki, 
'session' => $session )
-                       );
-                       JobQueueGroup::singleton( $wiki )->push( $job );
-               }
-       }
 }
diff --git a/includes/CentralAuthPreAuthManagerHooks.php 
b/includes/CentralAuthPreAuthManagerHooks.php
new file mode 100644
index 0000000..ccc8de5
--- /dev/null
+++ b/includes/CentralAuthPreAuthManagerHooks.php
@@ -0,0 +1,235 @@
+<?php
+
+/**
+ * This holds hooks needed for pre-AuthManager versions of MediaWiki (or when
+ * $wgDisableAuthManager is set). Once we drop support for those versions, this
+ * may be removed.
+ * @deprecated
+ */
+class CentralAuthPreAuthManagerHooks {
+
+       /**
+        * This hook is used in cases where SpecialPageFactory::getPageList() 
is called before
+        * $wgExtensionFunctions are run, which happens when E:ShortUrl is 
installed.
+        *
+        * @param array $list
+        * @return bool
+        */
+       public static function onSpecialPage_initList( &$list ) {
+               global $wgCentralAuthCheckSULMigration;
+
+               if ( $wgCentralAuthCheckSULMigration ) {
+                       $list['SulRenameWarning'] = 'SpecialSulRenameWarning';
+               }
+
+               return true;
+       }
+
+       /**
+        * @param $auth
+        * @return bool
+        */
+       static function onAuthPluginSetup( &$auth ) {
+               $auth = new CentralAuthPlugin;
+               return true;
+       }
+
+       /**
+        * @param $user User
+        * @param $abortError
+        * @return bool
+        */
+       static function onAbortNewAccount( User $user, &$abortError ) {
+               global $wgCentralAuthPreventUnattached;
+               global $wgCentralAuthEnableGlobalRenameRequest;
+
+               $centralUser = CentralAuthUser::getMasterInstance( $user );
+               if ( $centralUser->exists() || 
$centralUser->renameInProgressOn( wfWikiID() ) ) {
+                       $abortError = wfMessage( 'centralauth-account-exists' 
)->text();
+                       return false;
+               }
+
+               if ( $wgCentralAuthPreventUnattached && 
$centralUser->listUnattached() ) {
+                       // If no global account exists and there are unattached 
accounts,
+                       // don't let a new unattached account get created. 
We'll pretend
+                       // that the name is already taken, because someone will 
eventually
+                       // get it. See bug 67901.
+                       $abortError = wfMessage( 
'centralauth-account-unattached-exists' )->text();
+                       return false;
+               }
+
+               // Block account creation if name is a pending rename request
+               if ( $wgCentralAuthEnableGlobalRenameRequest &&
+                       GlobalRenameRequest::nameHasPendingRequest( 
$user->getName() )
+               ) {
+                       $abortError = wfMessage( 
'centralauth-account-rename-exists' )->text();
+                       return false;
+               }
+
+               return true;
+       }
+
+       /**
+        * Prevent accounts from being autocreated if a rename is in progress
+        * @param User $user
+        * @param $msg
+        * @return bool
+        */
+       static function onAbortAutoAccount( User $user, &$msg ) {
+               $caUser = CentralAuthUser::getInstance( $user );
+               if ( $caUser->renameInProgress() ) {
+                       $msg = 'centralauth-rename-abortlogin';
+                       return false;
+               }
+
+               return true;
+       }
+
+       /**
+        * @param User $user
+        * @param string $pass
+        * @param integer &$retval
+        * @param string &$msg
+        * @return bool
+        * @throws Exception
+        */
+       static function onAbortLogin( User $user, $pass, &$retval, &$msg ) {
+               $centralUser = CentralAuthUser::getMasterInstance( $user );
+
+               // Since logins are rare, check the actual DB
+               $rename = $centralUser->renameInProgressOn( wfWikiID() );
+               if ( $rename ) {
+                       // This wiki specifically has a rename in progress, so 
always abort
+                       $retval = LoginForm::NOT_EXISTS;
+                       $msg = 'centralauth-rename-abortlogin';
+                       return false;
+               }
+
+               // Now check if the user is the target of a rename anywhere
+               $rename = $centralUser->renameInProgress();
+               if ( $rename ) {
+                       // It's possible a user is being renamed but someone 
else with
+                       // an unattached account is trying to login. Since 
we've already
+                       // moved everything over to the new account name, we 
only need
+                       // to check one username.
+                       $newCAUser = $rename[1] === $user->getName()
+                               ? $centralUser
+                               : CentralAuthUser::getInstanceByName( 
$rename[0] );
+
+                       if ( $newCAUser->isAttached() ) {
+                               // If there is an account using that name that 
exists on this wiki
+                               // reject the login.
+                               $retval = LoginForm::NOT_EXISTS;
+                               // This is an icky hack so the message is 
->parse()d and
+                               // gets the username as a parameter
+                               $msg = 'centralauth-rename-abortlogin';
+                               return false;
+                       }
+               }
+
+               switch ( (string)$centralUser->canAuthenticate() ) {
+                       case '1': // boolean true
+                               return true;
+
+                       case 'no user':
+                               // If they're local, we still want to let them 
log in. And if they
+                               // don't exist, this hook wouldn't have even 
been called.
+                               return true;
+
+                       case 'locked':
+                               $msg = 'centralauth-login-error-locked';
+                               $retval = LoginForm::USER_BLOCKED;
+                               return false;
+
+                       default:
+                               throw new Exception( "Unexpected result from 
CentralAuthUser::canAuthenticate()" );
+               }
+       }
+
+       /**
+        * Inform a user that their username was renamed as part of SUL
+        * Finalization, if their previous username doesn't exist any more 
(winner
+        * was renamed).
+        * @param User $user
+        * @param string &$msg return error key, or return an array with key 
and params
+        * @return bool
+        */
+       public static function onLoginUserMigrated( $user, &$msg ) {
+               global $wgCentralAuthCheckSULMigration;
+               if ( $wgCentralAuthCheckSULMigration ) {
+                       $centralUser = CentralAuthUser::getInstance( $user );
+                       if ( $user->getID() === 0 && !$centralUser->exists() ) {
+                               // If the local and global accounts don't exist,
+                               // otherwise wgAuth will handle those.
+                               $testName = $user->getName() . '~' . wfWikiID();
+                               $test = CentralAuthUser::getInstanceByName( 
$testName );
+                               if ( $test->exists() && $test->isAttached() ) {
+                                       $msg = array( 
'centralauth-abortlogin-renamed', $testName );
+                                       return false;
+                               }
+                       }
+               }
+               return true;
+       }
+
+       /**
+        * @param $user User
+        * @param $inject_html string
+        * @return bool
+        */
+       static function onUserLoginComplete( &$user, &$inject_html ) {
+               global $wgCentralAuthCheckSULMigration;
+
+               if ( $wgCentralAuthCheckSULMigration &&
+                       isset( $user->sulRenamed ) &&
+                       $user->sulRenamed === true
+               ) {
+                       // Add a flag in the user's session to track that they 
authenticated
+                       // with a pre-migration username.
+                       $request = $user->getRequest();
+                       $request->setSessionData( 'CentralAuthForcedRename', 
true );
+               }
+
+               return true;
+       }
+
+       /**
+        * @param $user User
+        * @return bool
+        */
+       static function onUserLogout( &$user ) {
+               // Clean up any possible forced rename markers
+               $user->getRequest()->setSessionData( 'CentralAuthForcedRename', 
null );
+
+               return true;
+       }
+
+       /**
+        * Handler for PostLoginRedirect
+        * @param string $returnTo The page to return to
+        * @param array $returnToQuery Url parameters
+        * @param string $type Type of login redirect
+        * @return bool
+        */
+       public static function onPostLoginRedirect(
+               &$returnTo, &$returnToQuery, &$type
+       ) {
+               global $wgCentralAuthCheckSULMigration, $wgUser;
+               if ( $wgCentralAuthCheckSULMigration &&
+                       $wgUser->getRequest()->getSessionData( 
'CentralAuthForcedRename' ) === true &&
+                       ( $type == 'success' || $type == 'successredirect' )
+               ) {
+                       wfDebugLog( 'SUL', 'Redirecting user to 
Special:SulRenameWarning' );
+                       // Store current redirect target in session so we can 
provide a link
+                       // later.
+                       $wgUser->getRequest()->setSessionData( 
'SulRenameWarning', array(
+                               'returnTo' => $returnTo,
+                               'returnToQuery' => $returnToQuery,
+                       ) );
+                       $returnTo = SpecialPageFactory::getLocalNameFor( 
'Special:SulRenameWarning' );
+                       $returnToQuery = array();
+                       return false;
+               }
+               return true;
+       }
+}
diff --git a/includes/CentralAuthPrimaryAuthenticationProvider.php 
b/includes/CentralAuthPrimaryAuthenticationProvider.php
new file mode 100644
index 0000000..4dbaa90
--- /dev/null
+++ b/includes/CentralAuthPrimaryAuthenticationProvider.php
@@ -0,0 +1,463 @@
+<?php
+/**
+ * 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 Auth
+ */
+
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Auth\AbstractPasswordPrimaryAuthenticationProvider;
+use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\Auth\AuthenticationResponse;
+use MediaWiki\Auth\PasswordAuthenticationRequest;
+use MediaWiki\Auth\ButtonAuthenticationRequest;
+
+/**
+ * A primary authentication provider that uses the CentralAuth password.
+ */
+class CentralAuthPrimaryAuthenticationProvider
+       extends AbstractPasswordPrimaryAuthenticationProvider
+{
+       /** @var bool Whether to check for force-renamed users on login */
+       protected $checkSULMigration = null;
+
+       /** @var bool Whether to auto-migrate non-merged accounts on login */
+       protected $autoMigrate = null;
+
+       /** @var bool Whether to auto-migrate non-global accounts on login */
+       protected $autoMigrateNonGlobalAccounts = null;
+
+       /** @var bool Whether to prevent a new account from being created if the
+        * account exists on other wikis in the SUL group. */
+       protected $preventUnattached = null;
+
+       /** @var bool Whether to check for spoofed user names */
+       protected $antiSpoofAccounts = null;
+
+       /**
+        * @param array $params Settings. All are optional, defaulting to the
+        *  similarly-named $wgCentralAuth* globals.
+        *  - checkSULMigration: If true, check if the user was force-renamed 
for
+        *    SUL unification on login.
+        *  - autoMigrate: If true, attempt to auto-migrate local accounts on 
other
+        *    wikis when logging in.
+        *  - autoMigrateNonGlobalAccounts: If true, attempt to auto-migrate
+        *    non-global accounts on login.
+        *  - preventUnattached: Whether to prevent new unattached accounts from
+        *    being created.
+        *  - antiSpoofAccounts: Whether to anti-spoof new accounts. Ignored if 
the
+        *    AntiSpoof extension isn't installed or the extension is outdated.
+        */
+       public function __construct( $params = [] ) {
+               global $wgCentralAuthCheckSULMigration, 
$wgCentralAuthAutoMigrate,
+                       $wgCentralAuthAutoMigrateNonGlobalAccounts, 
$wgCentralAuthPreventUnattached,
+                       $wgCentralAuthStrict, $wgAntiSpoofAccounts;
+
+               $params += [
+                       'checkSULMigration' => $wgCentralAuthCheckSULMigration,
+                       'autoMigrate' => $wgCentralAuthAutoMigrate,
+                       'autoMigrateNonGlobalAccounts' => 
$wgCentralAuthAutoMigrateNonGlobalAccounts,
+                       'preventUnattached' => $wgCentralAuthPreventUnattached,
+                       'antiSpoofAccounts' => $wgAntiSpoofAccounts,
+                       'authoritative' => $wgCentralAuthStrict,
+               ];
+
+               parent::__construct( $params );
+
+               $this->checkSULMigration = (bool)$params['checkSULMigration'];
+               $this->autoMigrate = (bool)$params['autoMigrate'];
+               $this->autoMigrateNonGlobalAccounts = 
(bool)$params['autoMigrateNonGlobalAccounts'];
+               $this->preventUnattached = (bool)$params['preventUnattached'];
+               $this->antiSpoofAccounts = (bool)$params['antiSpoofAccounts'];
+       }
+
+       public function getAuthenticationRequests( $action, array $options ) {
+               $ret = parent::getAuthenticationRequests( $action, $options );
+
+               if ( $this->antiSpoofAccounts && $action === 
AuthManager::ACTION_CREATE &&
+                       class_exists( 'AntiSpoofAuthenticationRequest' )
+               ) {
+                       $user = User::newFromName( $options['username'] ) ?: 
new User();
+                       if ( $user->isAllowed( 'override-antispoof' ) ) {
+                               $ret[] = new AntiSpoofAuthenticationRequest();
+                       }
+               }
+
+               return $ret;
+       }
+
+       public function beginPrimaryAuthentication( array $reqs ) {
+               $req = AuthenticationRequest::getRequestByClass( $reqs, 
PasswordAuthenticationRequest::class );
+               if ( !$req ) {
+                       return AuthenticationResponse::newAbstain();
+               }
+
+               if ( $req->username === null || $req->password === null ) {
+                       return AuthenticationResponse::newAbstain();
+               }
+
+               $username = User::getCanonicalName( $req->username, 'usable' );
+               if ( $username === false ) {
+                       return AuthenticationResponse::newAbstain();
+               }
+
+               $status = $this->checkPasswordValidity( $username, 
$req->password );
+               if ( !$status->isOk() ) {
+                       // Fatal, can't log in
+                       return AuthenticationResponse::newFail( 
$status->getMessage() );
+               }
+
+               $pass = false;
+
+               // First, check normal login
+               $centralUser = CentralAuthUser::getMasterInstanceByName( 
$username );
+               $pass = $centralUser->authenticate( $req->password ) === 'ok';
+
+               // See if it's a user affected by a rename, if applicable.
+               if ( !$pass && $this->checkSULMigration ) {
+                       $renamedUsername = User::getCanonicalName(
+                               $req->username . '~' . str_replace( '_', '-', 
wfWikiID() )
+                       );
+                       if ( $renamedUsername !== false ) {
+                               $renamed = 
CentralAuthUser::getMasterInstanceByName( $renamedUsername );
+                               if ( $renamed->getId() ) {
+                                       $this->logger->debug(
+                                               'CentralAuthMigration: Checking 
for migration of "{oldname}" to "{newname}"',
+                                               [
+                                                       'oldname' => $username,
+                                                       'newname' => 
$renamedUsername,
+                                               ]
+                                       );
+                                       
RequestContext::getMain()->getStats()->increment( 'centralauth.migration.check' 
);
+
+                                       if ( $renamed->authenticate( 
$req->password ) === 'ok' ) {
+                                               // Don't do any of the checks 
below if we checked for a
+                                               // renamed user. But do notify, 
unless this is coming
+                                               // through the API action=login 
where it's better to
+                                               // preserve BC and just let it 
go.
+                                               if ( defined( 'MW_API' ) &&
+                                                       
$this->manager->getRequest()->getVal( 'action' ) === 'login'
+                                               ) {
+                                                       return 
AuthenticationResponse::newPass( $renamedUsername );
+                                               }
+                                               
$this->manager->setAuthenticationSessionData( 'CA-renamed-from', $username );
+                                               
$this->manager->setAuthenticationSessionData( 'CA-renamed-to', $renamedUsername 
);
+                                               return 
AuthenticationResponse::newUI(
+                                                       [
+                                                               new 
ButtonAuthenticationRequest(
+                                                                       
'caRenameOk', wfMessage( 'ok' ), wfMessage( 
'sulrenamewarning-authmanager-ok-help' )
+                                                               )
+                                                       ],
+                                                       wfMessage( 
'sulrenamewarning-renamed', $username, $renamedUsername )
+                                               );
+                                       }
+                               }
+                       }
+               }
+
+               // If we don't have a central account, see if all local 
accounts match
+               // the password and can be globalized. (bug T72392)
+               if ( !$centralUser->exists() ) {
+                       $this->logger->debug( 'no global account for 
"{username}"', [ 'username' => $username ] );
+                       if ( $this->autoMigrateNonGlobalAccounts ) {
+                               $ok = $centralUser->storeAndMigrate( [ 
$req->password ], /* $sendToRC = */ true, /* $safe = */ true, /* $checkHome = 
*/ true );
+                               if ( $ok ) {
+                                       $this->logger->debug( 
'wgCentralAuthAutoMigrateNonGlobalAccounts successful in creating a global 
account for "{username}"', [
+                                               'username' => $username
+                                       ] );
+                                       return AuthenticationResponse::newPass( 
$username );
+                               }
+                       }
+                       return $this->failResponse( $req );
+               }
+
+               if ( $pass && $this->autoMigrate ) {
+                       // If the user passed in the global password, we can 
identify
+                       // any remaining local accounts with a matching password
+                       // and migrate them in transparently.
+                       //
+                       // That may or may not include the current wiki.
+                       //
+                       $this->logger->debug( 'attempting 
wgCentralAuthAutoMigrate for "{username}"', [
+                               'username' => $username,
+                       ] );
+                       $centralUser->attemptPasswordMigration( $req->password 
);
+               }
+
+               if ( !$centralUser->isAttached() ) {
+                       $local = User::newFromName( $username );
+                       if ( $local && $local->getId() ) {
+                               // An unattached local account; central 
authentication can't
+                               // be used until this account has been 
transferred.
+                               // $wgCentralAuthStrict will determine if local 
login is allowed.
+                               $this->logger->debug( 'unattached account for 
"{username}"', [
+                                       'username' => $username,
+                               ] );
+                               return $this->failResponse( $req );
+                       }
+               }
+
+               if ( $pass ) {
+                       return AuthenticationResponse::newPass( $username );
+               } else {
+                       // We know the central user is attached at this point, 
so never
+                       // fall back to other password providers.
+                       return AuthenticationResponse::newFail( wfMessage( 
'wrongpassword' ) );
+               }
+       }
+
+       public function continuePrimaryAuthentication( array $reqs ) {
+               $username = $this->manager->getAuthenticationSessionData( 
'CA-renamed-from' );
+               $renamedUsername = 
$this->manager->getAuthenticationSessionData( 'CA-renamed-to' );
+               if ( $username === null || $renamedUsername === null ) {
+                       // What?
+                       $this->logger->debug( 'Missing "CA-renamed-from" or 
"CA-renamed-to" in session data' );
+                       return AuthenticationResponse::newFail( wfMessage( 
'authmanager-authn-not-in-progress' ) );
+               }
+
+               $req = ButtonAuthenticationRequest::getRequestByName( $reqs, 
'caRenameOk' );
+               if ( $req ) {
+                       return AuthenticationResponse::newPass( 
$renamedUsername );
+               } else {
+                       // Try again, client, and please get it right this time.
+                       return AuthenticationResponse::newUI(
+                               [
+                                       new ButtonAuthenticationRequest(
+                                               'caRenameOk', wfMessage( 'ok' 
), wfMessage( 'sulrenamewarning-authmanager-ok-help' )
+                                       )
+                               ],
+                               wfMessage( 'sulrenamewarning-renamed', 
$username, $renamedUsername )
+                       );
+               }
+       }
+
+       public function postAuthentication( $user, AuthenticationResponse 
$response ) {
+               if ( $response->status === AuthenticationResponse::PASS ) {
+                       $centralUser = CentralAuthUser::getMasterInstance( 
$user );
+                       if ( $centralUser->exists() && 
$centralUser->isAttached() &&
+                               $centralUser->getEmail() != $user->getEmail()
+                       ) {
+                               $user->setEmail( $centralUser->getEmail() );
+                               $user->mEmailAuthenticated = 
$centralUser->getEmailAuthenticationTimestamp();
+                               $user->saveSettings();
+                       }
+               }
+       }
+
+       public function testUserCanAuthenticate( $username ) {
+               $username = User::getCanonicalName( $username, 'usable' );
+               if ( $username === false ) {
+                       return false;
+               }
+
+               // Note this omits the case where an unattached local user 
exists but
+               // will be globalized on login thanks to $this->autoMigrate or
+               // $this->autoMigrateNonGlobalAccounts. Both are impossible to 
really
+               // test here because they both need cleartext passwords to do 
their
+               // thing. If you have such accounts on your wiki, you should 
have
+               // LocalPasswordPrimaryAuthenticationProvider configured too 
which
+               // will return true for such users.
+
+               $centralUser = CentralAuthUser::getInstanceByName( $username );
+               return $centralUser && $centralUser->exists() &&
+                       ( $centralUser->isAttached() || !User::idFromName( 
$username ) ) &&
+                       !$centralUser->getPasswordObject() instanceof 
InvalidPassword;
+       }
+
+       public function testUserExists( $username, $flags = User::READ_NORMAL ) 
{
+               $username = User::getCanonicalName( $username, 'usable' );
+               if ( $username === false ) {
+                       return false;
+               }
+
+               $centralUser = CentralAuthUser::getInstanceByName( $username );
+               return $centralUser && $centralUser->exists();
+       }
+
+       public function providerAllowsAuthenticationDataChange(
+               AuthenticationRequest $req, $checkData = true
+       ) {
+               if ( get_class( $req ) === PasswordAuthenticationRequest::class 
) {
+                       if ( !$checkData ) {
+                               return StatusValue::newGood();
+                       }
+
+                       $username = User::getCanonicalName( $req->username, 
'usable' );
+                       if ( $username !== false ) {
+                               $centralUser = 
CentralAuthUser::getInstanceByName( $username );
+                               if ( $centralUser && $centralUser->isAttached() 
) {
+                                       $sv = StatusValue::newGood();
+                                       if ( $req->password !== null ) {
+                                               if ( $req->password !== 
$req->retype ) {
+                                                       $sv->fatal( 'badretype' 
);
+                                               } else {
+                                                       $sv->merge( 
$this->checkPasswordValidity( $username, $req->password ) );
+                                               }
+                                       }
+                                       return $sv;
+                               }
+                       }
+               }
+
+               return StatusValue::newGood( 'ignored' );
+       }
+
+       public function providerChangeAuthenticationData( AuthenticationRequest 
$req ) {
+               $username = $req->username !== null ? User::getCanonicalName( 
$req->username, 'usable' ) : false;
+               if ( $username === false ) {
+                       return;
+               }
+
+               if ( get_class( $req ) === PasswordAuthenticationRequest::class 
) {
+                       $centralUser = 
CentralAuthUser::getMasterInstanceByName( $username );
+                       if ( $centralUser && $centralUser->isAttached() ) {
+                               $centralUser->setPassword( $req->password );
+                       }
+               }
+       }
+
+       public function accountCreationType() {
+               return self::TYPE_CREATE;
+       }
+
+       public function testUserForCreation( $user, $autocreate ) {
+               global $wgCentralAuthEnableGlobalRenameRequest;
+
+               $status = parent::testUserForCreation( $user, $autocreate );
+               if ( !$status->isOk() ) {
+                       return $status;
+               }
+
+               $centralUser = CentralAuthUser::getMasterInstance( $user );
+
+               // Rename in progress?
+               if ( $centralUser->renameInProgressOn( wfWikiID() ) ) {
+                       $status->fatal( 'centralauth-rename-abortlogin' );
+                       return $status;
+               }
+
+               if ( $autocreate !== $this->getUniqueId() ) {
+                       // Prevent creation if the user exists centrally
+                       if ( $centralUser->exists() ) {
+                               $status->fatal( 'centralauth-account-exists' );
+                               return $status;
+                       }
+
+                       // Prevent creation if the user exists anywhere else we 
know about,
+                       // and we're asked to
+                       if ( $this->preventUnattached && 
$centralUser->listUnattached() ) {
+                               $status->fatal( 
'centralauth-account-unattached-exists' );
+                               return $status;
+                       }
+
+                       // Block account creation if name is a pending rename 
request
+                       if ( $wgCentralAuthEnableGlobalRenameRequest &&
+                               GlobalRenameRequest::nameHasPendingRequest( 
$user->getName() )
+                       ) {
+                               $status->fatal( 
'centralauth-account-rename-exists' );
+                               return $status;
+                       }
+               }
+
+               return $status;
+       }
+
+       public function testForAccountCreation( $user, $creator, array $reqs ) {
+               $req = AuthenticationRequest::getRequestByClass( $reqs, 
PasswordAuthenticationRequest::class );
+
+               $ret = StatusValue::newGood();
+               if ( $req && $req->username !== null && $req->password !== null 
) {
+                       if ( $req->password !== $req->retype ) {
+                               $ret->fatal( 'badretype' );
+                       } else {
+                               $ret->merge(
+                                       $this->checkPasswordValidity( 
$user->getName(), $req->password )
+                               );
+                       }
+               }
+
+               // Check CentralAuthAntiSpoof, if applicable
+               if ( class_exists( 'AntiSpoofAuthenticationRequest' ) ) {
+                       $antiSpoofReq = 
AuthenticationRequest::getRequestByClass( $reqs, 
AntiSpoofAuthenticationRequest::class );
+                       $ret->merge( CentralAuthAntiSpoofHooks::testNewAccount(
+                               $user, $creator, $this->antiSpoofAccounts, 
$antiSpoofReq && $antiSpoofReq->ignoreAntiSpoof
+                       ) );
+               }
+
+               return $ret;
+       }
+
+       public function beginPrimaryAccountCreation( $user, $creator, array 
$reqs ) {
+               $req = AuthenticationRequest::getRequestByClass( $reqs, 
PasswordAuthenticationRequest::class );
+               if ( $req ) {
+                       if ( $req->username !== null && $req->password !== null 
) {
+                               $centralUser = 
CentralAuthUser::getMasterInstance( $user );
+                               if ( $centralUser->exists() ) {
+                                       return AuthenticationResponse::newFail( 
wfMessage( 'centralauth-account-exists' ) );
+                               }
+                               if ( $centralUser->listUnattached() ) {
+                                       // $this->testUserForCreation() will 
already have rejected it if necessary
+                                       return 
AuthenticationResponse::newAbstain();
+                               }
+                               // Username is unused; set up as a global 
account
+                               if ( !$centralUser->register( $req->password, 
$user->getEmail() ) ) {
+                                       // Wha?
+                                       return AuthenticationResponse::newFail( 
wfMessage( 'userexists' ) );
+                               }
+                               $centralUser->attach( wfWikiID(), 'new' );
+                               CentralAuthUtils::scheduleCreationJobs( 
$centralUser );
+                               return AuthenticationResponse::newPass( 
$user->getName() );
+                       }
+               }
+
+               return AuthenticationResponse::newAbstain();
+       }
+
+       public function autoCreatedAccount( $user, $source ) {
+               $centralUser = CentralAuthUser::getMasterInstance( $user );
+               if ( !$centralUser->exists() ) {
+                       $this->logger->debug(
+                               'Not centralizing auto-created user {username}, 
central account doesn\'t exist',
+                               [
+                                       'user' => $user->getName(),
+                               ]
+                       );
+               } elseif ( $source !== $this->getUniqueId() && 
$centralUser->listUnattached() ) {
+                       $this->logger->debug(
+                               'Not centralizing auto-created user {username}, 
unattached accounts exist',
+                               [
+                                       'user' => $user->getName(),
+                                       'source' => $source,
+                               ]
+                       );
+               } else {
+                       $this->logger->debug(
+                               'Centralizing auto-created user {username}',
+                               [
+                                       'user' => $user->getName(),
+                               ]
+                       );
+                       $centralUser->attach( wfWikiID(), 'login' );
+                       $centralUser->addLocalName( wfWikiID() );
+
+                       if ( $centralUser->getEmail() != $user->getEmail() ) {
+                               $user->setEmail( $centralUser->getEmail() );
+                               $user->mEmailAuthenticated = 
$centralUser->getEmailAuthenticationTimestamp();
+                       }
+               }
+       }
+}
diff --git a/includes/CentralAuthSecondaryAuthenticationProvider.php 
b/includes/CentralAuthSecondaryAuthenticationProvider.php
new file mode 100644
index 0000000..f97be5b
--- /dev/null
+++ b/includes/CentralAuthSecondaryAuthenticationProvider.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * 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 Auth
+ */
+
+use MediaWiki\Auth\AbstractSecondaryAuthenticationProvider;
+use MediaWiki\Auth\AuthenticationResponse;
+
+/**
+ * This implements CentralAuth checks that should be done on all logins, e.g.
+ * being renamed or centrally locked.
+ */
+class CentralAuthSecondaryAuthenticationProvider extends 
AbstractSecondaryAuthenticationProvider {
+
+       /**
+        * @param array $params
+        */
+       public function __construct( $params = [] ) {
+       }
+
+       public function getAuthenticationRequests( $action, array $options ) {
+               return [];
+       }
+
+       public function beginSecondaryAuthentication( $user, array $reqs ) {
+               $centralUser = CentralAuthUser::getMasterInstance( $user );
+
+               // Since logins are rare, check the actual DB
+               $rename = $centralUser->renameInProgressOn( wfWikiID() );
+               if ( $rename ) {
+                       // This wiki specifically has a rename in progress, so 
always abort
+                       return AuthenticationResponse::newFail( wfMessage( 
'centralauth-rename-abortlogin' ) );
+               }
+
+               // Now check if the user is the target of a rename anywhere
+               $rename = $centralUser->renameInProgress();
+               if ( $rename ) {
+                       // It's possible a user is being renamed but someone 
else with
+                       // an unattached account is trying to login. Since 
we've already
+                       // moved everything over to the new account name, we 
only need
+                       // to check one username.
+                       $newCAUser = $rename[1] === $user->getName()
+                               ? $centralUser
+                               : CentralAuthUser::getInstanceByName( 
$rename[0] );
+
+                       if ( $newCAUser->isAttached() ) {
+                               // If there is an account using that name that 
exists on this wiki
+                               // reject the login.
+                               return AuthenticationResponse::newFail( 
wfMessage( 'centralauth-rename-abortlogin' ) );
+                       }
+               }
+
+               if ( $centralUser->canAuthenticate() === 'locked' ) {
+                       return AuthenticationResponse::newFail( wfMessage( 
'centralauth-login-error-locked' ) );
+               }
+
+               return AuthenticationResponse::newAbstain();
+       }
+
+       public function beginSecondaryAccountCreation( $user, $creator, array 
$reqs ) {
+               return AuthenticationResponse::newAbstain();
+       }
+}
diff --git a/includes/CentralAuthUser.php b/includes/CentralAuthUser.php
index 5b6f3cf..797882d 100644
--- a/includes/CentralAuthUser.php
+++ b/includes/CentralAuthUser.php
@@ -584,7 +584,7 @@
         *
         * @return Password
         */
-       protected function getPasswordObject() {
+       public function getPasswordObject() {
                $this->loadState();
                return $this->getPasswordFromString( $this->mPassword, 
$this->mSalt );
        }
diff --git a/includes/CentralAuthUtils.php b/includes/CentralAuthUtils.php
index ad267d2..fab028b 100644
--- a/includes/CentralAuthUtils.php
+++ b/includes/CentralAuthUtils.php
@@ -227,4 +227,28 @@
                        RequestContext::getMain()->getStats()->timing( 
"centralauth.session.delete", $real );
                }
        }
+
+       /**
+        * Sets up jobs to create and attach a local account for the given user 
on every wiki listed in
+        * $wgCentralAuthAutoCreateWikis.
+        * @param CentralAuthUser $centralUser
+        */
+       public static function scheduleCreationJobs( CentralAuthUser 
$centralUser ) {
+               global $wgCentralAuthAutoCreateWikis;
+
+               $name = $centralUser->getName();
+               $thisWiki = wfWikiID();
+               $session = RequestContext::getMain()->exportSession();
+               foreach ( $wgCentralAuthAutoCreateWikis as $wiki ) {
+                       if ( $wiki === $thisWiki ) {
+                               continue;
+                       }
+                       $job = Job::factory(
+                               'CentralAuthCreateLocalAccountJob',
+                               Title::makeTitleSafe( NS_USER, $name ),
+                               array( 'name' => $name, 'from' => $thisWiki, 
'session' => $session )
+                       );
+                       JobQueueGroup::singleton( $wiki )->push( $job );
+               }
+       }
 }
diff --git a/includes/LocalRenameJob/LocalUserMergeJob.php 
b/includes/LocalRenameJob/LocalUserMergeJob.php
index 5111870..c38df35 100755
--- a/includes/LocalRenameJob/LocalUserMergeJob.php
+++ b/includes/LocalRenameJob/LocalUserMergeJob.php
@@ -60,22 +60,24 @@
         * @throws Exception
         */
        private function maybeCreateNewUser( $newName ) {
-               global $wgAuth;
+               global $wgDisableAuthManager;
                $user = User::newFromName( $newName );
                if ( $user->getId() ) {
                        // User already exists, nothing to do.
                        return $user;
                }
 
-               // Logic from CentralAuthHooks::attemptAddUser
-               $user->loadDefaults( $newName );
-               $status = $user->addToDatabase();
-               if ( !$status->isOK() ) {
+               if ( class_exists( MediaWiki\Auth\AuthManager::class ) && 
!$wgDisableAuthManager ) {
+                       $status = MediaWiki\Auth\AuthManager::autoCreateUser( 
$user, CentralAuthPrimaryAuthenticationProvider::class, false );
+                       if ( !$status->isGood() ) {
+                               $this->updateStatus( 'failed' );
+                               throw new Exception( 
"AuthManager::autoCreateUser failed for $newName: " . $status->getWikiText( 
null, null, 'en' ) );
+                       }
+               } elseif ( !MediaWiki\Session\SessionManager::autoCreateUser( 
$user ) ) {
                        $this->updateStatus( 'failed' );
-                       throw new Exception( "User::addToDatabase failed for 
$newName: {$status->getWikiText()}" );
+                       throw new Exception( "SessionManager::autoCreateUser 
failed for $newName" );
                }
-               $wgAuth->initUser( $user, true );
-               Hooks::run( 'AuthPluginAutoCreate', array( $user ) );
+
                return $user;
        }
 
diff --git a/includes/specials/SpecialCentralLogin.php 
b/includes/specials/SpecialCentralLogin.php
index d62de05..da1d70a 100644
--- a/includes/specials/SpecialCentralLogin.php
+++ b/includes/specials/SpecialCentralLogin.php
@@ -230,9 +230,8 @@
                $this->getContext()->setUser( $user );
 
                // Show the login success page
-               $form = new LoginForm( new FauxRequest() );
-               $inject_html = '';
 
+               $inject_html = '';
                if ( $attempt['type'] === 'signup' ) {
                        $msg = $this->msg( 'centralauth-welcomecreation-msg' );
                        if ( !$msg->isDisabled() ) {
@@ -266,7 +265,8 @@
                                CentralAuthHooks::getDomainAutoLoginHtml( 
$user, $centralUser ) );
                }
 
-               $form->showReturnToPage(
+               $helper = new LoginHelper( $this->getContext() );
+               $helper->showReturnToPage(
                        $action,
                        $attempt['returnTo'],
                        $attempt['returnToQuery'],
@@ -292,9 +292,10 @@
                        $this->showError( 'centralauth-warning-notattached' );
                        return;
                }
+
                // Show the login success page
-               $form = new LoginForm( new FauxRequest() );
-               $form->showReturnToPage( 'success',
+               $helper = new LoginHelper( $this->getContext() );
+               $helper->showReturnToPage( 'success',
                        $this->getRequest()->getVal( 'returnto', '' ),
                        $this->getRequest()->getVal( 'returntoquery', '' )
                );
diff --git a/maintenance/createLocalAccount.php 
b/maintenance/createLocalAccount.php
index b4ca65d..931274a 100644
--- a/maintenance/createLocalAccount.php
+++ b/maintenance/createLocalAccount.php
@@ -14,6 +14,8 @@
        }
 
        public function execute() {
+               global $wgDisableAuthManager;
+
                if ( !class_exists( 'CentralAuthUser' ) ) {
                        $this->error( "CentralAuth isn't enabled on this 
wiki\n", 1 );
                }
@@ -29,21 +31,19 @@
                if ( $user->getId() ) {
                        $this->error( "User '$username' already exists\n", 1 );
                } else {
-                       global $wgAuth;
-
                        $central = CentralAuthUser::getInstance( $user );
                        if ( !$central->exists() ) {
                                $this->error( "No such global user: 
'$username'\n", 1 );
                        }
 
-                       $user->loadDefaults( $username );
-                       $user->addToDatabase();
-
-                       $wgAuth->initUser( $user, true );
-                       $wgAuth->updateUser( $user );
-
-                       # Notify hooks (e.g. Newuserlog)
-                       Hooks::run( 'AuthPluginAutoCreate', array( $user ) );
+                       if ( class_exists( MediaWiki\Auth\AuthManager::class ) 
&& !$wgDisableAuthManager ) {
+                               $status = 
MediaWiki\Auth\AuthManager::autoCreateUser( $user, 
CentralAuthPrimaryAuthenticationProvider::class, false );
+                               if ( !$status->isGood() ) {
+                                       $this->error( 
"AuthManager::autoCreateUser failed for $username: " . $status->getWikiText( 
null, null, 'en' ) );
+                               }
+                       } elseif ( 
!MediaWiki\Session\SessionManager::autoCreateUser( $user ) ) {
+                               $this->error( "SessionManager::autoCreateUser 
failed for $username" );
+                       }
 
                        # Update user count
                        $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
diff --git a/tests/phpunit/CentralAuthHooksUsingDatabaseTest.php 
b/tests/phpunit/CentralAuthHooksUsingDatabaseTest.php
index 380344f..f0a74f1 100644
--- a/tests/phpunit/CentralAuthHooksUsingDatabaseTest.php
+++ b/tests/phpunit/CentralAuthHooksUsingDatabaseTest.php
@@ -5,26 +5,6 @@
  */
 class CentralAuthHooksUsingDatabaseTest extends 
CentralAuthTestCaseUsingDatabase {
 
-
-       /**
-        * @dataProvider provideAbortNewAccount
-        * @covers CentralAuthHooks::onAbortNewAccount
-        */
-       public function testOnAbortNewAccount( $user, $result) {
-               $error = '';
-               $hookResult = CentralAuthHooks::onAbortNewAccount( $user, 
$error );
-               $this->assertSame( $result, $hookResult );
-       }
-
-       public function provideAbortNewAccount() {
-               $userExists = User::newFromName( 'GlobalUser' );
-               $userNotExists = User::newFromName( 'UserNotExists' );
-               return array(
-                       array( $userNotExists, true ),
-                       array( $userExists, false ),
-               );
-       }
-
        /**
         * @covers CentralAuthHooks::onUserGetEmailAuthenticationTimestampp
         */
@@ -32,33 +12,6 @@
                $user = User::newFromName( 'GlobalLockedUser' );
                $this->assertFalse( $user->isEmailConfirmed() );
                $this->assertFalse( $user->canReceiveEmail() );
-       }
-
-       /**
-        * @dataProvider provideAbortLogin
-        * @covers CentralAuthHooks::onAbortLogin
-        */
-       public function testOnAbortLogin( $user, $result, $test) {
-               $retval = 0;
-               $msg = '';
-               $hookResult = CentralAuthHooks::onAbortLogin( $user, '', 
$retval, $msg );
-               $this->assertEquals( $result, $hookResult, $test );
-       }
-
-       public function provideAbortLogin() {
-               $user = User::newFromName( 'GlobalUser' );
-               $lockedUser = User::newFromName( 'GlobalLockedUser' );
-
-               // Fake out CentralAuthUser::getInstance()
-               $noUser = User::newFromName( 'NoUser' );
-               TestingAccessWrapper::newFromClass( 'CentralAuthUser' 
)->getUserCache()
-                       ->set( $noUser->getName(), 
CentralAuthUser::newUnattached( 'NoUser' ) );
-
-               return array(
-                       array( $user, true, 'Attached user can login' ),
-                       array( $noUser, true, 'Unattached user can login' ),
-                       array( $lockedUser, false, 'Locked User cannot login' ),
-               );
        }
 
        /**
diff --git a/tests/phpunit/CentralAuthPreAuthManagerHooksUsingDatabaseTest.php 
b/tests/phpunit/CentralAuthPreAuthManagerHooksUsingDatabaseTest.php
new file mode 100644
index 0000000..a06ebb7
--- /dev/null
+++ b/tests/phpunit/CentralAuthPreAuthManagerHooksUsingDatabaseTest.php
@@ -0,0 +1,93 @@
+<?php
+/**
+ * Basic tests for CentralAuthPreAuthManagerHooks
+ * @group CentralAuthDB
+ */
+class CentralAuthPreAuthManagerHooksUsingDatabaseTest extends 
CentralAuthTestCaseUsingDatabase {
+
+
+       /**
+        * @dataProvider provideAbortNewAccount
+        * @covers CentralAuthPreAuthManagerHooks::onAbortNewAccount
+        */
+       public function testOnAbortNewAccount( $user, $result) {
+               $error = '';
+               $hookResult = 
CentralAuthPreAuthManagerHooks::onAbortNewAccount( $user, $error );
+               $this->assertSame( $result, $hookResult );
+       }
+
+       public function provideAbortNewAccount() {
+               $userExists = User::newFromName( 'GlobalUser' );
+               $userNotExists = User::newFromName( 'UserNotExists' );
+               return array(
+                       array( $userNotExists, true ),
+                       array( $userExists, false ),
+               );
+       }
+
+       /**
+        * @dataProvider provideAbortLogin
+        * @covers CentralAuthPreAuthManagerHooks::onAbortLogin
+        */
+       public function testOnAbortLogin( $user, $result, $test) {
+               $retval = 0;
+               $msg = '';
+               $hookResult = CentralAuthPreAuthManagerHooks::onAbortLogin( 
$user, '', $retval, $msg );
+               $this->assertEquals( $result, $hookResult, $test );
+       }
+
+       public function provideAbortLogin() {
+               $user = User::newFromName( 'GlobalUser' );
+               $lockedUser = User::newFromName( 'GlobalLockedUser' );
+
+               // Fake out CentralAuthUser::getInstance()
+               $noUser = User::newFromName( 'NoUser' );
+               TestingAccessWrapper::newFromClass( 'CentralAuthUser' 
)->getUserCache()
+                       ->set( $noUser->getName(), 
CentralAuthUser::newUnattached( 'NoUser' ) );
+
+               return array(
+                       array( $user, true, 'Attached user can login' ),
+                       array( $noUser, true, 'Unattached user can login' ),
+                       array( $lockedUser, false, 'Locked User cannot login' ),
+               );
+       }
+
+       /**
+        * Setup a fresh set of global users for each test.
+        * Note: MediaWikiTestCase::resetDB() will delete all tables between
+        * test runs, so no explicite tearDown() is needed.
+        */
+       protected function setUp() {
+               parent::setUp();
+               $u = new CentralAuthTestUser(
+                       'GlobalUser',
+                       'GUP@ssword',
+                       array( 'gu_id' => '1001' ),
+                       array(
+                               array( wfWikiID(), 'primary' ),
+                               array( 'enwiki', 'primary' ),
+                               array( 'dewiki', 'login' ),
+                               array( 'metawiki', 'password' ),
+                       )
+               );
+               $u->save( $this->db );
+
+               $u = new CentralAuthTestUser(
+                       'GlobalLockedUser',
+                       'GLUP@ssword',
+                       array(
+                               'gu_id' => '1003',
+                               'gu_locked' => 1,
+                               'gu_hidden' => CentralAuthUser::HIDDEN_NONE,
+                               'gu_email' => 'testlocked@localhost',
+                               'gu_home_db' => 'metawiki',
+                       ),
+                       array(
+                               array( 'metawiki', 'primary' ),
+                       )
+               );
+               $u->save( $this->db );
+       }
+
+
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/284499
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ia60d92f9e08740e1f76a3744147b93a7636ae09e
Gerrit-PatchSet: 25
Gerrit-Project: mediawiki/extensions/CentralAuth
Gerrit-Branch: master
Gerrit-Owner: Anomie <[email protected]>
Gerrit-Reviewer: Anomie <[email protected]>
Gerrit-Reviewer: BryanDavis <[email protected]>
Gerrit-Reviewer: GergÅ‘ Tisza <[email protected]>
Gerrit-Reviewer: Legoktm <[email protected]>
Gerrit-Reviewer: Paladox <[email protected]>
Gerrit-Reviewer: Siebrand <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to