Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 123
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
CentralAuthTemporaryPasswordPrimaryAuthenticationProvider
0.00% covered (danger)
0.00%
0 / 123
0.00% covered (danger)
0.00%
0 / 9
1260
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 testUserExists
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getAuthenticationRequests
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 getTemporaryPassword
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
56
 setTemporaryPassword
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 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 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 maybeSendPasswordResetEmail
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 sendPasswordResetEmail
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
132
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 MailAddress;
25use MediaWiki\Auth\AbstractTemporaryPasswordPrimaryAuthenticationProvider;
26use MediaWiki\Auth\AuthenticationRequest;
27use MediaWiki\Auth\AuthenticationResponse;
28use MediaWiki\Auth\AuthManager;
29use MediaWiki\Auth\TemporaryPasswordAuthenticationRequest;
30use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
31use MediaWiki\Languages\LanguageNameUtils;
32use MediaWiki\Mail\Emailer;
33use MediaWiki\MainConfigNames;
34use MediaWiki\Password\Password;
35use MediaWiki\Registration\ExtensionRegistry;
36use MediaWiki\Title\Title;
37use MediaWiki\User\Options\UserOptionsLookup;
38use MediaWiki\User\User;
39use MediaWiki\User\UserIdentityLookup;
40use MediaWiki\User\UserNameUtils;
41use MediaWiki\WikiMap\WikiMap;
42use Wikimedia\IPUtils;
43use Wikimedia\Rdbms\IConnectionProvider;
44use Wikimedia\Rdbms\IDBAccessObject;
45
46/**
47 * A primary authentication provider that uses the temporary password field in
48 * the 'globaluser' table. Adapted from core TemporaryPasswordPrimaryAuthenticationProvider.
49 *
50 * A successful login will force a password reset.
51 *
52 * @note For proper operation, this should generally come before any other
53 *  password-based authentication providers
54 *  (especially the core TemporaryPasswordPrimaryAuthenticationProvider).
55 */
56class CentralAuthTemporaryPasswordPrimaryAuthenticationProvider
57    extends AbstractTemporaryPasswordPrimaryAuthenticationProvider
58{
59
60    private Emailer $emailer;
61    private LanguageNameUtils $languageNameUtils;
62    private UserIdentityLookup $userIdentityLookup;
63    private CentralAuthDatabaseManager $databaseManager;
64    private CentralAuthUtilityService $utilityService;
65    private SharedDomainUtils $sharedDomainUtils;
66
67    public function __construct(
68        IConnectionProvider $dbProvider,
69        Emailer $emailer,
70        LanguageNameUtils $languageNameUtils,
71        UserIdentityLookup $userIdentityLookup,
72        UserOptionsLookup $userOptionsLookup,
73        CentralAuthDatabaseManager $databaseManager,
74        CentralAuthUtilityService $utilityService,
75        SharedDomainUtils $sharedDomainUtils,
76        array $params = []
77    ) {
78        parent::__construct( $dbProvider, $userOptionsLookup, $params );
79        $this->emailer = $emailer;
80        $this->languageNameUtils = $languageNameUtils;
81        $this->userIdentityLookup = $userIdentityLookup;
82        $this->databaseManager = $databaseManager;
83        $this->utilityService = $utilityService;
84        $this->sharedDomainUtils = $sharedDomainUtils;
85    }
86
87    /** @inheritDoc */
88    public function testUserExists( $username, $flags = IDBAccessObject::READ_NORMAL ) {
89        $username = $this->userNameUtils->getCanonical( $username, UserNameUtils::RIGOR_USABLE );
90        if ( $username === false ) {
91            return false;
92        }
93
94        $centralUser = CentralAuthUser::getInstanceByName( $username );
95        return $centralUser->exists();
96    }
97
98    /** @inheritDoc */
99    public function getAuthenticationRequests( $action, array $options ) {
100        if ( $this->sharedDomainUtils->isSul3Enabled( $this->manager->getRequest() )
101            && !$this->sharedDomainUtils->isSharedDomain()
102            && in_array( $action, [ AuthManager::ACTION_LOGIN, AuthManager::ACTION_CREATE ], true )
103        ) {
104            return [];
105        }
106
107        return parent::getAuthenticationRequests( $action, $options );
108    }
109
110    /** @inheritDoc */
111    protected function getTemporaryPassword( string $username, $flags = IDBAccessObject::READ_NORMAL ): array {
112        // Only allow central accounts with nothing weird going on.
113        $centralUser = CentralAuthUser::getInstanceByName( $username );
114        if ( !$centralUser->exists() || $centralUser->canAuthenticate() !== true ) {
115            return [ null, null ];
116        }
117        $localUser = $this->userIdentityLookup->getUserIdentityByName( $username );
118        if ( !$centralUser->isAttached() && $localUser && $localUser->isRegistered() ) {
119            return [ null, null ];
120        }
121
122        $db = $this->databaseManager->getCentralDBFromRecency( $flags );
123        $row = $db->newSelectQueryBuilder()
124            ->select( [ 'gu_password_reset_key', 'gu_password_reset_expiration' ] )
125            ->from( 'globaluser' )
126            ->where( [ 'gu_name' => $username ] )
127            ->recency( $flags )
128            ->caller( __METHOD__ )->fetchRow();
129
130        if ( !$row ) {
131            return [ null, null ];
132        }
133
134        return [
135            $this->getPassword( $row->gu_password_reset_key ),
136            $row->gu_password_reset_expiration,
137        ];
138    }
139
140    /** @inheritDoc */
141    protected function setTemporaryPassword( string $username, Password $tempPassHash, $tempPassTime ): void {
142        $db = $this->databaseManager->getCentralPrimaryDB();
143        $db->newUpdateQueryBuilder()
144            ->update( 'globaluser' )
145            ->set( [
146                'gu_password_reset_key' => $tempPassHash->toString(),
147                'gu_password_reset_expiration' => $db->timestampOrNull( $tempPassTime ),
148            ] )
149            ->where( [ 'gu_name' => $username ] )
150            ->caller( __METHOD__ )->execute();
151    }
152
153    /** @inheritDoc */
154    public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) {
155        /** @var TemporaryPasswordAuthenticationRequest $req */
156        $req = AuthenticationRequest::getRequestByClass(
157            $reqs, TemporaryPasswordAuthenticationRequest::class
158        );
159        if ( $req && $req->username !== null && $req->password !== null ) {
160            $centralUser = CentralAuthUser::getPrimaryInstance( $user );
161            if ( $centralUser->exists() ) {
162                return AuthenticationResponse::newFail(
163                    wfMessage( 'centralauth-account-exists' )
164                );
165            }
166            if ( $centralUser->listUnattached() ) {
167                // $this->testUserForCreation() will already have rejected it if necessary
168                return AuthenticationResponse::newAbstain();
169            }
170            // Username is unused; set up as a global account
171            if ( !$centralUser->register( $req->password, $user->getEmail() ) ) {
172                // Wha?
173                return AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
174            }
175        }
176        return parent::beginPrimaryAccountCreation( $user, $creator, $reqs );
177    }
178
179    /** @inheritDoc */
180    public function finishAccountCreation( $user, $creator, AuthenticationResponse $res ) {
181        $ret = parent::finishAccountCreation( $user, $creator, $res );
182
183        $centralUser = CentralAuthUser::getPrimaryInstance( $user );
184        // Do the attach in finishAccountCreation instead of begin because now the user has been
185        // added to database and local ID exists (which is needed in attach)
186        $centralUser->attach( WikiMap::getCurrentWikiId(), 'new' );
187        $this->databaseManager->getCentralPrimaryDB()->onTransactionCommitOrIdle(
188            function () use ( $centralUser ) {
189                $this->utilityService->scheduleCreationJobs( $centralUser );
190            },
191            __METHOD__
192        );
193
194        return $ret;
195    }
196
197    /** @inheritDoc */
198    protected function maybeSendPasswordResetEmail( TemporaryPasswordAuthenticationRequest $req ): void {
199        // Send email after DB commit (the callback does not run in case of DB rollback)
200        $this->databaseManager->getCentralPrimaryDB()->onTransactionCommitOrIdle(
201            function () use ( $req ) {
202                $this->sendPasswordResetEmail( $req );
203            },
204            __METHOD__
205        );
206    }
207
208    /** @inheritDoc */
209    protected function sendPasswordResetEmail( TemporaryPasswordAuthenticationRequest $req ): void {
210        global $wgConf;
211        $user = User::newFromName( $req->username );
212        if ( !$user ) {
213            return;
214        }
215        if ( $user->isRegistered() ) {
216            parent::sendPasswordResetEmail( $req );
217            return;
218        }
219
220        // Hint that the user can choose to require email address to request a temporary password.
221        // Since the local user doesn't exist, customize the email to refer to GlobalPreferences.
222        $centralUser = CentralAuthUser::getInstanceByName( $req->username );
223        $homewiki = $centralUser->getHomeWiki();
224        [ , $userLanguage ] = $wgConf->siteFromDB( $homewiki );
225        if (
226            !$userLanguage ||
227            !$this->languageNameUtils->isSupportedLanguage( $userLanguage )
228        ) {
229            $userLanguage = 'en';
230        }
231
232        $callerIsAnon = IPUtils::isValid( $req->caller );
233        $callerName = $callerIsAnon ? $req->caller : User::newFromName( $req->caller )->getName();
234        $passwordMessage = wfMessage( 'passwordreset-emailelement', $user->getName(),
235            $req->password )->inLanguage( $userLanguage );
236        $emailMessage = wfMessage( $callerIsAnon ? 'passwordreset-emailtext-ip'
237            : 'passwordreset-emailtext-user' )->inLanguage( $userLanguage );
238        $body = $emailMessage->params( $callerName, $passwordMessage->text(), 1,
239            '<' . Title::newMainPage()->getCanonicalURL() . '>',
240            round( $this->newPasswordExpiry / 86400 ) )->text();
241
242        if (
243            !$this->userOptionsLookup->getBoolOption( $user, 'requireemail' )
244        ) {
245            if ( ExtensionRegistry::getInstance()->isLoaded( 'GlobalPreferences' ) ) {
246                // Hint about global preferences if the local user doesn't exist
247                $centralUser = CentralAuthUser::getInstanceByName( $req->username );
248                $homewiki = $centralUser->getHomeWiki();
249                if ( $homewiki ) {
250                    $url = WikiMap::getForeignURL( $homewiki,
251                        'Special:GlobalPreferences', 'mw-prefsection-personal-email' );
252                    if ( $url ) {
253                        $body .= "\n\n" . wfMessage( 'passwordreset-emailtext-require-email' )
254                                ->inLanguage( $userLanguage )
255                                ->params( "<$url>" )
256                                ->text();
257                    }
258                }
259            }
260        }
261
262        $subject = wfMessage( 'passwordreset-emailtitle' )->inLanguage( $userLanguage )->text();
263
264        $passwordSender = $this->config->get( MainConfigNames::PasswordSender );
265        $sender = new MailAddress( $passwordSender,
266            wfMessage( 'emailsender' )->inContentLanguage()->text() );
267
268        $to = new MailAddress(
269            $centralUser->getEmail(),
270            $centralUser->getName(),
271            // No getRealName() / user_real_name equivalent for CentralUser
272            null
273        );
274
275        $this->emailer->send(
276            $to,
277            $sender,
278            $subject,
279            $body,
280        );
281    }
282}