Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.55% covered (success)
91.55%
65 / 71
76.19% covered (warning)
76.19%
16 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
SearchConfig
91.55% covered (success)
91.55%
65 / 71
76.19% covered (warning)
76.19%
16 / 21
44.12
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 createClusterAssignment
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getClusterAssignment
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 clearCachesForTesting
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 resetWikiIdForTesting
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isLocalWiki
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHostWikiConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 has
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 newFromGlobals
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getWikiId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 makeId
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 makePageId
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 getUserLanguage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getElement
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 setSource
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getNonCirrusConfigVarNames
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isCrossProjectSearchEnabled
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 isCrossLanguageSearchEnabled
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getProfileService
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
3.21
 isCompletionSuggesterEnabled
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace CirrusSearch;
4
5use CirrusSearch\Profile\SearchProfileService;
6use CirrusSearch\Profile\SearchProfileServiceFactory;
7use CirrusSearch\Profile\SearchProfileServiceFactoryFactory;
8use LogicException;
9use MediaWiki\Config\Config;
10use MediaWiki\Config\GlobalVarConfig;
11use MediaWiki\Context\RequestContext;
12use MediaWiki\MainConfigNames;
13use MediaWiki\MediaWikiServices;
14use MediaWiki\WikiMap\WikiMap;
15use Wikimedia\Assert\Assert;
16
17/**
18 * Configuration class encapsulating Searcher environment.
19 * This config class can import settings from the environment globals,
20 * or from specific wiki configuration.
21 */
22class SearchConfig implements Config {
23    // Constants for referring to various config values. Helps prevent fat-fingers
24    public const INDEX_BASE_NAME = 'CirrusSearchIndexBaseName';
25    private const PREFIX_IDS = 'CirrusSearchPrefixIds';
26    private const CIRRUS_VAR_PREFIX = 'wgCirrus';
27
28    // Magic word to tell the SearchConfig to translate INDEX_BASE_NAME into WikiMap::getCurrentWikiId()
29    public const WIKI_ID_MAGIC_WORD = '__wikiid__';
30
31    /** Non cirrus vars to load when loading external wiki config */
32    private const NON_CIRRUS_VARS = [
33        MainConfigNames::LanguageCode,
34        MainConfigNames::ContentNamespaces,
35        MainConfigNames::NamespacesToBeSearchedDefault,
36    ];
37
38    /**
39     * @var self Configuration of host wiki.
40     */
41    private $hostConfig;
42
43    /**
44     * Override settings
45     * @var Config
46     */
47    private $source;
48
49    /**
50     * Wiki id or null for current wiki
51     * @var string|null
52     */
53    private $wikiId;
54
55    /**
56     * @var Assignment\ClusterAssignment|null
57     */
58    private $clusters;
59
60    /**
61     * @var SearchProfileService|null
62     */
63    private $profileService;
64
65    /**
66     * @var SearchProfileServiceFactoryFactory|null (lazy loaded)
67     */
68    private $searchProfileServiceFactoryFactory;
69
70    /**
71     * Create new search config for the current wiki.
72     * @param SearchProfileServiceFactoryFactory|null $searchProfileServiceFactoryFactory
73     */
74    public function __construct( ?SearchProfileServiceFactoryFactory $searchProfileServiceFactoryFactory = null ) {
75        $this->source = new GlobalVarConfig();
76        $this->wikiId = WikiMap::getCurrentWikiId();
77        // The only ability to mutate SearchConfig is via a protected method, setSource.
78        // As long as we have an instance of SearchConfig it must then be the hostConfig.
79        $this->hostConfig = static::class === self::class ? $this : new self();
80        $this->searchProfileServiceFactoryFactory = $searchProfileServiceFactoryFactory;
81    }
82
83    /**
84     * This must be delayed until after construction is complete. Before then
85     * subclasses could change out the configuration we see.
86     */
87    private function createClusterAssignment(): Assignment\ClusterAssignment {
88        // Configuring CirrusSearchServers enables "easy mode" which assumes
89        // everything happens inside a single elasticsearch cluster.
90        if ( $this->has( 'CirrusSearchServers' ) ) {
91            return new Assignment\ConstantAssignment(
92                $this->get( 'CirrusSearchServers' ) );
93        } else {
94            return new Assignment\MultiClusterAssignment( $this );
95        }
96    }
97
98    public function getClusterAssignment(): Assignment\ClusterAssignment {
99        if ( $this->clusters === null ) {
100            $this->clusters = $this->createClusterAssignment();
101        }
102        return $this->clusters;
103    }
104
105    /**
106     * Reset any cached state so testing can ensures changes to global state
107     * are reflected here. Only public for use from phpunit.
108     */
109    public function clearCachesForTesting() {
110        $this->profileService = null;
111        $this->clusters = null;
112    }
113
114    /**
115     * Resets the internal wiki ID for the SearchConfig to be the current value returned
116     * by {@link WikiMap::getCurrentWikiId}. This exists to allow PHPUnit tests to
117     * properly reset the value which may have been cached as a different
118     * fake value (T393901).
119     *
120     * @internal Only for use in tests
121     * @return void
122     */
123    public function resetWikiIdForTesting(): void {
124        $this->wikiId = WikiMap::getCurrentWikiId();
125    }
126
127    /**
128     * @return bool true if this config was built for this wiki.
129     */
130    public function isLocalWiki() {
131        // FIXME: this test is somewhat obscure (very indirect to say the least)
132        // problem is that testing $this->wikiId === WikiMap::getCurrentWikiId()
133        // would not work properly during unit tests.
134        return $this->source instanceof GlobalVarConfig;
135    }
136
137    /**
138     * @return self Configuration of the host wiki.
139     */
140    public function getHostWikiConfig(): self {
141        return $this->hostConfig;
142    }
143
144    /**
145     * @param string $name
146     * @return bool
147     */
148    public function has( $name ) {
149        return $this->source->has( $name );
150    }
151
152    /**
153     * @param string $name
154     * @return mixed
155     */
156    public function get( $name ) {
157        if ( !$this->source->has( $name ) ) {
158            return null;
159        }
160        $value = $this->source->get( $name );
161        if ( $name === self::INDEX_BASE_NAME && $value === self::WIKI_ID_MAGIC_WORD ) {
162            return $this->getWikiId();
163        }
164        return $value;
165    }
166
167    /**
168     * Produce new configuration from globals
169     */
170    public static function newFromGlobals(): self {
171        return new self();
172    }
173
174    /**
175     * Return configured Wiki ID
176     * @return string
177     */
178    public function getWikiId() {
179        return $this->wikiId;
180    }
181
182    /**
183     * @todo
184     * The indices have to be rebuilt with new id's and we have to know when
185     * generating queries if new style id's are being used, or old style. It
186     * could plausibly be done with the metastore index, but that seems like
187     * overkill because the knowledge is only necessary during transition, and
188     * not post-transition.  Additionally this function would then need to know
189     * the name of the index being queried, and that isn't always known when
190     * building.
191     *
192     * @param string|int $pageId
193     * @return string
194     */
195    public function makeId( $pageId ) {
196        Assert::parameter( is_int( $pageId ) || ( is_string( $pageId ) && ctype_digit( $pageId ) ),
197            '$pageId', "should be an integer or a string with digits, got [$pageId]." );
198        $prefix = $this->get( self::PREFIX_IDS )
199            ? $this->getWikiId()
200            : null;
201
202        if ( $prefix === null ) {
203            return (string)$pageId;
204        } else {
205            return "{$prefix}|{$pageId}";
206        }
207    }
208
209    /**
210     * Convert an elasticsearch document id back into a mediawiki page id.
211     *
212     * @param string $docId Elasticsearch document id
213     * @return int Related mediawiki page id
214     * @throws \Exception
215     */
216    public function makePageId( $docId ) {
217        if ( !$this->get( self::PREFIX_IDS ) ) {
218            return (int)$docId;
219        }
220
221        $pieces = explode( '|', $docId );
222        switch ( count( $pieces ) ) {
223            case 2:
224                return (int)$pieces[1];
225            case 1:
226                // Broken doc id...assume somehow this didn't get prefixed.
227                // Attempt to continue on...but maybe should throw exception
228                // instead?
229                return (int)$docId;
230            default:
231                throw new LogicException( "Invalid document id: $docId" );
232        }
233    }
234
235    /**
236     * Get user's language
237     * @return string User's language code
238     */
239    public function getUserLanguage() {
240        // I suppose using $wgLang would've been more evil than this, but
241        // only marginally so. Find some real context to use here.
242        return RequestContext::getMain()->getLanguage()->getCode();
243    }
244
245    /**
246     * Get chain of elements from config array
247     * @param string $configName
248     * @param string ...$path list of path elements
249     * @return mixed Returns value or null if not present
250     */
251    public function getElement( $configName, ...$path ) {
252        if ( !$this->has( $configName ) ) {
253            return null;
254        }
255        $data = $this->get( $configName );
256        foreach ( $path as $el ) {
257            if ( !isset( $data[$el] ) ) {
258                return null;
259            }
260            $data = $data[$el];
261        }
262        return $data;
263    }
264
265    /**
266     * For Unit tests
267     * @param Config $source Config override source
268     */
269    protected function setSource( Config $source ) {
270        $this->source = $source;
271        $this->clusters = null;
272    }
273
274    /**
275     * for unit tests purpose only
276     * @return string[] list of "non-cirrus" var names
277     */
278    public static function getNonCirrusConfigVarNames() {
279        return self::NON_CIRRUS_VARS;
280    }
281
282    /**
283     * @return bool if cross project (same language) is enabled
284     */
285    public function isCrossProjectSearchEnabled() {
286        if ( $this->get( 'CirrusSearchEnableCrossProjectSearch' ) ) {
287            return true;
288        }
289        return false;
290    }
291
292    /**
293     * @return bool if cross language (same project) is enabled
294     */
295    public function isCrossLanguageSearchEnabled() {
296        if ( $this->get( 'CirrusSearchEnableAltLanguage' ) ) {
297            return true;
298        }
299        return false;
300    }
301
302    /**
303     * Load the SearchProfileService suited for this SearchConfig.
304     * The service is initialized thanks to SearchProfileServiceFactory
305     * that will load CirrusSearch profiles and additional extension profiles
306     *
307     * <b>NOTE:</b> extension profiles are not loaded if this config is built
308     * for a sister wiki.
309     *
310     * @return SearchProfileService
311     * @see SearchProfileService
312     * @see SearchProfileServiceFactory
313     */
314    public function getProfileService() {
315        if ( $this->profileService === null ) {
316            if ( $this->searchProfileServiceFactoryFactory === null ) {
317
318                /** @var SearchProfileServiceFactory $factory */
319                $factory = MediaWikiServices::getInstance()
320                    ->getService( SearchProfileServiceFactory::SERVICE_NAME );
321            } else {
322                $factory = $this->searchProfileServiceFactoryFactory->getFactory( $this );
323            }
324            $this->profileService = $factory->loadService( $this );
325        }
326        return $this->profileService;
327    }
328
329    /**
330     * @return bool true if the completion suggester is enabled
331     */
332    public function isCompletionSuggesterEnabled() {
333        $useCompletion = $this->getElement( 'CirrusSearchUseCompletionSuggester' );
334        if ( is_string( $useCompletion ) ) {
335            return wfStringToBool( $useCompletion );
336        }
337        return $useCompletion === true;
338    }
339}