Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.14% covered (warning)
82.14%
69 / 84
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
SearchApi
83.13% covered (warning)
83.13%
69 / 83
25.00% covered (danger)
25.00%
1 / 4
18.39
0.00% covered (danger)
0.00%
0 / 1
 checkDependenciesSet
25.00% covered (danger)
25.00%
1 / 4
0.00% covered (danger)
0.00%
0 / 1
6.80
 buildCommonApiParams
85.71% covered (warning)
85.71%
30 / 35
0.00% covered (danger)
0.00%
0 / 1
3.03
 buildProfileApiParam
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
6
 buildSearchEngine
64.71% covered (warning)
64.71%
11 / 17
0.00% covered (danger)
0.00%
0 / 1
6.10
 getSearchProfileParams
n/a
0 / 0
n/a
0 / 0
0
 getContext
n/a
0 / 0
n/a
0 / 0
0
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 * http://www.gnu.org/copyleft/gpl.html
18 *
19 * @file
20 * @since 1.28
21 */
22
23namespace MediaWiki\Api;
24
25use LogicException;
26use MediaWiki\Context\IContextSource;
27use SearchEngine;
28use SearchEngineConfig;
29use SearchEngineFactory;
30use Wikimedia\ParamValidator\ParamValidator;
31use Wikimedia\ParamValidator\TypeDef\IntegerDef;
32
33/**
34 * Traits for API components that use a SearchEngine.
35 * @ingroup API
36 */
37trait SearchApi {
38
39    private SearchEngineConfig $searchEngineConfig;
40    private SearchEngineFactory $searchEngineFactory;
41
42    private function checkDependenciesSet() {
43        // Since this is a trait, we can't have a constructor where the services
44        // that we need are injected. Instead, the api modules that use this trait
45        // are responsible for setting them (since api modules *can* have services
46        // injected). Double check that the api module did indeed set them
47        // @phan-suppress-next-line PhanRedundantCondition Phan trusts the type hints too much
48        if ( !isset( $this->searchEngineConfig ) || !isset( $this->searchEngineFactory ) ) {
49            throw new LogicException(
50                'SearchApi requires both a SearchEngineConfig and SearchEngineFactory to be set'
51            );
52        }
53    }
54
55    /**
56     * When $wgSearchType is null, $wgSearchAlternatives[0] is null. Null isn't
57     * a valid option for an array for PARAM_TYPE, so we'll use a fake name
58     * that can't possibly be a class name and describes what the null behavior
59     * does
60     * @var string
61     */
62    private static $BACKEND_NULL_PARAM = 'database-backed';
63
64    /**
65     * The set of api parameters that are shared between api calls that
66     * call the SearchEngine. Primarily this defines parameters that
67     * are utilized by self::buildSearchEngine().
68     *
69     * @param bool $isScrollable True if the api offers scrolling
70     * @return array
71     */
72    public function buildCommonApiParams( $isScrollable = true ) {
73        $this->checkDependenciesSet();
74
75        $params = [
76            'search' => [
77                ParamValidator::PARAM_TYPE => 'string',
78                ParamValidator::PARAM_REQUIRED => true,
79            ],
80            'namespace' => [
81                ParamValidator::PARAM_DEFAULT => NS_MAIN,
82                ParamValidator::PARAM_TYPE => 'namespace',
83                ParamValidator::PARAM_ISMULTI => true,
84            ],
85            'limit' => [
86                ParamValidator::PARAM_DEFAULT => 10,
87                ParamValidator::PARAM_TYPE => 'limit',
88                IntegerDef::PARAM_MIN => 1,
89                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
90                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2,
91            ],
92        ];
93        if ( $isScrollable ) {
94            $params['offset'] = [
95                ParamValidator::PARAM_DEFAULT => 0,
96                IntegerDef::PARAM_MIN => 0,
97                ParamValidator::PARAM_TYPE => 'integer',
98                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
99            ];
100        }
101
102        $alternatives = $this->searchEngineConfig->getSearchTypes();
103        if ( count( $alternatives ) > 1 ) {
104            $alternatives[0] ??= self::$BACKEND_NULL_PARAM;
105            $params['backend'] = [
106                ParamValidator::PARAM_DEFAULT => $this->searchEngineConfig->getSearchType(),
107                ParamValidator::PARAM_TYPE => $alternatives,
108            ];
109            // @todo: support profile selection when multiple
110            // backends are available. The solution could be to
111            // merge all possible profiles and let ApiBase
112            // subclasses do the check. Making ApiHelp and ApiSandbox
113            // comprehensive might be more difficult.
114        } else {
115            $params += $this->buildProfileApiParam();
116        }
117
118        return $params;
119    }
120
121    /**
122     * Build the profile api param definitions. Makes bold assumption only one search
123     * engine is available, ensure that is true before calling.
124     *
125     * @return array array containing available additional api param definitions.
126     *  Empty if profiles are not supported by the searchEngine implementation.
127     * @suppress PhanTypeMismatchDimFetch
128     */
129    private function buildProfileApiParam() {
130        $this->checkDependenciesSet();
131
132        $configs = $this->getSearchProfileParams();
133        $searchEngine = $this->searchEngineFactory->create();
134        $params = [];
135        foreach ( $configs as $paramName => $paramConfig ) {
136            $profiles = $searchEngine->getProfiles(
137                $paramConfig['profile-type'],
138                $this->getContext()->getUser()
139            );
140            if ( !$profiles ) {
141                continue;
142            }
143
144            $types = [];
145            $helpMessages = [];
146            $defaultProfile = null;
147            foreach ( $profiles as $profile ) {
148                $types[] = $profile['name'];
149                if ( isset( $profile['desc-message'] ) ) {
150                    $helpMessages[$profile['name']] = $profile['desc-message'];
151                }
152
153                if ( !empty( $profile['default'] ) ) {
154                    $defaultProfile = $profile['name'];
155                }
156            }
157
158            $params[$paramName] = [
159                ParamValidator::PARAM_TYPE => $types,
160                ApiBase::PARAM_HELP_MSG => $paramConfig['help-message'],
161                ApiBase::PARAM_HELP_MSG_PER_VALUE => $helpMessages,
162                ParamValidator::PARAM_DEFAULT => $defaultProfile,
163            ];
164        }
165
166        return $params;
167    }
168
169    /**
170     * Build the search engine to use.
171     * If $params is provided then the following searchEngine options
172     * will be set:
173     *  - backend: which search backend to use
174     *  - limit: mandatory
175     *  - offset: optional
176     *  - namespace: mandatory
177     *  - search engine profiles defined by SearchApi::getSearchProfileParams()
178     * @param array|null $params API request params (must be sanitized by
179     * ApiBase::extractRequestParams() before)
180     * @return SearchEngine
181     */
182    public function buildSearchEngine( ?array $params = null ) {
183        $this->checkDependenciesSet();
184
185        if ( $params == null ) {
186            return $this->searchEngineFactory->create();
187        }
188
189        $type = $params['backend'] ?? null;
190        if ( $type === self::$BACKEND_NULL_PARAM ) {
191            $type = null;
192        }
193        $searchEngine = $this->searchEngineFactory->create( $type );
194        $searchEngine->setNamespaces( $params['namespace'] );
195        $searchEngine->setLimitOffset( $params['limit'], $params['offset'] ?? 0 );
196
197        // Initialize requested search profiles.
198        $configs = $this->getSearchProfileParams();
199        foreach ( $configs as $paramName => $paramConfig ) {
200            if ( isset( $params[$paramName] ) ) {
201                $searchEngine->setFeatureData(
202                    $paramConfig['profile-type'],
203                    $params[$paramName]
204                );
205            }
206        }
207        return $searchEngine;
208    }
209
210    /**
211     * @return array[] array of arrays mapping from parameter name to a two value map
212     *  containing 'help-message' and 'profile-type' keys.
213     */
214    abstract public function getSearchProfileParams();
215
216    /**
217     * @return IContextSource
218     */
219    abstract public function getContext();
220}
221
222/** @deprecated class alias since 1.43 */
223class_alias( SearchApi::class, 'SearchApi' );