Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 123 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
CentralAuthTemporaryPasswordPrimaryAuthenticationProvider | |
0.00% |
0 / 123 |
|
0.00% |
0 / 9 |
1260 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
testUserExists | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getAuthenticationRequests | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
getTemporaryPassword | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
56 | |||
setTemporaryPassword | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
beginPrimaryAccountCreation | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
56 | |||
finishAccountCreation | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
maybeSendPasswordResetEmail | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
sendPasswordResetEmail | |
0.00% |
0 / 48 |
|
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 | |
22 | namespace MediaWiki\Extension\CentralAuth; |
23 | |
24 | use MailAddress; |
25 | use MediaWiki\Auth\AbstractTemporaryPasswordPrimaryAuthenticationProvider; |
26 | use MediaWiki\Auth\AuthenticationRequest; |
27 | use MediaWiki\Auth\AuthenticationResponse; |
28 | use MediaWiki\Auth\AuthManager; |
29 | use MediaWiki\Auth\TemporaryPasswordAuthenticationRequest; |
30 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
31 | use MediaWiki\Languages\LanguageNameUtils; |
32 | use MediaWiki\Mail\Emailer; |
33 | use MediaWiki\MainConfigNames; |
34 | use MediaWiki\Password\Password; |
35 | use MediaWiki\Registration\ExtensionRegistry; |
36 | use MediaWiki\Title\Title; |
37 | use MediaWiki\User\Options\UserOptionsLookup; |
38 | use MediaWiki\User\User; |
39 | use MediaWiki\User\UserIdentityLookup; |
40 | use MediaWiki\User\UserNameUtils; |
41 | use MediaWiki\WikiMap\WikiMap; |
42 | use Wikimedia\IPUtils; |
43 | use Wikimedia\Rdbms\IConnectionProvider; |
44 | use 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 | */ |
56 | class 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 | } |