Move Blob class to Rdbms namespaces
[mediawiki.git] / includes / auth / AuthPluginPrimaryAuthenticationProvider.php
blobb8e36bc4f3a2ed83a180f99346befc1c990c7c89
1 <?php
2 /**
3 * Primary authentication provider wrapper for AuthPlugin
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.
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.
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
20 * @file
21 * @ingroup Auth
24 namespace MediaWiki\Auth;
26 use AuthPlugin;
27 use User;
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
36 class AuthPluginPrimaryAuthenticationProvider
37 extends AbstractPasswordPrimaryAuthenticationProvider
39 private $auth;
40 private $hasDomain;
41 private $requestType = null;
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.
49 public function __construct( AuthPlugin $auth, $requestType = null ) {
50 parent::__construct();
52 if ( $auth instanceof AuthManagerAuthPlugin ) {
53 throw new \InvalidArgumentException(
54 'Trying to wrap AuthManagerAuthPlugin in AuthPluginPrimaryAuthenticationProvider ' .
55 'makes no sense.'
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" );
68 $this->auth = $auth;
69 $this->requestType = $requestType;
70 $this->hasDomain = (
71 $requestType === PasswordDomainAuthenticationRequest::class ||
72 is_subclass_of( $requestType, PasswordDomainAuthenticationRequest::class )
74 $this->authoritative = $auth->strict();
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' ] );
84 /**
85 * Create an appropriate AuthenticationRequest
86 * @return PasswordAuthenticationRequest
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();
97 /**
98 * Call $this->auth->setDomain()
99 * @param PasswordAuthenticationRequest $req
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 );
110 // Special:UserLogin does this. Strange.
111 if ( !$this->auth->validDomain( $domain ) ) {
112 $domain = $this->auth->getDomain();
114 $this->auth->setDomain( $domain );
118 * Hook function to call AuthPlugin::updateExternalDB()
119 * @param User $user
120 * @codeCoverageIgnore
122 public function onUserSaveSettings( $user ) {
123 // No way to know the domain, just hope the provider handles that.
124 $this->auth->updateExternalDB( $user );
128 * Hook function to call AuthPlugin::updateExternalDBGroups()
129 * @param User $user
130 * @param array $added
131 * @param array $removed
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 );
139 * Hook function to call AuthPlugin::updateUser()
140 * @param User $user
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!'
154 * Hook function to call AuthPlugin::initUser()
155 * @param User $user
156 * @param bool $autocreated
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!'
172 public function getUniqueId() {
173 return parent::getUniqueId() . ':' . get_class( $this->auth );
176 public function getAuthenticationRequests( $action, array $options ) {
177 switch ( $action ) {
178 case AuthManager::ACTION_LOGIN:
179 case AuthManager::ACTION_CREATE:
180 return [ $this->makeAuthReq() ];
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() ] : [];
187 default:
188 return [];
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 )
197 return AuthenticationResponse::newAbstain();
200 $username = User::getCanonicalName( $req->username, 'usable' );
201 if ( $username === false ) {
202 return AuthenticationResponse::newAbstain();
205 $this->setDomain( $req );
206 if ( $this->testUserCanAuthenticateInternal( User::newFromName( $username ) ) &&
207 $this->auth->authenticate( $username, $req->password )
209 return AuthenticationResponse::newPass( $username );
210 } else {
211 $this->authoritative = $this->auth->strict() || $this->auth->strictUserAuth( $username );
212 return $this->failResponse( $req );
216 public function testUserCanAuthenticate( $username ) {
217 $username = User::getCanonicalName( $username, 'usable' );
218 if ( $username === false ) {
219 return false;
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;
234 $this->auth->setDomain( $curDomain );
235 return false;
239 * @see self::testUserCanAuthenticate
240 * @note The caller is responsible for calling $this->auth->setDomain()
241 * @param User $user
242 * @return bool
244 private function testUserCanAuthenticateInternal( $user ) {
245 if ( $this->auth->userExists( $user->getName() ) ) {
246 return !$this->auth->getUserInstance( $user )->isLocked();
247 } else {
248 return false;
252 public function providerRevokeAccessForUser( $username ) {
253 $username = User::getCanonicalName( $username, 'usable' );
254 if ( $username === false ) {
255 return;
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 )
268 $failed[] = $domain === '' ? '(default)' : $domain;
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 )
281 public function testUserExists( $username, $flags = User::READ_NORMAL ) {
282 $username = User::getCanonicalName( $username, 'usable' );
283 if ( $username === false ) {
284 return false;
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;
299 $this->auth->setDomain( $curDomain );
300 return false;
303 public function providerAllowsPropertyChange( $property ) {
304 // No way to know the domain, just hope the provider handles that.
305 return $this->auth->allowPropChange( $property );
308 public function providerAllowsAuthenticationDataChange(
309 AuthenticationRequest $req, $checkData = true
311 if ( get_class( $req ) !== $this->requestType ) {
312 return \StatusValue::newGood( 'ignored' );
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' );
324 if ( !$checkData ) {
325 return \StatusValue::newGood();
328 if ( $this->hasDomain ) {
329 if ( $req->domain === null ) {
330 return \StatusValue::newGood( 'ignored' );
332 if ( !$this->auth->validDomain( $req->domain ) ) {
333 return \StatusValue::newFatal( 'authmanager-authplugin-setpass-bad-domain' );
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 ) );
347 return $sv;
348 } else {
349 return \StatusValue::newGood( 'ignored' );
351 } finally {
352 $this->auth->setDomain( $curDomain );
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;
363 if ( $this->hasDomain && $req->domain === null ) {
364 return;
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'
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;
386 public function testForAccountCreation( $user, $creator, array $reqs ) {
387 return \StatusValue::newGood();
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' );
395 $req = AuthenticationRequest::getRequestByClass( $reqs, $this->requestType );
396 if ( !$req || $req->username === null || $req->password === null ||
397 ( $this->hasDomain && $req->domain === null )
399 return AuthenticationResponse::newAbstain();
402 $username = User::getCanonicalName( $req->username, 'usable' );
403 if ( $username === false ) {
404 return AuthenticationResponse::newAbstain();
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' )
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!'