Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
9.77% |
21 / 215 |
|
7.41% |
2 / 27 |
CRAP | |
0.00% |
0 / 1 |
CentralAuthHooks | |
9.77% |
21 / 215 |
|
7.41% |
2 / 27 |
6725.36 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
onRegistration | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
56 | |||
onGetPreferences | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
72 | |||
onSpecialPasswordResetOnSubmit | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
132 | |||
onAuthManagerFilterProviders | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getAuthIconHtml | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
42 | |||
getAutoLoginWikis | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
isMobileDomain | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
onUserArrayFromResult | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
onUserGetEmail | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
3.33 | |||
onUserGetEmailAuthenticationTimestamp | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
onInvalidateEmailComplete | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
onUserSetEmail | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
onUserSaveSettings | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
onUserSetEmailAuthenticationTimestamp | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
onUserGetRights | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
6 | |||
onUserIsLocked | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
onUserIsBot | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
56 | |||
onMakeGlobalVariablesScript | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
getCentralautologinJsData | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
getEdgeLoginHTML | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
onTestCanonicalRedirect | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
onUserGetReservedNames | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
onApiQueryTokensRegisterTypes | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
onResourceLoaderForeignApiModules | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onSessionCheckInfo | |
42.86% |
3 / 7 |
|
0.00% |
0 / 1 |
4.68 | |||
onGetLogTypesOnUser | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
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 | */ |
20 | |
21 | namespace MediaWiki\Extension\CentralAuth; |
22 | |
23 | use CentralAuthSessionProvider; |
24 | use MediaWiki\Api\Hook\ApiQueryTokensRegisterTypesHook; |
25 | use MediaWiki\Auth\Hook\AuthManagerFilterProvidersHook; |
26 | use MediaWiki\Auth\TemporaryPasswordPrimaryAuthenticationProvider; |
27 | use MediaWiki\Config\Config; |
28 | use MediaWiki\Extension\CentralAuth\Config\CAMainConfigNames; |
29 | use MediaWiki\Extension\CentralAuth\Hooks\Handlers\PageDisplayHookHandler; |
30 | use MediaWiki\Extension\CentralAuth\Special\SpecialCentralAutoLogin; |
31 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
32 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUserArrayFromResult; |
33 | use MediaWiki\Hook\GetLogTypesOnUserHook; |
34 | use MediaWiki\Hook\TestCanonicalRedirectHook; |
35 | use MediaWiki\Html\Html; |
36 | use MediaWiki\MediaWikiServices; |
37 | use MediaWiki\Output\Hook\MakeGlobalVariablesScriptHook; |
38 | use MediaWiki\Output\OutputPage; |
39 | use MediaWiki\Permissions\Hook\UserGetRightsHook; |
40 | use MediaWiki\Preferences\Hook\GetPreferencesHook; |
41 | use MediaWiki\Registration\ExtensionRegistry; |
42 | use MediaWiki\Request\ContentSecurityPolicy; |
43 | use MediaWiki\Request\WebRequest; |
44 | use MediaWiki\ResourceLoader as RL; |
45 | use MediaWiki\ResourceLoader\Hook\ResourceLoaderForeignApiModulesHook; |
46 | use MediaWiki\Session\CookieSessionProvider; |
47 | use MediaWiki\Session\Hook\SessionCheckInfoHook; |
48 | use MediaWiki\Session\SessionInfo; |
49 | use MediaWiki\SpecialPage\SpecialPage; |
50 | use MediaWiki\Title\Title; |
51 | use MediaWiki\User\Hook\InvalidateEmailCompleteHook; |
52 | use MediaWiki\User\Hook\SpecialPasswordResetOnSubmitHook; |
53 | use MediaWiki\User\Hook\UserArrayFromResultHook; |
54 | use MediaWiki\User\Hook\UserGetEmailAuthenticationTimestampHook; |
55 | use MediaWiki\User\Hook\UserGetEmailHook; |
56 | use MediaWiki\User\Hook\UserGetReservedNamesHook; |
57 | use MediaWiki\User\Hook\UserIsBotHook; |
58 | use MediaWiki\User\Hook\UserIsLockedHook; |
59 | use MediaWiki\User\Hook\UserSaveSettingsHook; |
60 | use MediaWiki\User\Hook\UserSetEmailAuthenticationTimestampHook; |
61 | use MediaWiki\User\Hook\UserSetEmailHook; |
62 | use MediaWiki\User\Options\UserOptionsLookup; |
63 | use MediaWiki\User\User; |
64 | use MediaWiki\User\UserArrayFromResult; |
65 | use MediaWiki\User\UserNameUtils; |
66 | use MediaWiki\WikiMap\WikiMap; |
67 | use MobileContext; |
68 | use OOUI\ButtonWidget; |
69 | use OOUI\HorizontalLayout; |
70 | use OOUI\IconWidget; |
71 | use Wikimedia\Rdbms\IResultWrapper; |
72 | |
73 | class CentralAuthHooks implements |
74 | ApiQueryTokensRegisterTypesHook, |
75 | AuthManagerFilterProvidersHook, |
76 | MakeGlobalVariablesScriptHook, |
77 | TestCanonicalRedirectHook, |
78 | UserGetRightsHook, |
79 | GetPreferencesHook, |
80 | ResourceLoaderForeignApiModulesHook, |
81 | SessionCheckInfoHook, |
82 | GetLogTypesOnUserHook, |
83 | InvalidateEmailCompleteHook, |
84 | SpecialPasswordResetOnSubmitHook, |
85 | UserArrayFromResultHook, |
86 | UserGetEmailAuthenticationTimestampHook, |
87 | UserGetEmailHook, |
88 | UserGetReservedNamesHook, |
89 | UserIsBotHook, |
90 | UserIsLockedHook, |
91 | UserSaveSettingsHook, |
92 | UserSetEmailAuthenticationTimestampHook, |
93 | UserSetEmailHook |
94 | { |
95 | |
96 | public const BACKFILL_ACCOUNT_CREATOR = "MediaWikiAccountBackfiller"; |
97 | |
98 | private Config $config; |
99 | private UserNameUtils $userNameUtils; |
100 | private UserOptionsLookup $userOptionsLookup; |
101 | |
102 | public function __construct( |
103 | Config $config, |
104 | UserNameUtils $userNameUtils, |
105 | UserOptionsLookup $userOptionsLookup |
106 | ) { |
107 | $this->config = $config; |
108 | $this->userNameUtils = $userNameUtils; |
109 | $this->userOptionsLookup = $userOptionsLookup; |
110 | } |
111 | |
112 | /** |
113 | * Called right after configuration variables have been set. |
114 | */ |
115 | public static function onRegistration() { |
116 | global $wgCentralAuthDatabase, $wgSessionProviders, |
117 | $wgCentralIdLookupProvider, $wgVirtualDomainsMapping; |
118 | |
119 | if ( |
120 | // Test against the local database |
121 | defined( 'MW_PHPUNIT_TEST' ) |
122 | // Install tables to the local database in CI |
123 | // TODO: configure this in CI |
124 | || defined( 'MW_QUIBBLE_CI' ) |
125 | ) { |
126 | $wgCentralAuthDatabase = false; |
127 | unset( $wgVirtualDomainsMapping['virtual-centralauth'] ); |
128 | } else { |
129 | if ( !isset( $wgVirtualDomainsMapping['virtual-centralauth'] ) ) { |
130 | $wgVirtualDomainsMapping['virtual-centralauth'] = [ 'db' => $wgCentralAuthDatabase ?? false ]; |
131 | } |
132 | } |
133 | |
134 | // CentralAuthSessionProvider is supposed to replace core |
135 | // CookieSessionProvider, so remove the latter if both are configured |
136 | if ( isset( $wgSessionProviders[CookieSessionProvider::class] ) && |
137 | isset( $wgSessionProviders[CentralAuthSessionProvider::class] ) |
138 | ) { |
139 | unset( $wgSessionProviders[CookieSessionProvider::class] ); |
140 | } |
141 | |
142 | // Assume they want CentralAuth as the default central ID provider, unless |
143 | // already configured otherwise. |
144 | if ( $wgCentralIdLookupProvider === 'local' ) { |
145 | $wgCentralIdLookupProvider = 'CentralAuth'; |
146 | } |
147 | |
148 | // The prefix for the constant is the numbers 6765, which are the ASCII codes for "C" and "A" to stand |
149 | // for CentralAuth. This method is used here, like in the FlaggedRevs extension, to ensure that the |
150 | // constant is not used by any other APCOND. |
151 | define( 'APCOND_CA_INGLOBALGROUPS', 67651 ); |
152 | } |
153 | |
154 | /** |
155 | * Add a little pretty to the preferences user info section |
156 | * |
157 | * @param User $user |
158 | * @param array &$preferences |
159 | * @return bool |
160 | */ |
161 | public function onGetPreferences( $user, &$preferences ) { |
162 | // Possible states: |
163 | // - account not merged at all |
164 | // - global accounts exists, but this local account is unattached |
165 | // - this local account is attached, but migration incomplete |
166 | // - all local accounts are attached (no $message shown) |
167 | |
168 | $global = CentralAuthUser::getInstance( $user ); |
169 | $unattached = count( $global->listUnattached() ); |
170 | if ( $global->exists() ) { |
171 | if ( $global->isAttached() && $unattached ) { |
172 | // Migration incomplete - unattached accounts at other wikis |
173 | $attached = count( $global->listAttached() ); |
174 | $message = wfMessage( 'centralauth-prefs-unattached' )->parse() . |
175 | '<br />' . |
176 | wfMessage( 'centralauth-prefs-count-attached' ) |
177 | ->numParams( $attached )->parse() . |
178 | '<br />' . |
179 | wfMessage( 'centralauth-prefs-count-unattached' ) |
180 | ->numParams( $unattached )->parse(); |
181 | } elseif ( !$global->isAttached() ) { |
182 | // Global account exists but the local account is not attached |
183 | $message = wfMessage( 'centralauth-prefs-detail-unattached' )->parse(); |
184 | } |
185 | } else { |
186 | // No global account |
187 | $message = wfMessage( 'centralauth-prefs-not-managed' )->parse(); |
188 | } |
189 | |
190 | $manageButtons = []; |
191 | |
192 | if ( $unattached && $user->isAllowed( 'centralauth-merge' ) ) { |
193 | // Add "Manage your global account" button |
194 | $manageButtons[] = new ButtonWidget( [ |
195 | 'href' => SpecialPage::getTitleFor( 'MergeAccount' )->getLinkURL(), |
196 | 'label' => wfMessage( 'centralauth-prefs-manage' )->text(), |
197 | ] ); |
198 | } |
199 | |
200 | // Add "View your global account info" button |
201 | $manageButtons[] = new ButtonWidget( [ |
202 | 'href' => SpecialPage::getTitleFor( 'CentralAuth', $user->getName() )->getLinkURL(), |
203 | 'label' => wfMessage( 'centralauth-prefs-view' )->text(), |
204 | ] ); |
205 | |
206 | $manageLinkList = (string)( new HorizontalLayout( [ 'items' => $manageButtons ] ) ); |
207 | |
208 | $preferences['globalaccountstatus'] = [ |
209 | 'section' => 'personal/info', |
210 | 'label-message' => 'centralauth-prefs-status', |
211 | 'type' => 'info', |
212 | 'raw' => true, |
213 | 'default' => $manageLinkList |
214 | ]; |
215 | |
216 | // Display a notice about the user account status with an alert icon |
217 | if ( isset( $message ) ) { |
218 | $messageIconWidget = (string)new IconWidget( [ |
219 | 'icon' => 'alert', |
220 | ] ); |
221 | $preferences['globalaccountstatus']['default'] = $messageIconWidget |
222 | . "$message<br>$manageLinkList"; |
223 | } |
224 | |
225 | return true; |
226 | } |
227 | |
228 | /** |
229 | * Allow password reset when the user account does not exist on the local wiki, but |
230 | * does exist globally |
231 | * |
232 | * @param User[] &$users |
233 | * @param array $data |
234 | * @param string &$error |
235 | * @return bool |
236 | */ |
237 | public function onSpecialPasswordResetOnSubmit( &$users, $data, &$error ) { |
238 | $usersByName = []; |
239 | foreach ( $users as $user ) { |
240 | $usersByName[ $user->getName() ] = $user; |
241 | } |
242 | |
243 | if ( $data['Username'] !== null ) { |
244 | // PasswordReset ensures that the username is valid before calling the hook. |
245 | // CentralAuthUser canonicalizes the provided username. |
246 | $centralUser = CentralAuthUser::getInstanceByName( $data['Username'] ); |
247 | if ( $centralUser->exists() && $centralUser->getEmail() ) { |
248 | // User that does not exist locally is okay, if it exists globally |
249 | // TODO: Use UserIdentity instead, once allowed by the hook |
250 | $user = User::newFromName( $centralUser->getName() ); |
251 | if ( |
252 | !$this->userOptionsLookup->getBoolOption( $user, 'requireemail' ) || |
253 | $centralUser->getEmail() === $data['Email'] |
254 | ) { |
255 | // Email is not required to request a reset, or the correct email was provided |
256 | $usersByName[ $centralUser->getName() ] ??= $user; |
257 | } |
258 | } |
259 | |
260 | } elseif ( $data['Email'] !== null ) { |
261 | // PasswordReset ensures that the email is valid before calling the hook. |
262 | /** @var iterable<CentralAuthUser> $centralUsers */ |
263 | $centralUsers = CentralAuthServices::getGlobalUserSelectQueryBuilderFactory() |
264 | ->newGlobalUserSelectQueryBuilder() |
265 | ->where( [ 'gu_email' => $data['Email'] ] ) |
266 | ->caller( __METHOD__ ) |
267 | ->fetchCentralAuthUsers(); |
268 | |
269 | foreach ( $centralUsers as $centralUser ) { |
270 | if ( isset( $usersByName[ $centralUser->getName() ] ) ) { |
271 | continue; |
272 | } |
273 | $localUser = User::newFromName( $centralUser->getName() ); |
274 | |
275 | // Skip users whose preference 'requireemail' is on since username was not submitted |
276 | // (If the local user doesn't exist, the preference is looked up in GlobalPreferences, |
277 | // to ensure users can't be harassed with password resets coming from other wikis) |
278 | if ( $this->userOptionsLookup->getBoolOption( $localUser, 'requireemail' ) ) { |
279 | continue; |
280 | } |
281 | |
282 | $usersByName[ $centralUser->getName() ] = $localUser; |
283 | } |
284 | } |
285 | |
286 | $users = array_values( $usersByName ); |
287 | |
288 | return true; |
289 | } |
290 | |
291 | /** |
292 | * Disable core password reset if we're running in strict mode. |
293 | * This is independent of SUL3 (although we also do it in SUL3 mode). |
294 | * @inheritDoc |
295 | */ |
296 | public function onAuthManagerFilterProviders( array &$providers ): void { |
297 | if ( $this->config->get( CAMainConfigNames::CentralAuthStrict ) ) { |
298 | unset( $providers['primaryauth'][TemporaryPasswordPrimaryAuthenticationProvider::class] ); |
299 | } |
300 | } |
301 | |
302 | /** |
303 | * Get the HTML for an <img> element used to perform edge login, autologin (no-JS), or central logout. |
304 | * |
305 | * @param string $wikiID Target wiki |
306 | * @param string $page Target page, should be a Special:CentralAutoLogin subpage |
307 | * @param array $params URL query parameters. Some also affect the generated HTML: |
308 | * - 'type': when set to '1x1', generate an invisible pixel image, instead of a visible icon |
309 | * - 'mobile': when set, use target wiki's mobile domain URL instead of canonical URL |
310 | * @param ContentSecurityPolicy|null $csp If provided, it will be modified to allow requests to |
311 | * the target wiki. Otherwise, that must be done in 'ContentSecurityPolicyDefaultSource' hook. |
312 | * @return string HTML |
313 | */ |
314 | public static function getAuthIconHtml( |
315 | string $wikiID, string $page, array $params, ?ContentSecurityPolicy $csp |
316 | ): string { |
317 | // Use WikiMap to avoid localization of the 'Special' namespace, see T56195. |
318 | $wiki = WikiMap::getWiki( $wikiID ); |
319 | $url = wfAppendQuery( |
320 | $wiki->getCanonicalUrl( $page ), |
321 | $params |
322 | ); |
323 | if ( isset( $params['mobile'] ) ) { |
324 | // Do autologin on the mobile domain for each wiki |
325 | $url = MobileContext::singleton()->getMobileUrl( $url ); |
326 | } |
327 | if ( $csp ) { |
328 | $csp->addDefaultSrc( wfParseUrl( $url )['host'] ); |
329 | } |
330 | |
331 | $type = $params['type']; |
332 | return Html::element( 'img', [ |
333 | 'src' => $url, |
334 | 'alt' => '', |
335 | 'width' => $type === '1x1' ? 1 : 20, |
336 | 'height' => $type === '1x1' ? 1 : 20, |
337 | 'style' => $type === '1x1' ? 'border: none; position: absolute;' : 'border: 1px solid #ccc;', |
338 | ] ); |
339 | } |
340 | |
341 | /** |
342 | * Get autologin wikis, in the same format as $wgCentralAuthAutoLoginWikis, but with the |
343 | * current domain removed. |
344 | * @return string[] |
345 | */ |
346 | public static function getAutoLoginWikis(): array { |
347 | global $wgServer, $wgCentralAuthAutoLoginWikis, $wgCentralAuthCookieDomain; |
348 | $autoLoginWikis = $wgCentralAuthAutoLoginWikis; |
349 | if ( $wgCentralAuthCookieDomain ) { |
350 | unset( $autoLoginWikis[$wgCentralAuthCookieDomain] ); |
351 | } else { |
352 | $serverParts = MediaWikiServices::getInstance()->getUrlUtils()->parse( $wgServer ); |
353 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
354 | unset( $autoLoginWikis[ $serverParts['host'] ] ); |
355 | } |
356 | return $autoLoginWikis; |
357 | } |
358 | |
359 | /** |
360 | * @return bool |
361 | */ |
362 | public static function isMobileDomain() { |
363 | return ExtensionRegistry::getInstance()->isLoaded( 'MobileFrontend' ) |
364 | && MobileContext::singleton()->usingMobileDomain(); |
365 | } |
366 | |
367 | /** |
368 | * @param UserArrayFromResult|null &$userArray |
369 | * @param IResultWrapper $res |
370 | * @return bool |
371 | */ |
372 | public function onUserArrayFromResult( &$userArray, $res ) { |
373 | $userArray = new CentralAuthUserArrayFromResult( $res ); |
374 | return true; |
375 | } |
376 | |
377 | /** |
378 | * @param User $user |
379 | * @param string &$email |
380 | * @return bool |
381 | */ |
382 | public function onUserGetEmail( $user, &$email ) { |
383 | if ( $this->userNameUtils->getCanonical( $user->getName() ) === false ) { |
384 | return true; |
385 | } |
386 | $ca = CentralAuthUser::getInstance( $user ); |
387 | if ( $ca->isAttached() ) { |
388 | $email = $ca->getEmail(); |
389 | } |
390 | return true; |
391 | } |
392 | |
393 | /** |
394 | * @param User $user |
395 | * @param string|null &$timestamp |
396 | * @return bool |
397 | */ |
398 | public function onUserGetEmailAuthenticationTimestamp( $user, &$timestamp ) { |
399 | $ca = CentralAuthUser::getInstance( $user ); |
400 | if ( $ca->isAttached() ) { |
401 | if ( $ca->isLocked() ) { |
402 | // Locked users shouldn't be receiving email (T87559) |
403 | $timestamp = null; |
404 | } else { |
405 | $timestamp = $ca->getEmailAuthenticationTimestamp(); |
406 | } |
407 | } |
408 | return true; |
409 | } |
410 | |
411 | /** |
412 | * @param User $user |
413 | * @return bool |
414 | */ |
415 | public function onInvalidateEmailComplete( $user ) { |
416 | $ca = CentralAuthUser::getPrimaryInstance( $user ); |
417 | if ( $ca->isAttached() ) { |
418 | $ca->setEmail( '' ); |
419 | $ca->setEmailAuthenticationTimestamp( null ); |
420 | $ca->saveSettings(); |
421 | } |
422 | return true; |
423 | } |
424 | |
425 | /** |
426 | * @param User $user |
427 | * @param string &$email |
428 | * @return bool |
429 | */ |
430 | public function onUserSetEmail( $user, &$email ) { |
431 | $ca = CentralAuthUser::getPrimaryInstance( $user ); |
432 | if ( $ca->isAttached() ) { |
433 | $ca->setEmail( $email ); |
434 | $ca->saveSettings(); |
435 | } |
436 | return true; |
437 | } |
438 | |
439 | /** |
440 | * @param User $user |
441 | * @return bool |
442 | */ |
443 | public function onUserSaveSettings( $user ) { |
444 | $ca = CentralAuthUser::getPrimaryInstance( $user ); |
445 | if ( $ca->isAttached() ) { |
446 | $ca->saveSettings(); |
447 | } |
448 | |
449 | return true; |
450 | } |
451 | |
452 | /** |
453 | * @param User $user |
454 | * @param ?string &$timestamp |
455 | * @return bool |
456 | */ |
457 | public function onUserSetEmailAuthenticationTimestamp( $user, &$timestamp ) { |
458 | $ca = CentralAuthUser::getInstance( $user ); |
459 | if ( $ca->isAttached() ) { |
460 | $latestCa = CentralAuthUser::newPrimaryInstanceFromId( $ca->getId() ); |
461 | if ( $latestCa->isAttached() ) { |
462 | $latestCa->setEmailAuthenticationTimestamp( $timestamp ); |
463 | $latestCa->saveSettings(); |
464 | } |
465 | } |
466 | |
467 | return true; |
468 | } |
469 | |
470 | /** |
471 | * @param User $user |
472 | * @param string[] &$rights |
473 | * @return bool |
474 | */ |
475 | public function onUserGetRights( $user, &$rights ) { |
476 | // checking rights not just for registered users but also for |
477 | // anon (local) users based on name only will allow autocreation of |
478 | // local account based on global rights, see T316303 |
479 | $anonUserOK = $this->config->get( CAMainConfigNames::CentralAuthStrict ); |
480 | if ( $this->userNameUtils->getCanonical( $user->getName() ) !== false ) { |
481 | $centralUser = CentralAuthUser::getInstance( $user ); |
482 | |
483 | if ( $centralUser->exists() |
484 | && ( $centralUser->isAttached() || ( $anonUserOK && !$user->isRegistered() ) ) ) { |
485 | $extraRights = $centralUser->getGlobalRights(); |
486 | |
487 | $rights = array_merge( $extraRights, $rights ); |
488 | } |
489 | } |
490 | |
491 | return true; |
492 | } |
493 | |
494 | /** |
495 | * @param User $user |
496 | * @param bool &$isLocked |
497 | * @return bool |
498 | */ |
499 | public function onUserIsLocked( $user, &$isLocked ) { |
500 | $centralUser = CentralAuthUser::getInstance( $user ); |
501 | if ( $centralUser->exists() |
502 | && ( $centralUser->isAttached() || !$user->isRegistered() ) |
503 | && $centralUser->isLocked() |
504 | ) { |
505 | $isLocked = true; |
506 | return false; |
507 | } |
508 | |
509 | return true; |
510 | } |
511 | |
512 | /** |
513 | * @param User $user |
514 | * @param bool &$isBot |
515 | * @return bool |
516 | */ |
517 | public function onUserIsBot( $user, &$isBot ) { |
518 | // No need to check global groups if the user is already marked as a bot, |
519 | // and no global groups for unregistered user |
520 | if ( !$isBot && $user->isRegistered() ) { |
521 | $centralUser = CentralAuthUser::getInstance( $user ); |
522 | if ( $centralUser->exists() |
523 | && $centralUser->isAttached() |
524 | && array_intersect( [ 'bot', 'global-bot' ], $centralUser->getGlobalGroups() ) |
525 | && in_array( 'bot', $centralUser->getGlobalRights() ) |
526 | ) { |
527 | $isBot = true; |
528 | } |
529 | } |
530 | |
531 | return true; |
532 | } |
533 | |
534 | /** |
535 | * @param array &$vars |
536 | * @param OutputPage $out |
537 | */ |
538 | public function onMakeGlobalVariablesScript( &$vars, $out ): void { |
539 | $user = $out->getUser(); |
540 | if ( $user->isRegistered() ) { |
541 | $centralUser = CentralAuthUser::getInstance( $user ); |
542 | $vars['wgGlobalGroups'] = ( $centralUser->exists() && $centralUser->isAttached() ) |
543 | ? $centralUser->getActiveGlobalGroups() |
544 | : []; |
545 | } |
546 | } |
547 | |
548 | /** |
549 | * Data to be serialised as JSON for the 'ext.centralauth.centralautologin' module. |
550 | * @return array |
551 | */ |
552 | public static function getCentralautologinJsData() { |
553 | global $wgCentralAuthLoginWiki; |
554 | $data = []; |
555 | |
556 | $wikiId = WikiMap::getCurrentWikiId(); |
557 | if ( $wgCentralAuthLoginWiki && $wgCentralAuthLoginWiki !== $wikiId ) { |
558 | $startUrl = WikiMap::getForeignURL( $wikiId, 'Special:CentralAutoLogin/start' ); |
559 | |
560 | if ( $startUrl !== false ) { |
561 | $params = [ 'type' => 'script' ]; |
562 | if ( self::isMobileDomain() ) { |
563 | $params['mobile'] = 1; |
564 | } |
565 | $data['startURL'] = wfAppendQuery( $startUrl, $params ); |
566 | } |
567 | } |
568 | |
569 | return $data; |
570 | } |
571 | |
572 | /** |
573 | * Get a HTML fragment that will trigger central autologin, i.e. try to log in the user on |
574 | * each of $wgCentralAuthAutoLoginWikis in the background by embedding invisible pixel images |
575 | * which point to Special:CentralAutoLogin on each of those wikis. |
576 | * |
577 | * It also calls Special:CentralAutoLogin/refreshCookies on the central wiki, to refresh |
578 | * central session cookies if needed (e.g. because the "remember me" setting changed). |
579 | * |
580 | * This is typically used on the next page view after a successful login (by setting the |
581 | * CentralAuthDoEdgeLogin session flag). |
582 | * |
583 | * @return string |
584 | * |
585 | * @see SpecialCentralAutoLogin |
586 | * @see PageDisplayHookHandler::onBeforePageDisplay() |
587 | */ |
588 | public static function getEdgeLoginHTML() { |
589 | global $wgCentralAuthLoginWiki; |
590 | |
591 | $html = ''; |
592 | |
593 | foreach ( self::getAutoLoginWikis() as $domain => $wikiID ) { |
594 | $params = [ |
595 | 'type' => '1x1', |
596 | 'from' => WikiMap::getCurrentWikiId(), |
597 | ]; |
598 | if ( self::isMobileDomain() ) { |
599 | $params['mobile'] = 1; |
600 | } |
601 | $html .= self::getAuthIconHtml( $wikiID, 'Special:CentralAutoLogin/start', $params, null ); |
602 | } |
603 | |
604 | if ( $wgCentralAuthLoginWiki ) { |
605 | $html .= self::getAuthIconHtml( $wgCentralAuthLoginWiki, 'Special:CentralAutoLogin/refreshCookies', [ |
606 | 'type' => '1x1', |
607 | 'wikiid' => WikiMap::getCurrentWikiId(), |
608 | ], null ); |
609 | } |
610 | |
611 | return $html; |
612 | } |
613 | |
614 | /** |
615 | * Prevent "canonicalization" of Special:CentralAutoLogin to a localized |
616 | * Special namespace name. See T56195. |
617 | * @param WebRequest $request |
618 | * @param Title $title |
619 | * @param OutputPage $output |
620 | * @return bool |
621 | */ |
622 | public function onTestCanonicalRedirect( $request, $title, $output ) { |
623 | return $title->getNamespace() !== NS_SPECIAL || |
624 | !str_starts_with( $request->getVal( 'title', '' ), 'Special:CentralAutoLogin/' ); |
625 | } |
626 | |
627 | /** |
628 | * Handler for UserGetReservedNames |
629 | * @param array &$reservedUsernames |
630 | */ |
631 | public function onUserGetReservedNames( &$reservedUsernames ) { |
632 | $reservedUsernames[] = 'Global rename script'; |
633 | $reservedUsernames[] = self::BACKFILL_ACCOUNT_CREATOR; |
634 | } |
635 | |
636 | /** |
637 | * @param array &$salts |
638 | * @return bool |
639 | */ |
640 | public function onApiQueryTokensRegisterTypes( &$salts ) { |
641 | $salts += [ |
642 | 'setglobalaccountstatus' => 'setglobalaccountstatus', |
643 | 'deleteglobalaccount' => 'deleteglobalaccount', |
644 | ]; |
645 | return true; |
646 | } |
647 | |
648 | /** |
649 | * @param string[] &$dependencies |
650 | * @param RL\Context|null $context |
651 | * @return void |
652 | */ |
653 | public function onResourceLoaderForeignApiModules( |
654 | &$dependencies, |
655 | $context = null |
656 | ): void { |
657 | $dependencies[] = 'ext.centralauth.ForeignApi'; |
658 | } |
659 | |
660 | /** |
661 | * Hook function to prevent logged-in sessions when a user is being |
662 | * renamed. |
663 | * @param string &$reason Failure reason to log |
664 | * @param SessionInfo $info |
665 | * @param WebRequest $request |
666 | * @param array|bool $metadata |
667 | * @param array|bool $data |
668 | * @return bool |
669 | */ |
670 | public function onSessionCheckInfo( |
671 | &$reason, |
672 | $info, |
673 | $request, |
674 | $metadata, |
675 | $data |
676 | ) { |
677 | $name = $info->getUserInfo()->getName(); |
678 | if ( $name !== null ) { |
679 | $centralUser = CentralAuthUser::getInstanceByName( $name ); |
680 | if ( $centralUser->renameInProgress() ) { |
681 | $reason = 'CentralAuth rename in progress'; |
682 | return false; |
683 | } |
684 | } |
685 | return true; |
686 | } |
687 | |
688 | /** |
689 | * @param array &$types |
690 | */ |
691 | public function onGetLogTypesOnUser( &$types ) { |
692 | $types[] = 'gblrights'; |
693 | } |
694 | } |