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\Context\RequestContext; |
12 | use MediaWiki\MainConfigNames; |
13 | use MediaWiki\MediaWikiServices; |
14 | use MediaWiki\WikiMap\WikiMap; |
15 | use 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 | */ |
22 | class 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 | } |