Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
91.43% |
64 / 70 |
|
75.00% |
15 / 20 |
CRAP | |
0.00% |
0 / 1 |
SearchConfig | |
91.43% |
64 / 70 |
|
75.00% |
15 / 20 |
43.11 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
createClusterAssignment | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getClusterAssignment | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
clearCachesForTesting | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
isLocalWiki | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getHostWikiConfig | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
has | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
get | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
newFromGlobals | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getWikiId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
makeId | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
makePageId | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
getUserLanguage | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getElement | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
4.03 | |||
setSource | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getNonCirrusConfigVarNames | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isCrossProjectSearchEnabled | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
isCrossLanguageSearchEnabled | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getProfileService | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
3.21 | |||
isCompletionSuggesterEnabled | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace CirrusSearch; |
4 | |
5 | use CirrusSearch\Profile\SearchProfileService; |
6 | use CirrusSearch\Profile\SearchProfileServiceFactory; |
7 | use CirrusSearch\Profile\SearchProfileServiceFactoryFactory; |
8 | use LogicException; |
9 | use MediaWiki\Config\Config; |
10 | use MediaWiki\Config\GlobalVarConfig; |
11 | use MediaWiki\MediaWikiServices; |
12 | use MediaWiki\WikiMap\WikiMap; |
13 | use RequestContext; |
14 | use 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 | */ |
21 | class 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 | } |