Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
9.77% covered (danger)
9.77%
21 / 215
7.41% covered (danger)
7.41%
2 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
CentralAuthHooks
9.77% covered (danger)
9.77%
21 / 215
7.41% covered (danger)
7.41%
2 / 27
6725.36
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 onRegistration
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
56
 onGetPreferences
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
72
 onSpecialPasswordResetOnSubmit
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
132
 onAuthManagerFilterProviders
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getAuthIconHtml
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 getAutoLoginWikis
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 isMobileDomain
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 onUserArrayFromResult
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onUserGetEmail
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 onUserGetEmailAuthenticationTimestamp
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 onInvalidateEmailComplete
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 onUserSetEmail
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 onUserSaveSettings
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 onUserSetEmailAuthenticationTimestamp
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 onUserGetRights
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
 onUserIsLocked
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 onUserIsBot
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
56
 onMakeGlobalVariablesScript
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 getCentralautologinJsData
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 getEdgeLoginHTML
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 onTestCanonicalRedirect
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 onUserGetReservedNames
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onApiQueryTokensRegisterTypes
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 onResourceLoaderForeignApiModules
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onSessionCheckInfo
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
4.68
 onGetLogTypesOnUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
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
21namespace MediaWiki\Extension\CentralAuth;
22
23use CentralAuthSessionProvider;
24use MediaWiki\Api\Hook\ApiQueryTokensRegisterTypesHook;
25use MediaWiki\Auth\Hook\AuthManagerFilterProvidersHook;
26use MediaWiki\Auth\TemporaryPasswordPrimaryAuthenticationProvider;
27use MediaWiki\Config\Config;
28use MediaWiki\Extension\CentralAuth\Config\CAMainConfigNames;
29use MediaWiki\Extension\CentralAuth\Hooks\Handlers\PageDisplayHookHandler;
30use MediaWiki\Extension\CentralAuth\Special\SpecialCentralAutoLogin;
31use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
32use MediaWiki\Extension\CentralAuth\User\CentralAuthUserArrayFromResult;
33use MediaWiki\Hook\GetLogTypesOnUserHook;
34use MediaWiki\Hook\TestCanonicalRedirectHook;
35use MediaWiki\Html\Html;
36use MediaWiki\MediaWikiServices;
37use MediaWiki\Output\Hook\MakeGlobalVariablesScriptHook;
38use MediaWiki\Output\OutputPage;
39use MediaWiki\Permissions\Hook\UserGetRightsHook;
40use MediaWiki\Preferences\Hook\GetPreferencesHook;
41use MediaWiki\Registration\ExtensionRegistry;
42use MediaWiki\Request\ContentSecurityPolicy;
43use MediaWiki\Request\WebRequest;
44use MediaWiki\ResourceLoader as RL;
45use MediaWiki\ResourceLoader\Hook\ResourceLoaderForeignApiModulesHook;
46use MediaWiki\Session\CookieSessionProvider;
47use MediaWiki\Session\Hook\SessionCheckInfoHook;
48use MediaWiki\Session\SessionInfo;
49use MediaWiki\SpecialPage\SpecialPage;
50use MediaWiki\Title\Title;
51use MediaWiki\User\Hook\InvalidateEmailCompleteHook;
52use MediaWiki\User\Hook\SpecialPasswordResetOnSubmitHook;
53use MediaWiki\User\Hook\UserArrayFromResultHook;
54use MediaWiki\User\Hook\UserGetEmailAuthenticationTimestampHook;
55use MediaWiki\User\Hook\UserGetEmailHook;
56use MediaWiki\User\Hook\UserGetReservedNamesHook;
57use MediaWiki\User\Hook\UserIsBotHook;
58use MediaWiki\User\Hook\UserIsLockedHook;
59use MediaWiki\User\Hook\UserSaveSettingsHook;
60use MediaWiki\User\Hook\UserSetEmailAuthenticationTimestampHook;
61use MediaWiki\User\Hook\UserSetEmailHook;
62use MediaWiki\User\Options\UserOptionsLookup;
63use MediaWiki\User\User;
64use MediaWiki\User\UserArrayFromResult;
65use MediaWiki\User\UserNameUtils;
66use MediaWiki\WikiMap\WikiMap;
67use MobileContext;
68use OOUI\ButtonWidget;
69use OOUI\HorizontalLayout;
70use OOUI\IconWidget;
71use Wikimedia\Rdbms\IResultWrapper;
72
73class 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}