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