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