Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
75 / 75
100.00% covered (success)
100.00%
19 / 19
CRAP
100.00% covered (success)
100.00%
1 / 1
GlobalCustomFilter
100.00% covered (success)
100.00%
75 / 75
100.00% covered (success)
100.00%
19 / 19
46
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setLanguageDenyList
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setLanguageAllowList
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setRequiredPlugins
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setFallbackFilter
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setApplyToAnalyzers
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getApplyToAnalyzers
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRequiredTokenizer
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setDisallowedTokenFilters
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setDisallowedCharFilters
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setMustFollowFilters
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 enableGlobalCustomFilters
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
7
 languageCheck
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 pluginsAvailable
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 requiredTokenizerUsed
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 disallowedTokenFiltersPresent
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 disallowedCharFiltersPresent
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 analyzerCheck
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
7
 insertGlobalCustomFilter
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3namespace CirrusSearch\Maintenance;
4
5class GlobalCustomFilter {
6    /** @var string filter type, probably 'filter' or 'char_filter'; 'filter' by default */
7    private $type;
8
9    /** @var string[] languages where this filter should not be used, by language codes */
10    private $languageDenyList = [];
11
12    /** @var string[] only languages where this filter should be used, by language codes */
13    private $languageAllowList = [];
14
15    /** @var string[] plugins that must be present to use the filter */
16    private $requiredPlugins = [];
17
18    /** @var string local filter to use instead if requiredPlugins are not available */
19    private $fallbackFilter = '';
20
21    /** @var string[] which analyzers to apply to; 'text' and 'text_search' by default */
22    private $applyToAnalyzers = [ 'text', 'text_search' ];
23
24    /** @var string tokenizer that must be present to use the filter */
25    private $requiredTokenizer = '';
26
27    /** @var string[] token filters with which the filter is not allowed/needed */
28    private $disallowedTokenFilters = [];
29
30    /** @var string[] character filters with which the filter is not allowed/needed */
31    private $disallowedCharFilters = [];
32
33    /** @var string[] filters this one must come after. see T268730 */
34    private $mustFollowFilters = [];
35
36    public function __construct( string $type = 'filter' ) {
37        $this->type = $type;
38    }
39
40    /**
41     * @param string[] $languageDenyList
42     * @return self
43     */
44    public function setLanguageDenyList( array $languageDenyList ): self {
45        $this->languageDenyList = $languageDenyList;
46        return $this;
47    }
48
49    /**
50     * @param string[] $languageAllowList
51     * @return self
52     */
53    public function setLanguageAllowList( array $languageAllowList ): self {
54        $this->languageAllowList = $languageAllowList;
55        return $this;
56    }
57
58    /**
59     * @param string[] $requiredPlugins
60     * @return self
61     */
62    public function setRequiredPlugins( array $requiredPlugins ): self {
63        $this->requiredPlugins = $requiredPlugins;
64        return $this;
65    }
66
67    /**
68     * @param string $fallbackFilter
69     * @return self
70     */
71    public function setFallbackFilter( string $fallbackFilter ): self {
72        $this->fallbackFilter = $fallbackFilter;
73        return $this;
74    }
75
76    /**
77     * @param string[] $applyToAnalyzers
78     * @return self
79     */
80    public function setApplyToAnalyzers( array $applyToAnalyzers ): self {
81        $this->applyToAnalyzers = $applyToAnalyzers;
82        return $this;
83    }
84
85    public function getApplyToAnalyzers() {
86        return $this->applyToAnalyzers;
87    }
88
89    /**
90     * @param string $requiredTokenizer
91     * @return self
92     */
93    public function setRequiredTokenizer( string $requiredTokenizer ): self {
94        $this->requiredTokenizer = $requiredTokenizer;
95        return $this;
96    }
97
98    /**
99     * @param string[] $disallowedTokenFilters
100     * @return self
101     */
102    public function setDisallowedTokenFilters( array $disallowedTokenFilters ): self {
103        $this->disallowedTokenFilters = $disallowedTokenFilters;
104        return $this;
105    }
106
107    /**
108     * @param string[] $disallowedCharFilters
109     * @return self
110     */
111    public function setDisallowedCharFilters( array $disallowedCharFilters ): self {
112        $this->disallowedCharFilters = $disallowedCharFilters;
113        return $this;
114    }
115
116    /**
117     * @param string[] $mustFollowFilters
118     * @return self
119     */
120    public function setMustFollowFilters( array $mustFollowFilters ): self {
121        $this->mustFollowFilters = $mustFollowFilters;
122        return $this;
123    }
124
125    /**
126     * update languages with global custom filters (e.g., homoglyph & nnbsp filters)
127     *
128     * @param mixed[] $config
129     * @param string $language
130     * @param GlobalCustomFilter[] $customFilters list of filters and info
131     * @param string[] $installedPlugins
132     * @return mixed[] updated config
133     */
134    public static function enableGlobalCustomFilters( array $config, string $language,
135            array $customFilters, array $installedPlugins ) {
136        foreach ( $customFilters as $filterName => $gcfInfo ) {
137            if ( !$gcfInfo->languageCheck( $language ) ) {
138                continue;
139            }
140
141            if ( !$gcfInfo->pluginsAvailable( $installedPlugins ) ) {
142                if ( $gcfInfo->fallbackFilter ) {
143                    $filterName = $gcfInfo->fallbackFilter;
144                } else {
145                    continue;
146                }
147            }
148
149            foreach ( $gcfInfo->getApplyToAnalyzers() as $analyzer ) {
150                if ( $gcfInfo->analyzerCheck( $config, $analyzer, $filterName ) ) {
151                    $config = $gcfInfo->insertGlobalCustomFilter( $config, $analyzer,
152                        $filterName );
153                }
154            }
155        }
156
157        return $config;
158    }
159
160    /**
161     * check language deny and allow lists to see if this filter is allowed in this
162     * analyzer
163     *
164     * @param string $language
165     * @return bool
166     */
167    private function languageCheck( string $language ): bool {
168        if ( in_array( $language, $this->languageDenyList )
169             || ( $this->languageAllowList &&
170                !in_array( $language, $this->languageAllowList ) )
171            ) {
172             return false;
173        }
174        return true;
175    }
176
177    /**
178     * check to see if the filter is compatible with the set of installed plugins
179     *
180     * @param string[] $installedPlugins
181     * @return bool
182     */
183    private function pluginsAvailable( array $installedPlugins ): bool {
184        foreach ( $this->requiredPlugins as $reqPlugin ) {
185            if ( !Plugins::contains( $reqPlugin, $installedPlugins ) ) {
186                return false;
187            }
188        }
189        return true;
190    }
191
192    /**
193     * check to see if the filter is compatible with the configured tokenizer
194     *
195     * @param mixed[] $analyzerConfig
196     * @return bool
197     */
198    private function requiredTokenizerUsed( array $analyzerConfig ): bool {
199        if ( $this->requiredTokenizer ) {
200            if ( !array_key_exists( 'tokenizer', $analyzerConfig ) ||
201                    $analyzerConfig[ 'tokenizer' ] != $this->requiredTokenizer ) {
202                return false;
203            }
204        }
205        return true;
206    }
207
208    /**
209     * check if any disqualifying token filters are already present
210     *
211     * @param mixed[] $config
212     * @param string $analyzer
213     * @return bool
214     */
215    private function disallowedTokenFiltersPresent( array $config, string $analyzer ): bool {
216        $filters = $config['analyzer'][$analyzer]['filter'] ?? [];
217        foreach ( $this->disallowedTokenFilters as $df ) {
218            if ( in_array( $df, $filters ) ) {
219                return true;
220            }
221        }
222        return false;
223    }
224
225    /**
226     * check if any disqualifying character filters are already present
227     *
228     * @param mixed[] $config
229     * @param string $analyzer
230     * @return bool
231     */
232    private function disallowedCharFiltersPresent( array $config, string $analyzer ): bool {
233        $filters = $config['analyzer'][$analyzer]['char_filter'] ?? [];
234
235        foreach ( $this->disallowedCharFilters as $df ) {
236            if ( in_array( $df, $filters ) ) {
237                return true;
238            }
239        }
240        return false;
241    }
242
243    /**
244     * check that the analyzer checks all the boxes to insert this filter
245     *
246     * @param mixed[] $config
247     * @param string $analyzer
248     * @param string $filterName filter we want to add
249     * @return bool
250     */
251    private function analyzerCheck( array $config, string $analyzer,
252            string $filterName ): bool {
253        $filters = $config['analyzer'][$analyzer][$this->type] ?? [];
254
255        if ( !array_key_exists( $analyzer, $config['analyzer'] ) // array exists
256            || $config['analyzer'][$analyzer]['type'] != 'custom' // array is custom
257            || !$this->requiredTokenizerUsed( $config['analyzer'][$analyzer] )
258            || $this->disallowedTokenFiltersPresent( $config, $analyzer )
259            || $this->disallowedCharFiltersPresent( $config, $analyzer )
260            || in_array( $filterName, $filters ) // not a duplicate
261            ) {
262            return false;
263        }
264
265        return true;
266    }
267
268    /**
269     * insert one of the global custom filters into the right spot in the analysis chain
270     *
271     * @param mixed[] $config the analysis config we are modifying
272     * @param string $analyzer the specifc analyzer we are modifying
273     * @param string $filterName filter to add
274     * @return mixed[] updated config
275     */
276    private function insertGlobalCustomFilter( array $config, string $analyzer,
277            string $filterName ) {
278        $filters = $config['analyzer'][$analyzer][$this->type] ?? [];
279
280        $lastMustFollow = -1;
281        foreach ( $this->mustFollowFilters as $mustFollow ) {
282            $mustFollowIdx = array_keys( $filters, $mustFollow );
283            $mustFollowIdx = end( $mustFollowIdx );
284            if ( $mustFollowIdx !== false && $mustFollowIdx > $lastMustFollow ) {
285                $lastMustFollow = $mustFollowIdx;
286            }
287        }
288        array_splice( $filters, $lastMustFollow + 1, 0, $filterName );
289
290        $config['analyzer'][$analyzer][$this->type] = $filters;
291
292        return $config;
293    }
294
295}