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