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