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\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 SearchConfig 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 SearchConfig();
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     * @return Assignment\ClusterAssignment
88     */
89    private function createClusterAssignment(): Assignment\ClusterAssignment {
90        // Configuring CirrusSearchServers enables "easy mode" which assumes
91        // everything happens inside a single elasticsearch cluster.
92        if ( $this->has( 'CirrusSearchServers' ) ) {
93            return new Assignment\ConstantAssignment(
94                $this->get( 'CirrusSearchServers' ) );
95        } else {
96            return new Assignment\MultiClusterAssignment( $this );
97        }
98    }
99
100    public function getClusterAssignment(): Assignment\ClusterAssignment {
101        if ( $this->clusters === null ) {
102            $this->clusters = $this->createClusterAssignment();
103        }
104        return $this->clusters;
105    }
106
107    /**
108     * Reset any cached state so testing can ensures changes to global state
109     * are reflected here. Only public for use from phpunit.
110     */
111    public function clearCachesForTesting() {
112        $this->profileService = null;
113        $this->clusters = null;
114    }
115
116    /**
117     * @return bool true if this config was built for this wiki.
118     */
119    public function isLocalWiki() {
120        // FIXME: this test is somewhat obscure (very indirect to say the least)
121        // problem is that testing $this->wikiId === WikiMap::getCurrentWikiId()
122        // would not work properly during unit tests.
123        return $this->source instanceof GlobalVarConfig;
124    }
125
126    /**
127     * @return SearchConfig Configuration of the host wiki.
128     */
129    public function getHostWikiConfig(): SearchConfig {
130        return $this->hostConfig;
131    }
132
133    /**
134     * @param string $name
135     * @return bool
136     */
137    public function has( $name ) {
138        return $this->source->has( $name );
139    }
140
141    /**
142     * @param string $name
143     * @return mixed
144     */
145    public function get( $name ) {
146        if ( !$this->source->has( $name ) ) {
147            return null;
148        }
149        $value = $this->source->get( $name );
150        if ( $name === self::INDEX_BASE_NAME && $value === self::WIKI_ID_MAGIC_WORD ) {
151            return $this->getWikiId();
152        }
153        return $value;
154    }
155
156    /**
157     * Produce new configuration from globals
158     * @return SearchConfig
159     */
160    public static function newFromGlobals() {
161        return new self();
162    }
163
164    /**
165     * Return configured Wiki ID
166     * @return string
167     */
168    public function getWikiId() {
169        return $this->wikiId;
170    }
171
172    /**
173     * @todo
174     * The indices have to be rebuilt with new id's and we have to know when
175     * generating queries if new style id's are being used, or old style. It
176     * could plausibly be done with the metastore index, but that seems like
177     * overkill because the knowledge is only necessary during transition, and
178     * not post-transition.  Additionally this function would then need to know
179     * the name of the index being queried, and that isn't always known when
180     * building.
181     *
182     * @param string|int $pageId
183     * @return string
184     */
185    public function makeId( $pageId ) {
186        Assert::parameter( is_int( $pageId ) || ( is_string( $pageId ) && ctype_digit( $pageId ) ),
187            '$pageId', "should be an integer or a string with digits, got [$pageId]." );
188        $prefix = $this->get( self::PREFIX_IDS )
189            ? $this->getWikiId()
190            : null;
191
192        if ( $prefix === null ) {
193            return (string)$pageId;
194        } else {
195            return "{$prefix}|{$pageId}";
196        }
197    }
198
199    /**
200     * Convert an elasticsearch document id back into a mediawiki page id.
201     *
202     * @param string $docId Elasticsearch document id
203     * @return int Related mediawiki page id
204     * @throws \Exception
205     */
206    public function makePageId( $docId ) {
207        if ( !$this->get( self::PREFIX_IDS ) ) {
208            return (int)$docId;
209        }
210
211        $pieces = explode( '|', $docId );
212        switch ( count( $pieces ) ) {
213            case 2:
214                return (int)$pieces[1];
215            case 1:
216                // Broken doc id...assume somehow this didn't get prefixed.
217                // Attempt to continue on...but maybe should throw exception
218                // instead?
219                return (int)$docId;
220            default:
221                throw new LogicException( "Invalid document id: $docId" );
222        }
223    }
224
225    /**
226     * Get user's language
227     * @return string User's language code
228     */
229    public function getUserLanguage() {
230        // I suppose using $wgLang would've been more evil than this, but
231        // only marginally so. Find some real context to use here.
232        return RequestContext::getMain()->getLanguage()->getCode();
233    }
234
235    /**
236     * Get chain of elements from config array
237     * @param string $configName
238     * @param string ...$path list of path elements
239     * @return mixed Returns value or null if not present
240     */
241    public function getElement( $configName, ...$path ) {
242        if ( !$this->has( $configName ) ) {
243            return null;
244        }
245        $data = $this->get( $configName );
246        foreach ( $path as $el ) {
247            if ( !isset( $data[$el] ) ) {
248                return null;
249            }
250            $data = $data[$el];
251        }
252        return $data;
253    }
254
255    /**
256     * For Unit tests
257     * @param Config $source Config override source
258     */
259    protected function setSource( Config $source ) {
260        $this->source = $source;
261        $this->clusters = null;
262    }
263
264    /**
265     * for unit tests purpose only
266     * @return string[] list of "non-cirrus" var names
267     */
268    public static function getNonCirrusConfigVarNames() {
269        return self::NON_CIRRUS_VARS;
270    }
271
272    /**
273     * @return bool if cross project (same language) is enabled
274     */
275    public function isCrossProjectSearchEnabled() {
276        if ( $this->get( 'CirrusSearchEnableCrossProjectSearch' ) ) {
277            return true;
278        }
279        return false;
280    }
281
282    /**
283     * @return bool if cross language (same project) is enabled
284     */
285    public function isCrossLanguageSearchEnabled() {
286        if ( $this->get( 'CirrusSearchEnableAltLanguage' ) ) {
287            return true;
288        }
289        return false;
290    }
291
292    /**
293     * Load the SearchProfileService suited for this SearchConfig.
294     * The service is initialized thanks to SearchProfileServiceFactory
295     * that will load CirrusSearch profiles and additional extension profiles
296     *
297     * <b>NOTE:</b> extension profiles are not loaded if this config is built
298     * for a sister wiki.
299     *
300     * @return SearchProfileService
301     * @see SearchProfileService
302     * @see SearchProfileServiceFactory
303     */
304    public function getProfileService() {
305        if ( $this->profileService === null ) {
306            if ( $this->searchProfileServiceFactoryFactory === null ) {
307
308                /** @var SearchProfileServiceFactory $factory */
309                $factory = MediaWikiServices::getInstance()
310                    ->getService( SearchProfileServiceFactory::SERVICE_NAME );
311            } else {
312                $factory = $this->searchProfileServiceFactoryFactory->getFactory( $this );
313            }
314            $this->profileService = $factory->loadService( $this );
315        }
316        return $this->profileService;
317    }
318
319    /**
320     * @return bool true if the completion suggester is enabled
321     */
322    public function isCompletionSuggesterEnabled() {
323        $useCompletion = $this->getElement( 'CirrusSearchUseCompletionSuggester' );
324        if ( is_string( $useCompletion ) ) {
325            return wfStringToBool( $useCompletion );
326        }
327        return $useCompletion === true;
328    }
329}