Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
43.65% covered (danger)
43.65%
79 / 181
20.83% covered (danger)
20.83%
5 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
Util
43.65% covered (danger)
43.65%
79 / 181
20.83% covered (danger)
20.83%
5 / 24
1378.01
0.00% covered (danger)
0.00%
0 / 1
 getNamespaceText
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getPoolStatsKey
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 wrapWithPoolStats
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 doPoolCounterWork
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 parsePotentialPercent
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 parseSettingsInMessage
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 endsWith
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 overrideYesNo
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 overrideNumeric
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
72
 getDefaultBoostTemplates
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getOnWikiBoostTemplates
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
5.03
 stripQuestionMarks
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
 getExecutionId
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 resetExecutionId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRequestSetToken
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 generateIdentToken
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getExecutionContext
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 identifyNamespace
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
8.09
 isEmpty
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
7
 setIfDefined
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
 getStatsDataFactory
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 looksLikeAutomation
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
5.12
 getIndexMapping
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 processSearchRawReturn
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3namespace CirrusSearch;
4
5use Elastica\Index;
6use Elasticsearch\Endpoints\Indices\GetMapping;
7use IBufferingStatsdDataFactory;
8use MediaWiki\Logger\LoggerFactory;
9use MediaWiki\MediaWikiServices;
10use MediaWiki\User\UserIdentity;
11use NullStatsdDataFactory;
12use PoolCounterWorkViaCallback;
13use Status;
14use Title;
15use UIDGenerator;
16use WebRequest;
17use WikiMap;
18use Wikimedia\Assert\Assert;
19use Wikimedia\IPUtils;
20
21/**
22 * Random utility functions that don't have a better home
23 *
24 * This program is free software; you can redistribute it and/or modify
25 * it under the terms of the GNU General Public License as published by
26 * the Free Software Foundation; either version 2 of the License, or
27 * (at your option) any later version.
28 *
29 * This program is distributed in the hope that it will be useful,
30 * but WITHOUT ANY WARRANTY; without even the implied warranty of
31 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32 * GNU General Public License for more details.
33 *
34 * You should have received a copy of the GNU General Public License along
35 * with this program; if not, write to the Free Software Foundation, Inc.,
36 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
37 * http://www.gnu.org/copyleft/gpl.html
38 */
39class Util {
40    /**
41     * Cache getDefaultBoostTemplates()
42     *
43     * @var array|null boost templates
44     */
45    private static $defaultBoostTemplates = null;
46
47    /**
48     * @var string|null Id identifying this php execution
49     */
50    private static $executionId;
51
52    /**
53     * Get the textual representation of a namespace with underscores stripped, varying
54     * by gender if need be (using Title::getNsText()).
55     *
56     * @param Title $title The page title to use
57     * @return string|false
58     */
59    public static function getNamespaceText( Title $title ) {
60        $ret = $title->getNsText();
61        return is_string( $ret ) ? strtr( $ret, '_', ' ' ) : $ret;
62    }
63
64    /**
65     * @param string $type The pool counter type, such as CirrusSearch-Search
66     * @param bool $isSuccess If the pool counter gave a success, or failed the request
67     * @return string The key used for collecting timing stats about this pool counter request
68     */
69    private static function getPoolStatsKey( $type, $isSuccess ) {
70        $pos = strpos( $type, '-' );
71        if ( $pos !== false ) {
72            $type = substr( $type, $pos + 1 );
73        }
74        $postfix = $isSuccess ? 'successMs' : 'failureMs';
75        return "CirrusSearch.poolCounter.$type.$postfix";
76    }
77
78    /**
79     * @param float $startPoolWork The time this pool request started, from microtime( true )
80     * @param string $type The pool counter type, such as CirrusSearch-Search
81     * @param bool $isSuccess If the pool counter gave a success, or failed the request
82     * @param callable $callback The function to wrap
83     * @return callable The original callback wrapped to collect pool counter stats
84     */
85    private static function wrapWithPoolStats( $startPoolWork,
86        $type,
87        $isSuccess,
88        callable $callback
89    ) {
90        return function () use ( $type, $isSuccess, $callback, $startPoolWork ) {
91            MediaWikiServices::getInstance()->getStatsdDataFactory()->timing(
92                self::getPoolStatsKey( $type, $isSuccess ),
93                intval( 1000 * ( microtime( true ) - $startPoolWork ) )
94            );
95
96            return $callback( ...func_get_args() );
97        };
98    }
99
100    /**
101     * Wraps the complex pool counter interface to force the single call pattern
102     * that Cirrus always uses.
103     *
104     * @param string $type same as type parameter on PoolCounter::factory
105     * @param UserIdentity|null $user
106     * @param callable $workCallback callback when pool counter is acquired.  Called with
107     *  no parameters.
108     * @param string|null $busyErrorMsg The i18n key to return when the queue
109     *  is full, or null to use the default.
110     * @return mixed
111     */
112    public static function doPoolCounterWork( $type, $user, $workCallback, $busyErrorMsg = null ) {
113        global $wgCirrusSearchPoolCounterKey;
114
115        // By default the pool counter allows you to lock the same key with
116        // multiple types.  That might be useful but it isn't how Cirrus thinks.
117        // Instead, all keys are scoped to their type.
118
119        if ( !$user ) {
120            // We don't want to even use the pool counter if there isn't a user.
121            // Note that anonymous users are still users, this is most likely
122            // maintenance scripts.
123            // @todo Maintenenace scripts and jobs should already override
124            // poolcounters as necessary, can this be removed?
125            return $workCallback();
126        }
127
128        $key = "$type:$wgCirrusSearchPoolCounterKey";
129
130        $errorCallback = static function ( Status $status ) use ( $key, $busyErrorMsg ) {
131            /** @todo No good replacements for getErrorsArray */
132            $errors = $status->getErrorsArray();
133            $error = $errors[0][0];
134
135            LoggerFactory::getInstance( 'CirrusSearch' )->warning(
136                "Pool error on {key}:  {error}",
137                [ 'key' => $key, 'error' => $error ]
138            );
139            if ( $error === 'pool-queuefull' ) {
140                return Status::newFatal( $busyErrorMsg ?: 'cirrussearch-too-busy-error' );
141            }
142            return Status::newFatal( 'cirrussearch-backend-error' );
143        };
144
145        // wrap some stats collection on the success/failure handlers
146        $startPoolWork = microtime( true );
147        $workCallback = self::wrapWithPoolStats( $startPoolWork, $type, true, $workCallback );
148        $errorCallback = self::wrapWithPoolStats( $startPoolWork, $type, false, $errorCallback );
149
150        $work = new PoolCounterWorkViaCallback( $type, $key, [
151            'doWork' => $workCallback,
152            'error' => $errorCallback,
153        ] );
154        return $work->execute();
155    }
156
157    /**
158     * @param string $str
159     * @return float
160     */
161    public static function parsePotentialPercent( $str ) {
162        $result = floatval( $str );
163        if ( strpos( $str, '%' ) === false ) {
164            return (float)$result;
165        }
166        return $result / 100;
167    }
168
169    /**
170     * Parse a message content into an array. This function is generally used to
171     * parse settings stored as i18n messages (see cirrussearch-boost-templates).
172     *
173     * @param string $message
174     * @return string[]
175     */
176    public static function parseSettingsInMessage( $message ) {
177        $lines = explode( "\n", $message );
178        $lines = preg_replace( '/#.*$/', '', $lines ); // Remove comments
179        $lines = array_map( 'trim', $lines );          // Remove extra spaces
180        $lines = array_filter( $lines );               // Remove empty lines
181        return $lines;
182    }
183
184    /**
185     * Test if $string ends with $suffix
186     *
187     * @param string $string string to test
188     * @param string $suffix
189     * @return bool true if $string ends with $suffix
190     */
191    public static function endsWith( $string, $suffix ) {
192        $strlen = strlen( $string );
193        $suffixlen = strlen( $suffix );
194        if ( $suffixlen > $strlen ) {
195            return false;
196        }
197        return substr_compare( $string, $suffix, $strlen - $suffixlen, $suffixlen ) === 0;
198    }
199
200    /**
201     * Set $dest to the true/false from $request->getVal( $name ) if yes/no.
202     *
203     * @param mixed &$dest
204     * @param WebRequest $request
205     * @param string $name
206     */
207    public static function overrideYesNo( &$dest, $request, $name ) {
208        $val = $request->getVal( $name );
209        if ( $val !== null ) {
210            $dest = wfStringToBool( $val );
211        }
212    }
213
214    /**
215     * Set $dest to the numeric value from $request->getVal( $name ) if it is <= $limit
216     * or => $limit if upperLimit is false.
217     *
218     * @param mixed &$dest
219     * @param WebRequest $request
220     * @param string $name
221     * @param int|null $limit
222     * @param bool $upperLimit
223     */
224    public static function overrideNumeric( &$dest, $request, $name, $limit = null, $upperLimit = true ) {
225        $val = $request->getVal( $name );
226        if ( $val !== null && is_numeric( $val ) ) {
227            if ( !isset( $limit ) ) {
228                $dest = $val;
229            } elseif ( $upperLimit && $val <= $limit ) {
230                $dest = $val;
231            } elseif ( !$upperLimit && $val >= $limit ) {
232                $dest = $val;
233            }
234        }
235    }
236
237    /**
238     * Get boost templates configured in messages.
239     * @param SearchConfig|null $config Search config requesting the templates
240     * @return float[]
241     */
242    public static function getDefaultBoostTemplates( SearchConfig $config = null ) {
243        $config ??= MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'CirrusSearch' );
244
245        $fromConfig = $config->get( 'CirrusSearchBoostTemplates' );
246        if ( $config->get( 'CirrusSearchIgnoreOnWikiBoostTemplates' ) ) {
247            // on wiki messages disabled, we can return this config
248            // directly
249            return $fromConfig;
250        }
251
252        $fromMessage = self::getOnWikiBoostTemplates( $config );
253        if ( empty( $fromMessage ) ) {
254            // the onwiki config is empty (or unknown for non-local
255            // config), we can fallback to templates from config
256            return $fromConfig;
257        }
258        return $fromMessage;
259    }
260
261    /**
262     * Load and cache boost templates configured on wiki via the system
263     * message 'cirrussearch-boost-templates'.
264     * If called from the local wiki the message will be cached.
265     * If called from a non local wiki an attempt to fetch this data from the cache is made.
266     * If an empty array is returned it means that no config is available on wiki
267     * or the value possibly unknown if run from a non local wiki.
268     *
269     * @param SearchConfig $config
270     * @return float[] indexed by template name
271     */
272    private static function getOnWikiBoostTemplates( SearchConfig $config ) {
273        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
274        $cacheKey = $cache->makeGlobalKey( 'cirrussearch-boost-templates', $config->getWikiId() );
275        if ( $config->getWikiId() == WikiMap::getCurrentWikiId() ) {
276            // Local wiki we can fetch boost templates from system
277            // message
278            if ( self::$defaultBoostTemplates !== null ) {
279                // This static cache is never set with non-local
280                // wiki data.
281                return self::$defaultBoostTemplates;
282            }
283
284            $templates = $cache->getWithSetCallback(
285                $cacheKey,
286                600,
287                static function () {
288                    $source = wfMessage( 'cirrussearch-boost-templates' )->inContentLanguage();
289                    if ( !$source->isDisabled() ) {
290                        $lines = Util::parseSettingsInMessage( $source->plain() );
291                        // Now parse the templates
292                        return Query\BoostTemplatesFeature::parseBoostTemplates( implode( ' ', $lines ) );
293                    }
294                    return [];
295                }
296            );
297            self::$defaultBoostTemplates = $templates;
298            return $templates;
299        }
300        // Here we're dealing with boost template from other wiki, try to fetch it if it exists
301        // otherwise, don't bother.
302        $nonLocalCache = $cache->get( $cacheKey );
303        if ( !is_array( $nonLocalCache ) ) {
304            // not yet in cache, value is unknown
305            // return empty array
306            return [];
307        }
308        return $nonLocalCache;
309    }
310
311    /**
312     * Strip question marks from queries, according to the defined stripping
313     * level, defined by $wgCirrusSearchStripQuestionMarks. Strip all ?s, those
314     * at word breaks, or only string-final. Ignore queries that are all
315     * punctuation or use insource. Don't remove escaped \?s, but unescape them.
316     *
317     * @param string $term
318     * @param string $strippingLevel Either "all", "break", or "final"
319     * @return string modified term, based on strippingLevel
320     */
321    public static function stripQuestionMarks( $term, $strippingLevel ) {
322        if ( strpos( $term, 'insource:/' ) === false &&
323             strpos( $term, 'intitle:/' ) === false &&
324            !preg_match( '/^[\p{P}\p{Z}]+$/u', $term )
325        ) {
326            // FIXME: get rid of negative lookbehinds on (?<!\\\\)
327            // it may improperly transform \\? into \? instead of \\ and destroy properly escaped \
328            if ( $strippingLevel === 'final' ) {
329                // strip only query-final question marks that are not escaped
330                $term = preg_replace( "/((?<!\\\\)\?|\s)+$/", '', $term );
331                $term = preg_replace( '/\\\\\?/', '?', $term );
332            } elseif ( $strippingLevel === 'break' ) {
333                // strip question marks at word boundaries
334                $term = preg_replace( '/(?<!\\\\)\?+(\PL|$)/', '$1', $term );
335                $term = preg_replace( '/\\\\\?/', '?', $term );
336            } elseif ( $strippingLevel === 'all' ) {
337                // strip all unescaped question marks
338                $term = preg_replace( '/(?<!\\\\)\?+/', ' ', $term );
339                $term = preg_replace( '/\\\\\?/', '?', $term );
340            }
341        }
342        return $term;
343    }
344
345    /**
346     * Identifies a specific execution of php. That might be one web
347     * request, or multiple jobs run in the same executor. An execution id
348     * is valid over a brief timespan, perhaps a minute or two for some jobs.
349     *
350     * @return string unique identifier
351     */
352    public static function getExecutionId() {
353        if ( self::$executionId === null ) {
354            self::$executionId = (string)mt_rand();
355        }
356        return self::$executionId;
357    }
358
359    /**
360     * Unit tests only
361     */
362    public static function resetExecutionId() {
363        self::$executionId = null;
364    }
365
366    /**
367     * Get a token that (hopefully) uniquely identifies this search. It will be
368     * added to the search result page js config vars, and put into the url with
369     * history.replaceState(). This means click through's from supported browsers
370     * will record this token as part of the referrer.
371     *
372     * @return string
373     */
374    public static function getRequestSetToken() {
375        static $token;
376        if ( $token === null ) {
377            // random UID, 70B tokens have a collision probability of 4*10^-16
378            // so should work for marking unique queries.
379            $uuid = UIDGenerator::newUUIDv4();
380            // make it a little shorter by using straight base36
381            $hex = substr( $uuid, 0, 8 ) . substr( $uuid, 9, 4 ) .
382                substr( $uuid, 14, 4 ) . substr( $uuid, 19, 4 ) .
383                substr( $uuid, 24 );
384            $token = \Wikimedia\base_convert( $hex, 16, 36 );
385        }
386        return $token;
387    }
388
389    /**
390     * @param string $extraData Extra information to mix into the hash
391     * @return string A token that identifies the source of the request
392     */
393    public static function generateIdentToken( $extraData = '' ) {
394        $request = \RequestContext::getMain()->getRequest();
395        try {
396            $ip = $request->getIP();
397        } catch ( \MWException $e ) {
398            // No ip, probably running cli?
399            $ip = 'unknown';
400        }
401        return md5( implode( ':', [
402            $extraData,
403            $ip,
404            $request->getHeader( 'X-Forwarded-For' ),
405            $request->getHeader( 'User-Agent' ),
406        ] ) );
407    }
408
409    /**
410     * @return string The context the request is in. Either cli, api, web or misc.
411     */
412    public static function getExecutionContext() {
413        if ( PHP_SAPI === 'cli' ) {
414            return 'cli';
415        } elseif ( MW_ENTRY_POINT == 'api' ) {
416            return 'api';
417        } elseif ( MW_ENTRY_POINT == 'index' ) {
418            return 'web';
419        } else {
420            return 'misc';
421        }
422    }
423
424    /**
425     * Identify a namespace by attempting some unicode folding techniques.
426     * 2 methods supported:
427     * - naive: case folding + naive accents removal (only some combined accents are removed)
428     * - utr30: (slow to load) case folding + strong accent squashing based on the withdrawn UTR30 specs
429     * all methods will apply something similar to near space flattener.
430     * @param string $namespace name of the namespace to identify
431     * @param string $method either naive or utr30
432     * @param \Language|null $language
433     * @return bool|int
434     */
435    public static function identifyNamespace( $namespace, $method = 'naive', \Language $language = null ) {
436        static $naive = null;
437        static $utr30 = null;
438
439        $normalizer = null;
440        if ( $method === 'naive' ) {
441            if ( $naive === null ) {
442                $naive = \Transliterator::createFromRules(
443                    '::NFD;::Upper;::Lower;::[:Nonspacing Mark:] Remove;::NFC;[\_\-\'\u2019\u02BC]>\u0020;'
444                );
445            }
446            $normalizer = $naive;
447        } elseif ( $method === 'utr30' ) {
448            if ( $utr30 === null ) {
449                $utr30 =
450                $normalizer = \Transliterator::createFromRules( file_get_contents( __DIR__ . '/../data/utr30.txt' ) );
451            }
452            $normalizer = $utr30;
453        }
454
455        Assert::postcondition( $normalizer !== null,
456            'Failed to load Transliterator with method ' . $method );
457        $namespace = $normalizer->transliterate( $namespace );
458        if ( $namespace === '' ) {
459            return false;
460        }
461        $language ??= MediaWikiServices::getInstance()->getContentLanguage();
462        foreach ( $language->getNamespaceIds() as $candidate => $nsId ) {
463            if ( $normalizer->transliterate( $candidate ) === $namespace ) {
464                return $nsId;
465            }
466        }
467
468        return false;
469    }
470
471    /**
472     * Helper for PHP's annoying emptiness check.
473     * empty(0) should not be true!
474     * empty(false) should not be true!
475     * Empty arrays, strings, and nulls/undefined count as empty.
476     *
477     * False otherwise.
478     * @param mixed $v
479     * @return bool
480     */
481    public static function isEmpty( $v ) {
482        return ( is_array( $v ) && count( $v ) === 0 ) ||
483            ( is_object( $v ) && count( (array)$v ) === 0 ) ||
484            ( is_string( $v ) && strlen( $v ) === 0 ) ||
485            ( $v === null );
486    }
487
488    /**
489     * Helper function to conditionally set a key in a dest array only if it
490     * is defined in a source array.  This is just to help DRY up what would
491     * otherwise could be a long series of
492     * if ( isset($sourceArray[$key] )) { $destArray[$key] = $sourceArray[$key] }
493     * statements.  This also supports using a different key in the dest array,
494     * as well as mapping the value when assigning to $sourceArray.
495     *
496     * Usage:
497     * $arr1 = ['KEY1' => '123'];
498     * $arr2 = [];
499     *
500     * setIfDefined($arr1, 'KEY1', $arr2, 'key1', 'intval');
501     * // $arr2['key1'] is now set to 123 (integer value)
502     *
503     * setIfDefined($arr1, 'KEY2', $arr2);
504     * // $arr2 stays the same, because $arr1 does not have 'KEY2' defined.
505     *
506     * @param array $sourceArray the array from which to look for $sourceKey
507     * @param string $sourceKey the key to look for in $sourceArray
508     * @param array &$destArray by reference destination array in which to set value if defined
509     * @param string|null $destKey optional, key to use instead of $sourceKey in $destArray.
510     * @param callable|null $mapFn optional, If set, this will be called on the value before setting it.
511     * @param bool $checkEmpty If false, emptyiness of result after $mapFn is called will not be
512     *                 checked before setting on $destArray.  If true, it will, using Util::isEmpty.
513     *                 Default: true
514     * @return array
515     */
516    public static function setIfDefined(
517        array $sourceArray,
518        $sourceKey,
519        array &$destArray,
520        $destKey = null,
521        $mapFn = null,
522        $checkEmpty = true
523    ) {
524        if ( array_key_exists( $sourceKey, $sourceArray ) ) {
525            $val = $sourceArray[$sourceKey];
526            if ( $mapFn !== null ) {
527                $val = $mapFn( $val );
528            }
529            // Only set in $destArray if we are not checking emptiness,
530            // or if we are and the $val is not empty.
531            if ( !$checkEmpty || !self::isEmpty( $val ) ) {
532                $key = $destKey ?: $sourceKey;
533                $destArray[$key] = $val;
534            }
535        }
536        return $destArray;
537    }
538
539    /**
540     * @return IBufferingStatsdDataFactory
541     */
542    public static function getStatsDataFactory(): IBufferingStatsdDataFactory {
543        if ( defined( 'MW_PHPUNIT_TEST' ) ) {
544            return new NullStatsdDataFactory();
545        }
546        return MediaWikiServices::getInstance()->getStatsdDataFactory();
547    }
548
549    /**
550     * @param SearchConfig $config Configuration of the check
551     * @param string $ip The address to check against, ipv4 or ipv6.
552     * @param string $userAgent Http user agent of the request
553     * @return bool True when the parameters appear to be a non-interactive use case.
554     */
555    public static function looksLikeAutomation( SearchConfig $config, string $ip, string $userAgent ): bool {
556        // Does the user agent have an automation-like user agent, such as
557        // HeadlessChrome or a popular http client package for various
558        // languages?
559        $uaPattern = $config->get( 'CirrusSearchAutomationUserAgentRegex' );
560        if ( $uaPattern !== null ) {
561            $ret = preg_match( $uaPattern, $userAgent );
562            if ( $ret === 1 ) {
563                return true;
564            } elseif ( $ret === false ) {
565                LoggerFactory::getInstance( 'CirrusSearch' )->warning(
566                    'Invalid regex provided in `CirrusSearchAutomationUserAgentRegex`.' );
567                return false;
568            }
569        }
570
571        // Does the ip address fall into a subnet known for automation?
572        $ranges = $config->get( 'CirrusSearchAutomationCIDRs' );
573        if ( IPUtils::isInRanges( $ip, $ranges ) ) {
574            return true;
575        }
576
577        // Default assumption that requests are interactive
578        return false;
579    }
580
581    /**
582     * Recreation of Index::getMapping but with support for include_type_name.
583     *
584     * Should be removed once 7.x is the minimum supported version and all
585     * callers have transitioned to includeTypeName === false.
586     *
587     * @param Index $index
588     * @return array
589     */
590    public static function getIndexMapping( Index $index ) {
591        // $index->getMapping() does not support passing include_type_name so we rely on low-level
592        // elasticsearch/elasticsearch endpoints
593        // It should be fine to remove this while we no longer support es6 which defaults this value
594        // to true.
595        $response = $index->requestEndpoint( ( new GetMapping() )->setParams( [ 'include_type_name' => 'false' ] ) );
596        $data = $response->getData();
597        // $data is single element array with the backing index name as key
598        $mapping = array_shift( $data );
599        return $mapping['mappings'] ?? [];
600    }
601
602    /**
603     * If we're supposed to create raw result, create and return it,
604     * or output it and finish.
605     * @param mixed $result Search result data
606     * @param WebRequest $request Request context
607     * @param CirrusDebugOptions $debugOptions
608     * @return string The new raw result.
609     */
610    public static function processSearchRawReturn( $result, WebRequest $request,
611                                                   CirrusDebugOptions $debugOptions ) {
612        $output = null;
613        $header = null;
614        if ( $debugOptions->getCirrusExplainFormat() !== null ) {
615            $header = 'Content-type: text/html; charset=UTF-8';
616            $printer = new ExplainPrinter( $debugOptions->getCirrusExplainFormat() );
617            $output = $printer->format( $result );
618        }
619
620        // This should always be true, except in the case of the test suite which wants the actual
621        // objects returned.
622        if ( $debugOptions->isDumpAndDie() ) {
623            if ( $output === null ) {
624                $header = 'Content-type: application/json; charset=UTF-8';
625                if ( $result === null ) {
626                    $output = '{}';
627                } else {
628                    $output = json_encode( $result, JSON_PRETTY_PRINT );
629                }
630            }
631
632            // When dumping the query we skip _everything_ but echoing the query.
633            \RequestContext::getMain()->getOutput()->disable();
634            // @phan-suppress-next-line PhanTypeMismatchArgumentNullable $header can't be null here
635            $request->response()->header( $header );
636            echo $output;
637            exit();
638        }
639
640        return $result;
641    }
642}