Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
43.59% covered (danger)
43.59%
102 / 234
30.00% covered (danger)
30.00%
9 / 30
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
43.59% covered (danger)
43.59%
102 / 234
30.00% covered (danger)
30.00%
9 / 30
1577.47
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onBeforeInitialize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onApiBeforeMain
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 initializeForRequest
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
3
 overrideNumeric
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 overrideMinimumShouldMatch
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 overrideSecret
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 overrideYesNo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 overrideMoreLikeThisOptionsFromMessage
25.00% covered (danger)
25.00%
11 / 44
0.00% covered (danger)
0.00%
0 / 1
226.19
 isMinimumShouldMatch
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
9.58
 overrideMoreLikeThisOptions
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
2
 onSoftwareInfo
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 onSpecialSearchResultsAppend
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 addSearchFeedbackLink
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 onPrefixSearchExtractNamespace
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 prefixSearchExtractNamespaceWithConnection
53.33% covered (warning)
53.33%
8 / 15
0.00% covered (danger)
0.00%
0 / 1
5.63
 onSearchGetNearMatch
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 handleSearchGetNearMatch
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
42
 onResourceLoaderGetConfigVars
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getConfig
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getConnection
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onShowSearchHitTitle
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 onAPIAfterExecute
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 onSpecialSearchResults
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 addWordCount
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 onGetPreferences
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 autoCompleteOptionsForPreferences
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
4
 onUserGetDefaultOptions
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onSpecialStatsAddExtra
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onAPIQuerySiteInfoStatisticsInfo
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace CirrusSearch;
4
5use ApiBase;
6use ApiMain;
7use ApiOpenSearch;
8use CirrusSearch\Search\FancyTitleResultsType;
9use HtmlArmor;
10use ISearchResultSet;
11use MediaWiki\Actions\ActionEntryPoint;
12use MediaWiki\Api\Hook\APIAfterExecuteHook;
13use MediaWiki\Api\Hook\APIQuerySiteInfoStatisticsInfoHook;
14use MediaWiki\Config\Config;
15use MediaWiki\Config\ConfigFactory;
16use MediaWiki\Context\RequestContext;
17use MediaWiki\Hook\ApiBeforeMainHook;
18use MediaWiki\Hook\BeforeInitializeHook;
19use MediaWiki\Hook\SoftwareInfoHook;
20use MediaWiki\Hook\SpecialSearchResultsAppendHook;
21use MediaWiki\Hook\SpecialSearchResultsHook;
22use MediaWiki\Hook\SpecialStatsAddExtraHook;
23use MediaWiki\Html\Html;
24use MediaWiki\MediaWikiServices;
25use MediaWiki\Output\OutputPage;
26use MediaWiki\Preferences\Hook\GetPreferencesHook;
27use MediaWiki\Request\WebRequest;
28use MediaWiki\ResourceLoader\Hook\ResourceLoaderGetConfigVarsHook;
29use MediaWiki\Search\Hook\PrefixSearchExtractNamespaceHook;
30use MediaWiki\Search\Hook\SearchGetNearMatchHook;
31use MediaWiki\Search\Hook\ShowSearchHitTitleHook;
32use MediaWiki\Specials\SpecialSearch;
33use MediaWiki\Title\Title;
34use MediaWiki\User\Hook\UserGetDefaultOptionsHook;
35use MediaWiki\User\User;
36use SearchResult;
37
38/**
39 * All CirrusSearch's external hooks.
40 *
41 * This program is free software; you can redistribute it and/or modify
42 * it under the terms of the GNU General Public License as published by
43 * the Free Software Foundation; either version 2 of the License, or
44 * (at your option) any later version.
45 *
46 * This program is distributed in the hope that it will be useful,
47 * but WITHOUT ANY WARRANTY; without even the implied warranty of
48 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
49 * GNU General Public License for more details.
50 *
51 * You should have received a copy of the GNU General Public License along
52 * with this program; if not, write to the Free Software Foundation, Inc.,
53 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
54 * http://www.gnu.org/copyleft/gpl.html
55 */
56class Hooks implements
57    UserGetDefaultOptionsHook,
58    GetPreferencesHook,
59    APIAfterExecuteHook,
60    ApiBeforeMainHook,
61    APIQuerySiteInfoStatisticsInfoHook,
62    BeforeInitializeHook,
63    PrefixSearchExtractNamespaceHook,
64    ResourceLoaderGetConfigVarsHook,
65    SearchGetNearMatchHook,
66    ShowSearchHitTitleHook,
67    SoftwareInfoHook,
68    SpecialSearchResultsHook,
69    SpecialSearchResultsAppendHook,
70    SpecialStatsAddExtraHook
71{
72    /** @var ConfigFactory */
73    private $configFactory;
74
75    /**
76     * @param ConfigFactory $configFactory
77     */
78    public function __construct( ConfigFactory $configFactory ) {
79        $this->configFactory = $configFactory;
80    }
81
82    /**
83     * Hooked to call initialize after the user is set up.
84     *
85     * @param Title $title
86     * @param null $unused
87     * @param OutputPage $outputPage
88     * @param User $user
89     * @param WebRequest $request
90     * @param ActionEntryPoint $mediaWiki
91     */
92    public function onBeforeInitialize( $title, $unused, $outputPage, $user, $request, $mediaWiki ) {
93        self::initializeForRequest( $request );
94    }
95
96    /**
97     * Hooked to call initialize after the user is set up.
98     * @param ApiMain &$apiMain The ApiMain instance being used
99     */
100    public function onApiBeforeMain( &$apiMain ) {
101        self::initializeForRequest( $apiMain->getRequest() );
102    }
103
104    /**
105     * Initializes the portions of Cirrus that require the $request to be fully initialized
106     *
107     * @param WebRequest $request
108     */
109    public static function initializeForRequest( WebRequest $request ) {
110        global $wgCirrusSearchPhraseRescoreWindowSize,
111            $wgCirrusSearchFunctionRescoreWindowSize,
112            $wgCirrusSearchFragmentSize,
113            $wgCirrusSearchPhraseRescoreBoost,
114            $wgCirrusSearchPhraseSlop,
115            $wgCirrusSearchLogElasticRequests,
116            $wgCirrusSearchLogElasticRequestsSecret,
117            $wgCirrusSearchEnableAltLanguage,
118            $wgCirrusSearchUseCompletionSuggester;
119
120        self::overrideMoreLikeThisOptionsFromMessage();
121
122        self::overrideNumeric( $wgCirrusSearchPhraseRescoreWindowSize,
123            $request, 'cirrusPhraseWindow', 10000 );
124        self::overrideNumeric( $wgCirrusSearchPhraseRescoreBoost,
125            $request, 'cirrusPhraseBoost' );
126        self::overrideNumeric( $wgCirrusSearchPhraseSlop[ 'boost' ],
127            $request, 'cirrusPhraseSlop', 10 );
128        self::overrideNumeric( $wgCirrusSearchFunctionRescoreWindowSize,
129            $request, 'cirrusFunctionWindow', 10000 );
130        self::overrideNumeric( $wgCirrusSearchFragmentSize,
131            $request, 'cirrusFragmentSize', 1000 );
132        if ( $wgCirrusSearchUseCompletionSuggester === 'yes' || $wgCirrusSearchUseCompletionSuggester === true ) {
133            // Only allow disabling the completion suggester, enabling it from request params might cause failures
134            // as the index might not be present.
135            self::overrideYesNo( $wgCirrusSearchUseCompletionSuggester,
136                $request, 'cirrusUseCompletionSuggester' );
137        }
138        self::overrideMoreLikeThisOptions( $request );
139        self::overrideSecret( $wgCirrusSearchLogElasticRequests,
140            $wgCirrusSearchLogElasticRequestsSecret, $request, 'cirrusLogElasticRequests', false );
141        self::overrideYesNo( $wgCirrusSearchEnableAltLanguage,
142            $request, 'cirrusAltLanguage' );
143    }
144
145    /**
146     * Set $dest to the numeric value from $request->getVal( $name ) if it is <= $limit
147     * or => $limit if upperLimit is false.
148     *
149     * @param mixed &$dest
150     * @param WebRequest $request
151     * @param string $name
152     * @param int|null $limit
153     * @param bool $upperLimit
154     */
155    private static function overrideNumeric(
156        &$dest,
157        WebRequest $request,
158        $name,
159        $limit = null,
160        $upperLimit = true
161    ) {
162        Util::overrideNumeric( $dest, $request, $name, $limit, $upperLimit );
163    }
164
165    /**
166     * @param mixed &$dest
167     * @param WebRequest $request
168     * @param string $name
169     */
170    private static function overrideMinimumShouldMatch( &$dest, WebRequest $request, $name ) {
171        $val = $request->getVal( $name );
172        if ( $val !== null && self::isMinimumShouldMatch( $val ) ) {
173            $dest = $val;
174        }
175    }
176
177    /**
178     * Set $dest to $value when $request->getVal( $name ) contains $secret
179     *
180     * @param mixed &$dest
181     * @param string $secret
182     * @param WebRequest $request
183     * @param string $name
184     * @param mixed $value
185     */
186    private static function overrideSecret( &$dest, $secret, WebRequest $request, $name, $value = true ) {
187        if ( $secret && $secret === $request->getVal( $name ) ) {
188            $dest = $value;
189        }
190    }
191
192    /**
193     * Set $dest to the true/false from $request->getVal( $name ) if yes/no.
194     *
195     * @param mixed &$dest
196     * @param WebRequest $request
197     * @param string $name
198     */
199    private static function overrideYesNo( &$dest, WebRequest $request, $name ) {
200        Util::overrideYesNo( $dest, $request, $name );
201    }
202
203    /**
204     * Extract more like this settings from the i18n message cirrussearch-morelikethis-settings
205     */
206    private static function overrideMoreLikeThisOptionsFromMessage() {
207        global $wgCirrusSearchMoreLikeThisConfig,
208            $wgCirrusSearchMoreLikeThisAllowedFields,
209            $wgCirrusSearchMoreLikeThisMaxQueryTermsLimit,
210            $wgCirrusSearchMoreLikeThisFields;
211
212        $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
213        $lines = $cache->getWithSetCallback(
214            $cache->makeKey( 'cirrussearch-morelikethis-settings' ),
215            600,
216            static function () {
217                $source = wfMessage( 'cirrussearch-morelikethis-settings' )->inContentLanguage();
218                if ( $source->isDisabled() ) {
219                    return [];
220                }
221                return Util::parseSettingsInMessage( $source->plain() );
222            }
223        );
224
225        foreach ( $lines as $line ) {
226            if ( strpos( $line, ':' ) === false ) {
227                continue;
228            }
229            [ $k, $v ] = explode( ':', $line, 2 );
230            switch ( $k ) {
231                case 'min_doc_freq':
232                case 'max_doc_freq':
233                case 'max_query_terms':
234                case 'min_term_freq':
235                case 'min_word_length':
236                case 'max_word_length':
237                    if ( is_numeric( $v ) && $v >= 0 ) {
238                        $wgCirrusSearchMoreLikeThisConfig[$k] = intval( $v );
239                    } elseif ( $v === 'null' ) {
240                        unset( $wgCirrusSearchMoreLikeThisConfig[$k] );
241                    }
242                    break;
243                case 'percent_terms_to_match':
244                    // @deprecated Use minimum_should_match now
245                    $k = 'minimum_should_match';
246                    if ( is_numeric( $v ) && $v > 0 && $v <= 1 ) {
247                        $v = ( (int)( (float)$v * 100 ) ) . '%';
248                    } else {
249                        break;
250                    }
251                    // intentional fall-through
252                case 'minimum_should_match':
253                    if ( self::isMinimumShouldMatch( $v ) ) {
254                        $wgCirrusSearchMoreLikeThisConfig[$k] = $v;
255                    } elseif ( $v === 'null' ) {
256                        unset( $wgCirrusSearchMoreLikeThisConfig[$k] );
257                    }
258                    break;
259                case 'fields':
260                    $wgCirrusSearchMoreLikeThisFields = array_intersect(
261                        array_map( 'trim', explode( ',', $v ) ),
262                        $wgCirrusSearchMoreLikeThisAllowedFields );
263                    break;
264            }
265            // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
266            if ( $wgCirrusSearchMoreLikeThisConfig['max_query_terms'] > $wgCirrusSearchMoreLikeThisMaxQueryTermsLimit ) {
267                $wgCirrusSearchMoreLikeThisConfig['max_query_terms'] = $wgCirrusSearchMoreLikeThisMaxQueryTermsLimit;
268            }
269        }
270    }
271
272    /**
273     * @param string $v The value to check
274     * @return bool True if $v is an integer percentage in the domain -100 <= $v <= 100, $v != 0
275     * @todo minimum_should_match also supports combinations (3<90%) and multiple combinations
276     */
277    private static function isMinimumShouldMatch( string $v ) {
278        // specific integer count > 0
279        if ( ctype_digit( $v ) && $v != 0 ) {
280            return true;
281        }
282        // percentage 0 < x <= 100
283        if ( !str_ends_with( $v, '%' ) ) {
284            return false;
285        }
286        $v = substr( $v, 0, -1 );
287        if ( str_starts_with( $v, '-' ) ) {
288            $v = substr( $v, 1 );
289        }
290        return ctype_digit( $v ) && $v > 0 && $v <= 100;
291    }
292
293    /**
294     * Override more like this settings from request URI parameters
295     *
296     * @param WebRequest $request
297     */
298    private static function overrideMoreLikeThisOptions( WebRequest $request ) {
299        global $wgCirrusSearchMoreLikeThisConfig,
300            $wgCirrusSearchMoreLikeThisAllowedFields,
301            $wgCirrusSearchMoreLikeThisMaxQueryTermsLimit,
302            $wgCirrusSearchMoreLikeThisFields;
303
304        self::overrideNumeric( $wgCirrusSearchMoreLikeThisConfig['min_doc_freq'],
305            $request, 'cirrusMltMinDocFreq' );
306        self::overrideNumeric( $wgCirrusSearchMoreLikeThisConfig['max_doc_freq'],
307            $request, 'cirrusMltMaxDocFreq' );
308        // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
309        self::overrideNumeric( $wgCirrusSearchMoreLikeThisConfig['max_query_terms'],
310            $request, 'cirrusMltMaxQueryTerms', $wgCirrusSearchMoreLikeThisMaxQueryTermsLimit );
311        self::overrideNumeric( $wgCirrusSearchMoreLikeThisConfig['min_term_freq'],
312            $request, 'cirrusMltMinTermFreq' );
313        self::overrideMinimumShouldMatch( $wgCirrusSearchMoreLikeThisConfig['minimum_should_match'],
314            $request, 'cirrusMltMinimumShouldMatch' );
315        self::overrideNumeric( $wgCirrusSearchMoreLikeThisConfig['min_word_length'],
316            $request, 'cirrusMltMinWordLength' );
317        self::overrideNumeric( $wgCirrusSearchMoreLikeThisConfig['max_word_length'],
318            $request, 'cirrusMltMaxWordLength' );
319        $fields = $request->getVal( 'cirrusMltFields' );
320        if ( isset( $fields ) ) {
321            $wgCirrusSearchMoreLikeThisFields = array_intersect(
322                array_map( 'trim', explode( ',', $fields ) ),
323                $wgCirrusSearchMoreLikeThisAllowedFields );
324        }
325    }
326
327    /**
328     * Hook called to include Elasticsearch version info on Special:Version
329     * @param array &$software Array of wikitext and version numbers
330     */
331    public function onSoftwareInfo( &$software ) {
332        $version = new Version( self::getConnection() );
333        $status = $version->get();
334        if ( $status->isOK() ) {
335            // We've already logged if this isn't ok and there is no need to warn the user on this page.
336            $software[ '[https://www.elastic.co/elasticsearch Elasticsearch]' ] = $status->getValue();
337        }
338    }
339
340    /**
341     * @param SpecialSearch $specialSearch
342     * @param OutputPage $out
343     * @param string $term
344     */
345    public function onSpecialSearchResultsAppend( $specialSearch, $out, $term ) {
346        global $wgCirrusSearchFeedbackLink;
347
348        if ( $wgCirrusSearchFeedbackLink ) {
349            self::addSearchFeedbackLink( $wgCirrusSearchFeedbackLink, $specialSearch, $out );
350        }
351
352        // Embed metrics if this was a Cirrus page
353        $engine = $specialSearch->getSearchEngine();
354        if ( $engine instanceof CirrusSearch ) {
355            $out->addJsConfigVars( $engine->getLastSearchMetrics() );
356        }
357    }
358
359    /**
360     * @param string $link
361     * @param SpecialSearch $specialSearch
362     * @param OutputPage $out
363     */
364    private static function addSearchFeedbackLink( $link, SpecialSearch $specialSearch, OutputPage $out ) {
365        $anchor = Html::element(
366            'a',
367            [ 'href' => $link ],
368            $specialSearch->msg( 'cirrussearch-give-feedback' )->text()
369        );
370        $block = Html::rawElement( 'div', [], $anchor );
371        $out->addHTML( $block );
372    }
373
374    /**
375     * Extract namespaces from query string.
376     * @param array &$namespaces
377     * @param string &$search
378     * @return bool
379     */
380    public function onPrefixSearchExtractNamespace( &$namespaces, &$search ) {
381        global $wgSearchType;
382        if ( $wgSearchType !== 'CirrusSearch' ) {
383            return true;
384        }
385        return self::prefixSearchExtractNamespaceWithConnection( self::getConnection(), $namespaces, $search );
386    }
387
388    /**
389     * @param Connection $connection
390     * @param array &$namespaces
391     * @param string &$search
392     * @return false
393     */
394    public static function prefixSearchExtractNamespaceWithConnection(
395        Connection $connection,
396        &$namespaces,
397        &$search
398    ) {
399        $method = $connection->getConfig()->get( 'CirrusSearchNamespaceResolutionMethod' );
400        if ( $method === 'elastic' ) {
401            $searcher =
402                new Searcher( $connection, 0, 1, $connection->getConfig(), $namespaces );
403            $searcher->updateNamespacesFromQuery( $search );
404            $namespaces = $searcher->getSearchContext()->getNamespaces();
405        } else {
406            $colon = strpos( $search, ':' );
407            if ( $colon === false ) {
408                return false;
409            }
410            $namespaceName = substr( $search, 0, $colon );
411            $ns = Util::identifyNamespace( $namespaceName, $method );
412            if ( $ns !== false ) {
413                $namespaces = [ $ns ];
414                $search = substr( $search, $colon + 1 );
415            }
416        }
417
418        return false;
419    }
420
421    public function onSearchGetNearMatch( $term, &$titleResult ) {
422        return self::handleSearchGetNearMatch( $term, $titleResult );
423    }
424
425    /**
426     * Let Elasticsearch take a crack at getting near matches once mediawiki has tried all kinds of variants.
427     * @param string $term the original search term and all language variants
428     * @param null|Title &$titleResult resulting match.  A Title if we found something, unchanged otherwise.
429     * @return bool return false if we find something, true otherwise so mediawiki can try its default behavior
430     */
431    public static function handleSearchGetNearMatch( $term, &$titleResult ) {
432        global $wgSearchType;
433        if ( $wgSearchType !== 'CirrusSearch' ) {
434            return true;
435        }
436
437        $title = Title::newFromText( $term );
438        if ( $title === null ) {
439            return false;
440        }
441
442        $user = RequestContext::getMain()->getUser();
443        // Ask for the first 50 results we see.  If there are more than that too bad.
444        $searcher = new Searcher(
445            self::getConnection(), 0, 50, self::getConfig(), [ $title->getNamespace() ], $user );
446        if ( $title->getNamespace() === NS_MAIN ) {
447            $searcher->updateNamespacesFromQuery( $term );
448        } else {
449            $term = $title->getText();
450        }
451        $searcher->setResultsType( new FancyTitleResultsType( 'near_match' ) );
452        $status = $searcher->nearMatchTitleSearch( $term );
453        // There is no way to send errors or warnings back to the caller here so we have to make do with
454        // only sending results back if there are results and relying on the logging done at the status
455        // construction site to log errors.
456        if ( !$status->isOK() ) {
457            return true;
458        }
459
460        $contLang = MediaWikiServices::getInstance()->getContentLanguage();
461        $picker = new NearMatchPicker( $contLang, $term, $status->getValue() );
462        $best = $picker->pickBest();
463        if ( $best ) {
464            $titleResult = $best;
465            return false;
466        }
467        // Didn't find a result so let MediaWiki have a crack at it.
468        return true;
469    }
470
471    /**
472     * ResourceLoaderGetConfigVars hook handler
473     * This should be used for variables which vary with the html
474     * and for variables this should work cross skin
475     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderGetConfigVars
476     *
477     * @param array &$vars
478     * @param string $skin
479     * @param Config $config
480     */
481    public function onResourceLoaderGetConfigVars( array &$vars, $skin, Config $config ): void {
482        global $wgCirrusSearchFeedbackLink;
483
484        $vars += [
485            'wgCirrusSearchFeedbackLink' => $wgCirrusSearchFeedbackLink,
486        ];
487    }
488
489    /**
490     * @return SearchConfig
491     */
492    private static function getConfig() {
493        // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
494        return MediaWikiServices::getInstance()
495            ->getConfigFactory()
496            ->makeConfig( 'CirrusSearch' );
497    }
498
499    /**
500     * @return Connection
501     */
502    private static function getConnection() {
503        return new Connection( self::getConfig() );
504    }
505
506    /**
507     * Add $wgCirrusSearchInterwikiProv to external results.
508     * @param Title &$title
509     * @param string|HtmlArmor|null &$text
510     * @param SearchResult $result
511     * @param array $terms
512     * @param SpecialSearch $page
513     * @param string[] &$query
514     * @param string[] &$attributes
515     */
516    public function onShowSearchHitTitle( &$title, &$text, $result, $terms, $page, &$query, &$attributes ) {
517        global $wgCirrusSearchInterwikiProv;
518        if ( $wgCirrusSearchInterwikiProv && $title->isExternal() ) {
519            $query["wprov"] = $wgCirrusSearchInterwikiProv;
520        }
521    }
522
523    /**
524     * @param ApiBase $module
525     */
526    public function onAPIAfterExecute( $module ) {
527        if ( !ElasticsearchIntermediary::hasQueryLogs() ) {
528            return;
529        }
530        $response = $module->getContext()->getRequest()->response();
531        $response->header( 'X-Search-ID: ' . Util::getRequestSetToken() );
532        if ( $module instanceof ApiOpenSearch ) {
533            $types = ElasticsearchIntermediary::getQueryTypesUsed();
534            if ( $types ) {
535                $response->header( 'X-OpenSearch-Type: ' . implode( ',', $types ) );
536            }
537        }
538    }
539
540    /**
541     * @param string $term
542     * @param ISearchResultSet|null &$titleMatches
543     * @param ISearchResultSet|null &$textMatches
544     */
545    public function onSpecialSearchResults( $term, &$titleMatches, &$textMatches ) {
546        $context = RequestContext::getMain();
547        $out = $context->getOutput();
548
549        $out->addModules( 'ext.cirrus.serp' );
550
551        $jsVars = [
552            'wgCirrusSearchRequestSetToken' => Util::getRequestSetToken(),
553        ];
554        // In theory UserTesting should always have been activated by now, but if
555        // somehow it wasn't we don't want to activate it now at the end of the request
556        // and report incorrect data.
557        if ( UserTestingStatus::hasInstance() ) {
558            $ut = UserTestingStatus::getInstance();
559            if ( $ut->isActive() ) {
560                $trigger = $ut->getTrigger();
561                $jsVars['wgCirrusSearchActiveUserTest'] = $trigger;
562                // bc for first deployment, some users will still have old js.
563                // Should be removed in following deployment.
564                $jsVars['wgCirrusSearchBackendUserTests'] = $trigger ? [ $trigger ] : [];
565            }
566        }
567        $out->addJsConfigVars( $jsVars );
568
569        // This ignores interwiki results for now...not sure what do do with those
570        ElasticsearchIntermediary::setResultPages( [
571            $titleMatches,
572            $textMatches
573        ] );
574    }
575
576    /**
577     * @param array &$extraStats
578     * @return void
579     */
580    private static function addWordCount( array &$extraStats ): void {
581        $search = new CirrusSearch();
582
583        $status = $search->countContentWords();
584        if ( !$status->isOK() ) {
585            return;
586        }
587        $wordCount = $status->getValue();
588        if ( $wordCount !== null ) {
589            $extraStats['cirrussearch-article-words'] = $wordCount;
590        }
591    }
592
593    /** @inheritDoc */
594    public function onGetPreferences( $user, &$prefs ) {
595        $search = new CirrusSearch();
596        $profiles = $search->getProfiles( \SearchEngine::COMPLETION_PROFILE_TYPE, $user );
597        if ( !$profiles ) {
598            return;
599        }
600        $options = self::autoCompleteOptionsForPreferences( $profiles );
601        if ( !$options ) {
602            return;
603        }
604        $prefs['cirrussearch-pref-completion-profile'] = [
605            'type' => 'radio',
606            'section' => 'searchoptions/completion',
607            'options' => $options,
608            'label-message' => 'cirrussearch-pref-completion-profile-help',
609        ];
610    }
611
612    /**
613     * @param array[] $profiles
614     * @return string[]
615     */
616    private static function autoCompleteOptionsForPreferences( array $profiles ): array {
617        $available = array_column( $profiles, 'name' );
618        // Order in which we propose comp suggest profiles
619        $preferredOrder = [
620            'fuzzy',
621            'fuzzy-subphrases',
622            'strict',
623            'normal',
624            'normal-subphrases',
625            'classic'
626        ];
627        $messages = [];
628        foreach ( $preferredOrder as $name ) {
629            if ( in_array( $name, $available ) ) {
630                $display = wfMessage( "cirrussearch-completion-profile-$name-pref-name" )->escaped() .
631                    new \OOUI\LabelWidget( [
632                        'classes' => [ 'oo-ui-inline-help' ],
633                        'label' => wfMessage( "cirrussearch-completion-profile-$name-pref-desc" )->text()
634                    ] );
635                $messages[$display] = $name;
636            }
637        }
638        // At least 2 choices are required to provide the user a choice
639        return count( $messages ) >= 2 ? $messages : [];
640    }
641
642    /** @inheritDoc */
643    public function onUserGetDefaultOptions( &$defaultOptions ) {
644        $defaultOptions['cirrussearch-pref-completion-profile'] =
645            $this->configFactory->makeConfig( 'CirrusSearch' )->get( 'CirrusSearchCompletionSettings' );
646    }
647
648    public function onSpecialStatsAddExtra( &$extraStats, $context ) {
649        self::addWordCount( $extraStats );
650    }
651
652    public function onAPIQuerySiteInfoStatisticsInfo( &$extraStats ) {
653        self::addWordCount( $extraStats );
654    }
655}