Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 243
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
CentralAuthPrimaryAuthenticationProvider
0.00% covered (danger)
0.00%
0 / 243
0.00% covered (danger)
0.00%
0 / 16
9312
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 getAuthenticationRequests
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 getPasswordAuthenticationRequest
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 beginPrimaryAuthentication
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 1
380
 postAuthentication
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
72
 testUserCanAuthenticate
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
56
 testUserExists
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 providerAllowsAuthenticationDataChange
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
110
 providerChangeAuthenticationData
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
72
 accountCreationType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 testUserForCreation
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
182
 getAntiSpoofAuthenticationRequest
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 testForAccountCreation
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 beginPrimaryAccountCreation
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
56
 finishAccountCreation
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 autoCreatedAccount
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 * @ingroup Auth
20 */
21
22namespace MediaWiki\Extension\CentralAuth;
23
24use IDBAccessObject;
25use InvalidPassword;
26use MediaWiki\Auth\AbstractPasswordPrimaryAuthenticationProvider;
27use MediaWiki\Auth\AuthenticationRequest;
28use MediaWiki\Auth\AuthenticationResponse;
29use MediaWiki\Auth\AuthManager;
30use MediaWiki\Auth\PasswordAuthenticationRequest;
31use MediaWiki\Deferred\DeferredUpdates;
32use MediaWiki\Extension\AntiSpoof\AntiSpoofAuthenticationRequest;
33use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameRequestStore;
34use MediaWiki\Extension\CentralAuth\User\CentralAuthAntiSpoofManager;
35use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
36use MediaWiki\User\User;
37use MediaWiki\User\UserIdentityLookup;
38use MediaWiki\User\UserNameUtils;
39use MediaWiki\WikiMap\WikiMap;
40use Psr\Log\NullLogger;
41use RequestContext;
42use StatusValue;
43use Wikimedia\Rdbms\ReadOnlyMode;
44
45/**
46 * A primary authentication provider that uses the CentralAuth password.
47 */
48class CentralAuthPrimaryAuthenticationProvider
49    extends AbstractPasswordPrimaryAuthenticationProvider
50{
51    /** @var string The internal ID of this provider. */
52    public const ID = 'CentralAuthPrimaryAuthenticationProvider';
53
54    private ReadOnlyMode $readOnlyMode;
55    private UserIdentityLookup $userIdentityLookup;
56    private CentralAuthAntiSpoofManager $caAntiSpoofManager;
57    private CentralAuthDatabaseManager $databaseManager;
58    private CentralAuthUtilityService $utilityService;
59    private GlobalRenameRequestStore $globalRenameRequestStore;
60
61    /** @var bool Whether to auto-migrate non-merged accounts on login */
62    protected $autoMigrate = null;
63
64    /** @var bool Whether to auto-migrate non-global accounts on login */
65    protected $autoMigrateNonGlobalAccounts = null;
66
67    /** @var bool Whether to check for spoofed user names */
68    protected $antiSpoofAccounts = null;
69
70    /**
71     * @param ReadOnlyMode $readOnlyMode
72     * @param UserIdentityLookup $userIdentityLookup
73     * @param UserNameUtils $userNameUtils
74     * @param CentralAuthAntiSpoofManager $caAntiSpoofManager
75     * @param CentralAuthDatabaseManager $databaseManager
76     * @param CentralAuthUtilityService $utilityService
77     * @param GlobalRenameRequestStore $globalRenameRequestStore
78     * @param array $params Settings. All are optional, defaulting to the
79     *  similarly-named $wgCentralAuth* globals.
80     *  - autoMigrate: If true, attempt to auto-migrate local accounts on other
81     *    wikis when logging in.
82     *  - autoMigrateNonGlobalAccounts: If true, attempt to auto-migrate
83     *    non-global accounts on login.
84     *  - antiSpoofAccounts: Whether to anti-spoof new accounts. Ignored if the
85     *    AntiSpoof extension isn't installed or the extension is outdated.
86     */
87    public function __construct(
88        ReadOnlyMode $readOnlyMode,
89        UserIdentityLookup $userIdentityLookup,
90        UserNameUtils $userNameUtils,
91        CentralAuthAntiSpoofManager $caAntiSpoofManager,
92        CentralAuthDatabaseManager $databaseManager,
93        CentralAuthUtilityService $utilityService,
94        GlobalRenameRequestStore $globalRenameRequestStore,
95        $params = []
96    ) {
97        global $wgCentralAuthAutoMigrate,
98            $wgCentralAuthAutoMigrateNonGlobalAccounts,
99            $wgCentralAuthStrict, $wgAntiSpoofAccounts;
100
101        $this->readOnlyMode = $readOnlyMode;
102        $this->userIdentityLookup = $userIdentityLookup;
103        // AbstractAuthenticationProvider::$userNameUtils is protected
104        // TODO this is probably unneeded since AbstractAuthenticationProvider::init
105        // will be called and that will set the $userNameUtils, no need to even inject
106        // into this class
107        $this->userNameUtils = $userNameUtils;
108        $this->caAntiSpoofManager = $caAntiSpoofManager;
109        $this->databaseManager = $databaseManager;
110        $this->utilityService = $utilityService;
111        $this->globalRenameRequestStore = $globalRenameRequestStore;
112
113        $params += [
114            'autoMigrate' => $wgCentralAuthAutoMigrate,
115            'autoMigrateNonGlobalAccounts' => $wgCentralAuthAutoMigrateNonGlobalAccounts,
116            'antiSpoofAccounts' => $wgAntiSpoofAccounts,
117            'authoritative' => $wgCentralAuthStrict,
118        ];
119
120        parent::__construct( $params );
121
122        $this->autoMigrate = (bool)$params['autoMigrate'];
123        $this->autoMigrateNonGlobalAccounts = (bool)$params['autoMigrateNonGlobalAccounts'];
124        $this->antiSpoofAccounts = (bool)$params['antiSpoofAccounts'];
125    }
126
127    /** @inheritDoc */
128    public function getAuthenticationRequests( $action, array $options ) {
129        $ret = parent::getAuthenticationRequests( $action, $options );
130
131        if ( $this->antiSpoofAccounts && $action === AuthManager::ACTION_CREATE ) {
132            $user = User::newFromName( $options['username'] ) ?: new User();
133            if ( $user->isAllowed( 'override-antispoof' ) ) {
134                $ret[] = new AntiSpoofAuthenticationRequest();
135            }
136        }
137
138        return $ret;
139    }
140
141    /**
142     * @param array $reqs
143     * @return PasswordAuthenticationRequest|null
144     */
145    private static function getPasswordAuthenticationRequest( array $reqs ) {
146        return AuthenticationRequest::getRequestByClass(
147            $reqs, PasswordAuthenticationRequest::class
148        );
149    }
150
151    /** @inheritDoc */
152    public function beginPrimaryAuthentication( array $reqs ) {
153        $req = self::getPasswordAuthenticationRequest( $reqs );
154        if ( !$req ) {
155            return AuthenticationResponse::newAbstain();
156        }
157
158        if ( $req->username === null || $req->password === null ) {
159            return AuthenticationResponse::newAbstain();
160        }
161
162        $username = $this->userNameUtils->getCanonical( $req->username, UserNameUtils::RIGOR_USABLE );
163        if ( $username === false ) {
164            return AuthenticationResponse::newAbstain();
165        }
166
167        $status = $this->checkPasswordValidity( $username, $req->password );
168        if ( !$status->isOK() ) {
169            return $this->getFatalPasswordErrorResponse( $username, $status );
170        }
171
172        // First, check normal login
173        $centralUser = CentralAuthUser::getInstanceByName( $username );
174
175        $authenticateResult = $centralUser->authenticate( $req->password );
176
177        $pass = $authenticateResult === [ CentralAuthUser::AUTHENTICATE_OK ];
178
179        if ( in_array( CentralAuthUser::AUTHENTICATE_LOCKED, $authenticateResult ) ) {
180            if ( !in_array( CentralAuthUser::AUTHENTICATE_BAD_PASSWORD, $authenticateResult ) ) {
181                // Because the absence of "bad password" for any code that hooks and receives
182                // the returned AuthenticationResponse means either that the password
183                // was correct or that the password was not checked, provide "good password"
184                // which removes the two possible meanings of no "bad password".
185                $authenticateResult[] = CentralAuthUser::AUTHENTICATE_GOOD_PASSWORD;
186            }
187            return AuthenticationResponse::newFail(
188                wfMessage( 'centralauth-login-error-locked' )
189                    ->params( wfEscapeWikiText( $centralUser->getName() ) ),
190                $authenticateResult
191            );
192        }
193
194        // If we don't have a central account, see if all local accounts match
195        // the password and can be globalized. (bug T72392)
196        if ( !$centralUser->exists() ) {
197            $this->logger->debug(
198                'no global account for "{username}"', [ 'username' => $username ] );
199            // Confirm using DB_PRIMARY in case of replication lag
200            $latestCentralUser = CentralAuthUser::getPrimaryInstanceByName( $username );
201            if ( $this->autoMigrateNonGlobalAccounts && !$latestCentralUser->exists() ) {
202                $ok = $latestCentralUser->storeAndMigrate(
203                    [ $req->password ],
204                    /* $sendToRC = */ true,
205                    /* $safe = */ true,
206                    /* $checkHome = */ true
207                );
208                if ( $ok ) {
209                    $this->logger->debug(
210                        'wgCentralAuthAutoMigrateNonGlobalAccounts successful in creating ' .
211                        'a global account for "{username}"',
212                        [ 'username' => $username ]
213                    );
214                    $this->setPasswordResetFlag( $username, $status );
215                    return AuthenticationResponse::newPass( $username );
216                }
217            }
218            return $this->failResponse( $req );
219        }
220
221        if ( $pass && $this->autoMigrate ) {
222            // If the user passed in the global password, we can identify
223            // any remaining local accounts with a matching password
224            // and migrate them in transparently.
225            // That may or may not include the current wiki.
226            $this->logger->debug( 'attempting wgCentralAuthAutoMigrate for "{username}"', [
227                'username' => $username,
228            ] );
229            if ( $centralUser->isAttached() ) {
230                // Defer any automatic migration for other wikis
231                DeferredUpdates::addCallableUpdate( static function () use ( $username, $req ) {
232                    $latestCentralUser = CentralAuthUser::getPrimaryInstanceByName( $username );
233                    $latestCentralUser->attemptPasswordMigration( $req->password );
234                } );
235            } else {
236                // The next steps depend on whether a migration happens for this wiki.
237                // Update the $centralUser instance so the checks below reflect any migrations.
238                $centralUser = CentralAuthUser::getPrimaryInstanceByName( $username );
239                $centralUser->attemptPasswordMigration( $req->password );
240            }
241        }
242
243        if ( !$centralUser->isAttached() ) {
244            $local = User::newFromName( $username );
245            if ( $local && $local->getId() ) {
246                // An unattached local account; central authentication can't
247                // be used until this account has been transferred.
248                // $wgCentralAuthStrict will determine if local login is allowed.
249                $this->logger->debug( 'unattached account for "{username}"', [
250                    'username' => $username,
251                ] );
252                return $this->failResponse( $req );
253            }
254        }
255
256        if ( $pass ) {
257            $this->setPasswordResetFlag( $username, $status );
258            return AuthenticationResponse::newPass( $username );
259        } else {
260            // We know the central user is attached at this point, so never
261            // fall back to other password providers.
262            return AuthenticationResponse::newFail( wfMessage( 'wrongpassword' ) );
263        }
264    }
265
266    /** @inheritDoc */
267    public function postAuthentication( $user, AuthenticationResponse $response ) {
268        if ( $response->status === AuthenticationResponse::PASS ) {
269            $centralUser = CentralAuthUser::getInstance( $user );
270            if ( $centralUser->exists() &&
271                $centralUser->isAttached() &&
272                $centralUser->getEmail() != $user->getEmail() &&
273                !$this->readOnlyMode->isReadOnly()
274            ) {
275                DeferredUpdates::addCallableUpdate( static function () use ( $user ) {
276                    $centralUser = CentralAuthUser::getPrimaryInstance( $user );
277                    if ( !$centralUser->exists() || !$centralUser->isAttached() ) {
278                        // something major changed?
279                        return;
280                    }
281
282                    $user->setEmail( $centralUser->getEmail() );
283                    // @TODO: avoid direct User object field access
284                    $user->mEmailAuthenticated = $centralUser->getEmailAuthenticationTimestamp();
285                    $user->saveSettings();
286                } );
287            }
288        }
289    }
290
291    /** @inheritDoc */
292    public function testUserCanAuthenticate( $username ) {
293        $username = $this->userNameUtils->getCanonical( $username, UserNameUtils::RIGOR_USABLE );
294        if ( $username === false ) {
295            return false;
296        }
297
298        // Note this omits the case where an unattached local user exists but
299        // will be globalized on login thanks to $this->autoMigrate or
300        // $this->autoMigrateNonGlobalAccounts. Both are impossible to really
301        // test here because they both need cleartext passwords to do their
302        // thing. If you have such accounts on your wiki, you should have
303        // LocalPasswordPrimaryAuthenticationProvider configured too which
304        // will return true for such users.
305
306        $centralUser = CentralAuthUser::getInstanceByName( $username );
307        $userIdentity = $this->userIdentityLookup->getUserIdentityByName( $username );
308        return $centralUser && $centralUser->exists() &&
309            ( $centralUser->isAttached() || !$userIdentity || !$userIdentity->isRegistered() ) &&
310            !$centralUser->getPasswordObject() instanceof InvalidPassword;
311    }
312
313    /** @inheritDoc */
314    public function testUserExists( $username, $flags = IDBAccessObject::READ_NORMAL ) {
315        $username = $this->userNameUtils->getCanonical( $username, UserNameUtils::RIGOR_USABLE );
316        if ( $username === false ) {
317            return false;
318        }
319
320        $centralUser = CentralAuthUser::getInstanceByName( $username );
321        return $centralUser && $centralUser->exists();
322    }
323
324    /** @inheritDoc */
325    public function providerAllowsAuthenticationDataChange(
326        AuthenticationRequest $req, $checkData = true
327    ) {
328        if ( get_class( $req ) === PasswordAuthenticationRequest::class ) {
329            if ( !$checkData ) {
330                return StatusValue::newGood();
331            }
332
333            $username = $this->userNameUtils->getCanonical( $req->username, UserNameUtils::RIGOR_USABLE );
334            if ( $username !== false ) {
335                $centralUser = CentralAuthUser::getInstanceByName( $username );
336                $userIdentity = $this->userIdentityLookup
337                    ->getUserIdentityByName( $username, IDBAccessObject::READ_LATEST );
338                if ( $centralUser->exists() &&
339                    ( $centralUser->isAttached() ||
340                    !$userIdentity || !$userIdentity->isRegistered() )
341                ) {
342                    $sv = StatusValue::newGood();
343                    if ( $req->password !== null ) {
344                        if ( $req->password !== $req->retype ) {
345                            $sv->fatal( 'badretype' );
346                        } else {
347                            $sv->merge( $this->checkPasswordValidity( $username, $req->password ) );
348                        }
349                    }
350                    return $sv;
351                }
352            }
353        }
354
355        return StatusValue::newGood( 'ignored' );
356    }
357
358    /** @inheritDoc */
359    public function providerChangeAuthenticationData( AuthenticationRequest $req ) {
360        $username = $req->username !== null
361            ? $this->userNameUtils->getCanonical( $req->username, UserNameUtils::RIGOR_USABLE )
362            : false;
363        if ( $username === false ) {
364            return;
365        }
366
367        if ( get_class( $req ) === PasswordAuthenticationRequest::class ) {
368            $centralUser = CentralAuthUser::getPrimaryInstanceByName( $username );
369            $userIdentity = $this->userIdentityLookup->getUserIdentityByName( $username, IDBAccessObject::READ_LATEST );
370            if ( $centralUser->exists() &&
371                ( $centralUser->isAttached() || !$userIdentity || !$userIdentity->isRegistered() )
372            ) {
373                $centralUser->setPassword( $req->password );
374            }
375        }
376    }
377
378    public function accountCreationType() {
379        return self::TYPE_CREATE;
380    }
381
382    /** @inheritDoc */
383    public function testUserForCreation( $user, $autocreate, array $options = [] ) {
384        global $wgCentralAuthEnableGlobalRenameRequest;
385
386        $options += [ 'flags' => IDBAccessObject::READ_NORMAL ];
387
388        $status = parent::testUserForCreation( $user, $autocreate, $options );
389        if ( !$status->isOK() ) {
390            return $status;
391        }
392
393        $centralUser = ( $options['flags'] & IDBAccessObject::READ_LATEST ) == IDBAccessObject::READ_LATEST
394            ? CentralAuthUser::getPrimaryInstance( $user )
395            : CentralAuthUser::getInstance( $user );
396
397        // Rename in progress?
398        if ( $centralUser->renameInProgressOn( WikiMap::getCurrentWikiId(), $options['flags'] ) ) {
399            $status->fatal( 'centralauth-rename-abortlogin', $user->getName() );
400            return $status;
401        }
402
403        if ( $autocreate !== $this->getUniqueId() ) {
404            // Prevent creation if the user exists centrally
405            if ( $centralUser->exists() &&
406                $autocreate !== AuthManager::AUTOCREATE_SOURCE_SESSION
407            ) {
408                $status->fatal( 'centralauth-account-exists' );
409                return $status;
410            }
411
412            // Prevent creation if the user exists anywhere else we know about
413            if ( $centralUser->listUnattached() ) {
414                $status->fatal( 'centralauth-account-unattached-exists' );
415                return $status;
416            }
417
418            // Block account creation if name is a pending rename request
419            if ( $wgCentralAuthEnableGlobalRenameRequest &&
420                $this->globalRenameRequestStore->nameHasPendingRequest( $user->getName() )
421            ) {
422                $status->fatal( 'centralauth-account-rename-exists' );
423                return $status;
424            }
425        }
426
427        // Check CentralAuthAntiSpoof, if applicable. Assume the user will override if they can.
428        if ( $this->antiSpoofAccounts && empty( $options['creating'] ) &&
429            !RequestContext::getMain()->getAuthority()->isAllowed( 'override-antispoof' )
430        ) {
431            $status->merge( $this->caAntiSpoofManager->testNewAccount(
432                $user, new User, true, false, new NullLogger
433            ) );
434        }
435
436        return $status;
437    }
438
439    /**
440     * @param array $reqs
441     * @return AntiSpoofAuthenticationRequest|null
442     */
443    private static function getAntiSpoofAuthenticationRequest( array $reqs ) {
444        return AuthenticationRequest::getRequestByClass(
445            $reqs,
446            AntiSpoofAuthenticationRequest::class
447        );
448    }
449
450    /** @inheritDoc */
451    public function testForAccountCreation( $user, $creator, array $reqs ) {
452        $req = self::getPasswordAuthenticationRequest( $reqs );
453
454        $ret = StatusValue::newGood();
455        if ( $req && $req->username !== null && $req->password !== null ) {
456            if ( $req->password !== $req->retype ) {
457                $ret->fatal( 'badretype' );
458            } else {
459                $ret->merge(
460                    $this->checkPasswordValidity( $user->getName(), $req->password )
461                );
462            }
463        }
464
465        // Check CentralAuthAntiSpoof, if applicable
466        $antiSpoofReq = self::getAntiSpoofAuthenticationRequest( $reqs );
467        $ret->merge( $this->caAntiSpoofManager->testNewAccount(
468            $user, $creator, $this->antiSpoofAccounts,
469            $antiSpoofReq && $antiSpoofReq->ignoreAntiSpoof
470        ) );
471
472        return $ret;
473    }
474
475    /** @inheritDoc */
476    public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) {
477        $req = self::getPasswordAuthenticationRequest( $reqs );
478        if ( $req ) {
479            if ( $req->username !== null && $req->password !== null ) {
480                $centralUser = CentralAuthUser::getPrimaryInstance( $user );
481                if ( $centralUser->exists() ) {
482                    return AuthenticationResponse::newFail(
483                        wfMessage( 'centralauth-account-exists' )
484                    );
485                }
486                if ( $centralUser->listUnattached() ) {
487                    // $this->testUserForCreation() will already have rejected it if necessary
488                    return AuthenticationResponse::newAbstain();
489                }
490                // Username is unused; set up as a global account
491                if ( !$centralUser->register( $req->password, $user->getEmail() ) ) {
492                    // Wha?
493                    return AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
494                }
495                return AuthenticationResponse::newPass( $user->getName() );
496            }
497        }
498        return AuthenticationResponse::newAbstain();
499    }
500
501    /** @inheritDoc */
502    public function finishAccountCreation( $user, $creator, AuthenticationResponse $response ) {
503        $centralUser = CentralAuthUser::getPrimaryInstance( $user );
504        // Do the attach in finishAccountCreation instead of begin because now the user has been
505        // added to database and local ID exists (which is needed in attach)
506        $centralUser->attach( WikiMap::getCurrentWikiId(), 'new' );
507        $this->databaseManager->getCentralPrimaryDB()->onTransactionCommitOrIdle(
508            function () use ( $centralUser ) {
509                $this->utilityService->scheduleCreationJobs( $centralUser );
510            },
511            __METHOD__
512        );
513        return null;
514    }
515
516    /** @inheritDoc */
517    public function autoCreatedAccount( $user, $source ) {
518        $centralUser = CentralAuthUser::getPrimaryInstance( $user );
519        if ( !$centralUser->exists() ) {
520            $this->logger->debug(
521                'Not centralizing auto-created user {username}, central account doesn\'t exist',
522                [
523                    'user' => $user->getName(),
524                ]
525            );
526        } elseif ( $source !== $this->getUniqueId() && $centralUser->listUnattached() ) {
527            $this->logger->debug(
528                'Not centralizing auto-created user {username}, unattached accounts exist',
529                [
530                    'user' => $user->getName(),
531                    'source' => $source,
532                ]
533            );
534        } else {
535            $this->logger->debug(
536                'Centralizing auto-created user {username}',
537                [
538                    'user' => $user->getName(),
539                ]
540            );
541            $centralUser->attach( WikiMap::getCurrentWikiId(), 'login' );
542            $centralUser->addLocalName( WikiMap::getCurrentWikiId() );
543
544            if ( $centralUser->getEmail() != $user->getEmail() ) {
545                $user->setEmail( $centralUser->getEmail() );
546                $user->mEmailAuthenticated = $centralUser->getEmailAuthenticationTimestamp();
547            }
548        }
549    }
550}