Merge "Convert Special:DeletedContributions to use OOUI."
[lhc/web/wiklou.git] / includes / auth / AuthPluginPrimaryAuthenticationProvider.php
1 <?php
2 /**
3 * Primary authentication provider wrapper for AuthPlugin
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Auth
22 */
23
24 namespace MediaWiki\Auth;
25
26 use AuthPlugin;
27 use User;
28
29 /**
30 * Primary authentication provider wrapper for AuthPlugin
31 * @warning If anything depends on the wrapped AuthPlugin being $wgAuth, it won't work with this!
32 * @ingroup Auth
33 * @since 1.27
34 * @deprecated since 1.27
35 */
36 class AuthPluginPrimaryAuthenticationProvider
37 extends AbstractPasswordPrimaryAuthenticationProvider
38 {
39 private $auth;
40 private $hasDomain;
41 private $requestType = null;
42
43 /**
44 * @param AuthPlugin $auth AuthPlugin to wrap
45 * @param string|null $requestType Class name of the
46 * PasswordAuthenticationRequest to use. If $auth->domainList() returns
47 * more than one domain, this must be a PasswordDomainAuthenticationRequest.
48 */
49 public function __construct( AuthPlugin $auth, $requestType = null ) {
50 parent::__construct();
51
52 if ( $auth instanceof AuthManagerAuthPlugin ) {
53 throw new \InvalidArgumentException(
54 'Trying to wrap AuthManagerAuthPlugin in AuthPluginPrimaryAuthenticationProvider ' .
55 'makes no sense.'
56 );
57 }
58
59 $need = count( $auth->domainList() ) > 1
60 ? PasswordDomainAuthenticationRequest::class
61 : PasswordAuthenticationRequest::class;
62 if ( $requestType === null ) {
63 $requestType = $need;
64 } elseif ( $requestType !== $need && !is_subclass_of( $requestType, $need ) ) {
65 throw new \InvalidArgumentException( "$requestType is not a $need" );
66 }
67
68 $this->auth = $auth;
69 $this->requestType = $requestType;
70 $this->hasDomain = (
71 $requestType === PasswordDomainAuthenticationRequest::class ||
72 is_subclass_of( $requestType, PasswordDomainAuthenticationRequest::class )
73 );
74 $this->authoritative = $auth->strict();
75
76 // Registering hooks from core is unusual, but is needed here to be
77 // able to call the AuthPlugin methods those hooks replace.
78 \Hooks::register( 'UserSaveSettings', [ $this, 'onUserSaveSettings' ] );
79 \Hooks::register( 'UserGroupsChanged', [ $this, 'onUserGroupsChanged' ] );
80 \Hooks::register( 'UserLoggedIn', [ $this, 'onUserLoggedIn' ] );
81 \Hooks::register( 'LocalUserCreated', [ $this, 'onLocalUserCreated' ] );
82 }
83
84 /**
85 * Create an appropriate AuthenticationRequest
86 * @return PasswordAuthenticationRequest
87 */
88 protected function makeAuthReq() {
89 $class = $this->requestType;
90 if ( $this->hasDomain ) {
91 return new $class( $this->auth->domainList() );
92 } else {
93 return new $class();
94 }
95 }
96
97 /**
98 * Call $this->auth->setDomain()
99 * @param PasswordAuthenticationRequest $req
100 */
101 protected function setDomain( $req ) {
102 if ( $this->hasDomain ) {
103 $domain = $req->domain;
104 } else {
105 // Just grab the first one.
106 $domainList = $this->auth->domainList();
107 $domain = reset( $domainList );
108 }
109
110 // Special:UserLogin does this. Strange.
111 if ( !$this->auth->validDomain( $domain ) ) {
112 $domain = $this->auth->getDomain();
113 }
114 $this->auth->setDomain( $domain );
115 }
116
117 /**
118 * Hook function to call AuthPlugin::updateExternalDB()
119 * @param User $user
120 * @codeCoverageIgnore
121 */
122 public function onUserSaveSettings( $user ) {
123 // No way to know the domain, just hope the provider handles that.
124 $this->auth->updateExternalDB( $user );
125 }
126
127 /**
128 * Hook function to call AuthPlugin::updateExternalDBGroups()
129 * @param User $user
130 * @param array $added
131 * @param array $removed
132 */
133 public function onUserGroupsChanged( $user, $added, $removed ) {
134 // No way to know the domain, just hope the provider handles that.
135 $this->auth->updateExternalDBGroups( $user, $added, $removed );
136 }
137
138 /**
139 * Hook function to call AuthPlugin::updateUser()
140 * @param User $user
141 */
142 public function onUserLoggedIn( $user ) {
143 $hookUser = $user;
144 // No way to know the domain, just hope the provider handles that.
145 $this->auth->updateUser( $hookUser );
146 if ( $hookUser !== $user ) {
147 throw new \UnexpectedValueException(
148 get_class( $this->auth ) . '::updateUser() tried to replace $user!'
149 );
150 }
151 }
152
153 /**
154 * Hook function to call AuthPlugin::initUser()
155 * @param User $user
156 * @param bool $autocreated
157 */
158 public function onLocalUserCreated( $user, $autocreated ) {
159 // For $autocreated, see self::autoCreatedAccount()
160 if ( !$autocreated ) {
161 $hookUser = $user;
162 // No way to know the domain, just hope the provider handles that.
163 $this->auth->initUser( $hookUser, $autocreated );
164 if ( $hookUser !== $user ) {
165 throw new \UnexpectedValueException(
166 get_class( $this->auth ) . '::initUser() tried to replace $user!'
167 );
168 }
169 }
170 }
171
172 public function getUniqueId() {
173 return parent::getUniqueId() . ':' . get_class( $this->auth );
174 }
175
176 public function getAuthenticationRequests( $action, array $options ) {
177 switch ( $action ) {
178 case AuthManager::ACTION_LOGIN:
179 case AuthManager::ACTION_CREATE:
180 return [ $this->makeAuthReq() ];
181
182 case AuthManager::ACTION_CHANGE:
183 case AuthManager::ACTION_REMOVE:
184 // No way to know the domain, just hope the provider handles that.
185 return $this->auth->allowPasswordChange() ? [ $this->makeAuthReq() ] : [];
186
187 default:
188 return [];
189 }
190 }
191
192 public function beginPrimaryAuthentication( array $reqs ) {
193 $req = AuthenticationRequest::getRequestByClass( $reqs, $this->requestType );
194 if ( !$req || $req->username === null || $req->password === null ||
195 ( $this->hasDomain && $req->domain === null )
196 ) {
197 return AuthenticationResponse::newAbstain();
198 }
199
200 $username = User::getCanonicalName( $req->username, 'usable' );
201 if ( $username === false ) {
202 return AuthenticationResponse::newAbstain();
203 }
204
205 $this->setDomain( $req );
206 if ( $this->testUserCanAuthenticateInternal( User::newFromName( $username ) ) &&
207 $this->auth->authenticate( $username, $req->password )
208 ) {
209 return AuthenticationResponse::newPass( $username );
210 } else {
211 $this->authoritative = $this->auth->strict() || $this->auth->strictUserAuth( $username );
212 return $this->failResponse( $req );
213 }
214 }
215
216 public function testUserCanAuthenticate( $username ) {
217 $username = User::getCanonicalName( $username, 'usable' );
218 if ( $username === false ) {
219 return false;
220 }
221
222 // We have to check every domain, because at least LdapAuthentication
223 // interprets AuthPlugin::userExists() as applying only to the current
224 // domain.
225 $curDomain = $this->auth->getDomain();
226 $domains = $this->auth->domainList() ?: [ '' ];
227 foreach ( $domains as $domain ) {
228 $this->auth->setDomain( $domain );
229 if ( $this->testUserCanAuthenticateInternal( User::newFromName( $username ) ) ) {
230 $this->auth->setDomain( $curDomain );
231 return true;
232 }
233 }
234 $this->auth->setDomain( $curDomain );
235 return false;
236 }
237
238 /**
239 * @see self::testUserCanAuthenticate
240 * @note The caller is responsible for calling $this->auth->setDomain()
241 * @param User $user
242 * @return bool
243 */
244 private function testUserCanAuthenticateInternal( $user ) {
245 if ( $this->auth->userExists( $user->getName() ) ) {
246 return !$this->auth->getUserInstance( $user )->isLocked();
247 } else {
248 return false;
249 }
250 }
251
252 public function providerRevokeAccessForUser( $username ) {
253 $username = User::getCanonicalName( $username, 'usable' );
254 if ( $username === false ) {
255 return;
256 }
257 $user = User::newFromName( $username );
258 if ( $user ) {
259 // Reset the password on every domain.
260 $curDomain = $this->auth->getDomain();
261 $domains = $this->auth->domainList() ?: [ '' ];
262 $failed = [];
263 foreach ( $domains as $domain ) {
264 $this->auth->setDomain( $domain );
265 if ( $this->testUserCanAuthenticateInternal( $user ) &&
266 !$this->auth->setPassword( $user, null )
267 ) {
268 $failed[] = $domain === '' ? '(default)' : $domain;
269 }
270 }
271 $this->auth->setDomain( $curDomain );
272 if ( $failed ) {
273 throw new \UnexpectedValueException(
274 "AuthPlugin failed to reset password for $username in the following domains: "
275 . join( ' ', $failed )
276 );
277 }
278 }
279 }
280
281 public function testUserExists( $username, $flags = User::READ_NORMAL ) {
282 $username = User::getCanonicalName( $username, 'usable' );
283 if ( $username === false ) {
284 return false;
285 }
286
287 // We have to check every domain, because at least LdapAuthentication
288 // interprets AuthPlugin::userExists() as applying only to the current
289 // domain.
290 $curDomain = $this->auth->getDomain();
291 $domains = $this->auth->domainList() ?: [ '' ];
292 foreach ( $domains as $domain ) {
293 $this->auth->setDomain( $domain );
294 if ( $this->auth->userExists( $username ) ) {
295 $this->auth->setDomain( $curDomain );
296 return true;
297 }
298 }
299 $this->auth->setDomain( $curDomain );
300 return false;
301 }
302
303 public function providerAllowsPropertyChange( $property ) {
304 // No way to know the domain, just hope the provider handles that.
305 return $this->auth->allowPropChange( $property );
306 }
307
308 public function providerAllowsAuthenticationDataChange(
309 AuthenticationRequest $req, $checkData = true
310 ) {
311 if ( get_class( $req ) !== $this->requestType ) {
312 return \StatusValue::newGood( 'ignored' );
313 }
314
315 // Hope it works, AuthPlugin gives us no way to do this.
316 $curDomain = $this->auth->getDomain();
317 $this->setDomain( $req );
318 try {
319 // If !$checkData the domain might be wrong. Nothing we can do about that.
320 if ( !$this->auth->allowPasswordChange() ) {
321 return \StatusValue::newFatal( 'authmanager-authplugin-setpass-denied' );
322 }
323
324 if ( !$checkData ) {
325 return \StatusValue::newGood();
326 }
327
328 if ( $this->hasDomain ) {
329 if ( $req->domain === null ) {
330 return \StatusValue::newGood( 'ignored' );
331 }
332 if ( !$this->auth->validDomain( $req->domain ) ) {
333 return \StatusValue::newFatal( 'authmanager-authplugin-setpass-bad-domain' );
334 }
335 }
336
337 $username = User::getCanonicalName( $req->username, 'usable' );
338 if ( $username !== false ) {
339 $sv = \StatusValue::newGood();
340 if ( $req->password !== null ) {
341 if ( $req->password !== $req->retype ) {
342 $sv->fatal( 'badretype' );
343 } else {
344 $sv->merge( $this->checkPasswordValidity( $username, $req->password ) );
345 }
346 }
347 return $sv;
348 } else {
349 return \StatusValue::newGood( 'ignored' );
350 }
351 } finally {
352 $this->auth->setDomain( $curDomain );
353 }
354 }
355
356 public function providerChangeAuthenticationData( AuthenticationRequest $req ) {
357 if ( get_class( $req ) === $this->requestType ) {
358 $username = $req->username !== null ? User::getCanonicalName( $req->username, 'usable' ) : false;
359 if ( $username === false ) {
360 return;
361 }
362
363 if ( $this->hasDomain && $req->domain === null ) {
364 return;
365 }
366
367 $this->setDomain( $req );
368 $user = User::newFromName( $username );
369 if ( !$this->auth->setPassword( $user, $req->password ) ) {
370 // This is totally unfriendly and leaves other
371 // AuthenticationProviders in an uncertain state, but what else
372 // can we do?
373 throw new \ErrorPageError(
374 'authmanager-authplugin-setpass-failed-title',
375 'authmanager-authplugin-setpass-failed-message'
376 );
377 }
378 }
379 }
380
381 public function accountCreationType() {
382 // No way to know the domain, just hope the provider handles that.
383 return $this->auth->canCreateAccounts() ? self::TYPE_CREATE : self::TYPE_NONE;
384 }
385
386 public function testForAccountCreation( $user, $creator, array $reqs ) {
387 return \StatusValue::newGood();
388 }
389
390 public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) {
391 if ( $this->accountCreationType() === self::TYPE_NONE ) {
392 throw new \BadMethodCallException( 'Shouldn\'t call this when accountCreationType() is NONE' );
393 }
394
395 $req = AuthenticationRequest::getRequestByClass( $reqs, $this->requestType );
396 if ( !$req || $req->username === null || $req->password === null ||
397 ( $this->hasDomain && $req->domain === null )
398 ) {
399 return AuthenticationResponse::newAbstain();
400 }
401
402 $username = User::getCanonicalName( $req->username, 'usable' );
403 if ( $username === false ) {
404 return AuthenticationResponse::newAbstain();
405 }
406
407 $this->setDomain( $req );
408 if ( $this->auth->addUser(
409 $user, $req->password, $user->getEmail(), $user->getRealName()
410 ) ) {
411 return AuthenticationResponse::newPass();
412 } else {
413 return AuthenticationResponse::newFail(
414 new \Message( 'authmanager-authplugin-create-fail' )
415 );
416 }
417 }
418
419 public function autoCreatedAccount( $user, $source ) {
420 $hookUser = $user;
421 // No way to know the domain, just hope the provider handles that.
422 $this->auth->initUser( $hookUser, true );
423 if ( $hookUser !== $user ) {
424 throw new \UnexpectedValueException(
425 get_class( $this->auth ) . '::initUser() tried to replace $user!'
426 );
427 }
428 }
429 }