Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.05% covered (warning)
77.05%
47 / 61
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
DefaultOptionsLookup
78.33% covered (warning)
78.33%
47 / 60
57.14% covered (warning)
57.14%
4 / 7
22.67
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 getGenericDefaultOptions
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
5
 getDefaultOptions
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
5.01
 getOption
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getOptions
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 verifyUsable
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getOptionBatchForUserNames
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\User\Options;
8
9use MediaWiki\Config\ServiceOptions;
10use MediaWiki\HookContainer\HookContainer;
11use MediaWiki\HookContainer\HookRunner;
12use MediaWiki\Language\LanguageCode;
13use MediaWiki\Language\LanguageConverter;
14use MediaWiki\MainConfigNames;
15use MediaWiki\Skin\Skin;
16use MediaWiki\Title\NamespaceInfo;
17use MediaWiki\User\UserIdentity;
18use MediaWiki\User\UserIdentityLookup;
19use MediaWiki\User\UserNameUtils;
20use Wikimedia\Assert\Assert;
21use Wikimedia\Rdbms\IDBAccessObject;
22
23/**
24 * A service class to control default user options
25 * @since 1.35
26 */
27class DefaultOptionsLookup extends UserOptionsLookup {
28
29    /**
30     * @internal For use by ServiceWiring
31     */
32    public const CONSTRUCTOR_OPTIONS = [
33        MainConfigNames::DefaultSkin,
34        MainConfigNames::DefaultUserOptions,
35        MainConfigNames::NamespacesToBeSearchedDefault
36    ];
37
38    private ServiceOptions $serviceOptions;
39    private LanguageCode $contentLang;
40    private NamespaceInfo $nsInfo;
41    private ConditionalDefaultsLookup $conditionalDefaultsLookup;
42    private UserIdentityLookup $userIdentityLookup;
43
44    /** @var array Cache of default options by user */
45    private $cache = [];
46
47    /** @var array|null Cached default options */
48    private $defaultOptions = null;
49
50    private HookRunner $hookRunner;
51
52    /**
53     * @param ServiceOptions $options
54     * @param LanguageCode $contentLang
55     * @param HookContainer $hookContainer
56     * @param NamespaceInfo $nsInfo
57     * @param ConditionalDefaultsLookup $conditionalUserOptionsDefaultsLookup
58     * @param UserIdentityLookup $userIdentityLookup
59     * @param UserNameUtils $userNameUtils
60     */
61    public function __construct(
62        ServiceOptions $options,
63        LanguageCode $contentLang,
64        HookContainer $hookContainer,
65        NamespaceInfo $nsInfo,
66        ConditionalDefaultsLookup $conditionalUserOptionsDefaultsLookup,
67        UserIdentityLookup $userIdentityLookup,
68        UserNameUtils $userNameUtils
69    ) {
70        parent::__construct( $userNameUtils );
71        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
72        $this->serviceOptions = $options;
73        $this->contentLang = $contentLang;
74        $this->hookRunner = new HookRunner( $hookContainer );
75        $this->nsInfo = $nsInfo;
76        $this->conditionalDefaultsLookup = $conditionalUserOptionsDefaultsLookup;
77        $this->userIdentityLookup = $userIdentityLookup;
78    }
79
80    /**
81     * Get default user options from $wgDefaultUserOptions (ignoring any conditional defaults)
82     */
83    private function getGenericDefaultOptions(): array {
84        if ( $this->defaultOptions !== null ) {
85            return $this->defaultOptions;
86        }
87
88        $this->defaultOptions = $this->serviceOptions->get( MainConfigNames::DefaultUserOptions );
89
90        // Default language setting
91        // NOTE: don't use the content language code since the static default variant would
92        //  NOT always be the same as the content language code.
93        $contentLangCode = $this->contentLang->toString();
94        $LangsWithStaticDefaultVariant = LanguageConverter::$languagesWithStaticDefaultVariant;
95        $staticDefaultVariant = $LangsWithStaticDefaultVariant[$contentLangCode] ?? $contentLangCode;
96        $this->defaultOptions['language'] = $contentLangCode;
97        $this->defaultOptions['variant'] = $staticDefaultVariant;
98        foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
99            $staticDefaultVariant = $LangsWithStaticDefaultVariant[$langCode] ?? $langCode;
100            $this->defaultOptions["variant-$langCode"] = $staticDefaultVariant;
101        }
102
103        // NOTE: don't use SearchEngineConfig::getSearchableNamespaces here,
104        // since extensions may change the set of searchable namespaces depending
105        // on user groups/permissions.
106        $nsSearchDefault = $this->serviceOptions->get( MainConfigNames::NamespacesToBeSearchedDefault );
107        foreach ( $this->nsInfo->getValidNamespaces() as $n ) {
108            $this->defaultOptions['searchNs' . $n] = ( $nsSearchDefault[$n] ?? false ) ? 1 : 0;
109        }
110        $this->defaultOptions['skin'] = Skin::normalizeKey(
111            $this->serviceOptions->get( MainConfigNames::DefaultSkin ) );
112
113        $this->hookRunner->onUserGetDefaultOptions( $this->defaultOptions );
114
115        return $this->defaultOptions;
116    }
117
118    /**
119     * @inheritDoc
120     */
121    public function getDefaultOptions( ?UserIdentity $userIdentity = null ): array {
122        $defaultOptions = $this->getGenericDefaultOptions();
123
124        // If requested, process any conditional defaults
125        if ( $userIdentity ) {
126            $cacheKey = $this->getCacheKey( $userIdentity );
127            if ( isset( $this->cache[$cacheKey] ) ) {
128                return $this->cache[$cacheKey];
129            }
130            $conditionallyDefaultOptions = $this->conditionalDefaultsLookup->getConditionallyDefaultOptions();
131            foreach ( $conditionallyDefaultOptions as $optionName ) {
132                $conditionalDefault = $this->conditionalDefaultsLookup->getOptionDefaultForUser(
133                    $optionName, $userIdentity
134                );
135                if ( $conditionalDefault !== null ) {
136                    $defaultOptions[$optionName] = $conditionalDefault;
137                }
138            }
139            $this->cache[$cacheKey] = $defaultOptions;
140        }
141
142        return $defaultOptions;
143    }
144
145    /**
146     * @inheritDoc
147     */
148    public function getOption(
149        UserIdentity $user,
150        string $oname,
151        $defaultOverride = null,
152        bool $ignoreHidden = false,
153        int $queryFlags = IDBAccessObject::READ_NORMAL
154    ) {
155        $this->verifyUsable( $user, __METHOD__ );
156        return $this->getDefaultOption( $oname ) ?? $defaultOverride;
157    }
158
159    /**
160     * @inheritDoc
161     */
162    public function getOptions(
163        UserIdentity $user,
164        int $flags = 0,
165        int $queryFlags = IDBAccessObject::READ_NORMAL
166    ): array {
167        $this->verifyUsable( $user, __METHOD__ );
168        if ( $flags & self::EXCLUDE_DEFAULTS ) {
169            return [];
170        }
171        return $this->getDefaultOptions();
172    }
173
174    /**
175     * Checks if the DefaultOptionsLookup is usable as an instance of UserOptionsLookup.
176     *
177     * It only makes sense in an installer context when UserOptionsManager cannot be yet instantiated
178     * as the database is not available. Thus, this can only be called for an anon user,
179     * calling under different circumstances indicates a bug, or that a system user is being used.
180     *
181     * The only exception to this is database-less PHPUnit tests, where sometimes fake registered users are
182     * used and end up being passed to this class. This should not be considered a bug, and using the default
183     * preferences in this scenario is probably the intended behaviour.
184     *
185     * @param UserIdentity $user
186     * @param string $fname
187     */
188    private function verifyUsable( UserIdentity $user, string $fname ) {
189        if ( defined( 'MEDIAWIKI_INSTALL' ) ) {
190            return;
191        }
192        Assert::precondition( !$user->isRegistered(), "$fname called on a registered user" );
193    }
194
195    /** @inheritDoc */
196    public function getOptionBatchForUserNames( array $users, string $key ) {
197        $genericDefault = $this->getGenericDefaultOptions()[$key] ?? '';
198        $options = array_fill_keys( $users, $genericDefault );
199        if ( $this->conditionalDefaultsLookup->hasConditionalDefault( $key ) ) {
200            $userIdentities = $this->userIdentityLookup->newSelectQueryBuilder()
201                ->whereUserNames( $users )
202                ->caller( __METHOD__ )
203                ->fetchUserIdentities();
204            foreach ( $userIdentities as $user ) {
205                $options[$user->getName()] = $this->conditionalDefaultsLookup
206                    ->getOptionDefaultForUser( $key, $user );
207            }
208        }
209        return $options;
210    }
211}
212
213/** @deprecated class alias since 1.42 */
214class_alias( DefaultOptionsLookup::class, 'MediaWiki\\User\\DefaultOptionsLookup' );