Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.13% covered (warning)
83.13%
69 / 83
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
23use MediaWiki\Context\IContextSource;
24use Wikimedia\ParamValidator\ParamValidator;
25use Wikimedia\ParamValidator\TypeDef\IntegerDef;
26
27/**
28 * Traits for API components that use a SearchEngine.
29 * @ingroup API
30 */
31trait SearchApi {
32
33    private SearchEngineConfig $searchEngineConfig;
34    private SearchEngineFactory $searchEngineFactory;
35
36    private function checkDependenciesSet() {
37        // Since this is a trait, we can't have a constructor where the services
38        // that we need are injected. Instead, the api modules that use this trait
39        // are responsible for setting them (since api modules *can* have services
40        // injected). Double check that the api module did indeed set them
41        // @phan-suppress-next-line PhanRedundantCondition Phan trusts the type hints too much
42        if ( !isset( $this->searchEngineConfig ) || !isset( $this->searchEngineFactory ) ) {
43            throw new LogicException(
44                'SearchApi requires both a SearchEngineConfig and SearchEngineFactory to be set'
45            );
46        }
47    }
48
49    /**
50     * When $wgSearchType is null, $wgSearchAlternatives[0] is null. Null isn't
51     * a valid option for an array for PARAM_TYPE, so we'll use a fake name
52     * that can't possibly be a class name and describes what the null behavior
53     * does
54     */
55    private static $BACKEND_NULL_PARAM = 'database-backed';
56
57    /**
58     * The set of api parameters that are shared between api calls that
59     * call the SearchEngine. Primarily this defines parameters that
60     * are utilized by self::buildSearchEngine().
61     *
62     * @param bool $isScrollable True if the api offers scrolling
63     * @return array
64     */
65    public function buildCommonApiParams( $isScrollable = true ) {
66        $this->checkDependenciesSet();
67
68        $params = [
69            'search' => [
70                ParamValidator::PARAM_TYPE => 'string',
71                ParamValidator::PARAM_REQUIRED => true,
72            ],
73            'namespace' => [
74                ParamValidator::PARAM_DEFAULT => NS_MAIN,
75                ParamValidator::PARAM_TYPE => 'namespace',
76                ParamValidator::PARAM_ISMULTI => true,
77            ],
78            'limit' => [
79                ParamValidator::PARAM_DEFAULT => 10,
80                ParamValidator::PARAM_TYPE => 'limit',
81                IntegerDef::PARAM_MIN => 1,
82                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
83                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2,
84            ],
85        ];
86        if ( $isScrollable ) {
87            $params['offset'] = [
88                ParamValidator::PARAM_DEFAULT => 0,
89                IntegerDef::PARAM_MIN => 0,
90                ParamValidator::PARAM_TYPE => 'integer',
91                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
92            ];
93        }
94
95        $alternatives = $this->searchEngineConfig->getSearchTypes();
96        if ( count( $alternatives ) > 1 ) {
97            $alternatives[0] ??= self::$BACKEND_NULL_PARAM;
98            $params['backend'] = [
99                ParamValidator::PARAM_DEFAULT => $this->searchEngineConfig->getSearchType(),
100                ParamValidator::PARAM_TYPE => $alternatives,
101            ];
102            // @todo: support profile selection when multiple
103            // backends are available. The solution could be to
104            // merge all possible profiles and let ApiBase
105            // subclasses do the check. Making ApiHelp and ApiSandbox
106            // comprehensive might be more difficult.
107        } else {
108            $params += $this->buildProfileApiParam();
109        }
110
111        return $params;
112    }
113
114    /**
115     * Build the profile api param definitions. Makes bold assumption only one search
116     * engine is available, ensure that is true before calling.
117     *
118     * @return array array containing available additional api param definitions.
119     *  Empty if profiles are not supported by the searchEngine implementation.
120     * @suppress PhanTypeMismatchDimFetch
121     */
122    private function buildProfileApiParam() {
123        $this->checkDependenciesSet();
124
125        $configs = $this->getSearchProfileParams();
126        $searchEngine = $this->searchEngineFactory->create();
127        $params = [];
128        foreach ( $configs as $paramName => $paramConfig ) {
129            $profiles = $searchEngine->getProfiles(
130                $paramConfig['profile-type'],
131                $this->getContext()->getUser()
132            );
133            if ( !$profiles ) {
134                continue;
135            }
136
137            $types = [];
138            $helpMessages = [];
139            $defaultProfile = null;
140            foreach ( $profiles as $profile ) {
141                $types[] = $profile['name'];
142                if ( isset( $profile['desc-message'] ) ) {
143                    $helpMessages[$profile['name']] = $profile['desc-message'];
144                }
145
146                if ( !empty( $profile['default'] ) ) {
147                    $defaultProfile = $profile['name'];
148                }
149            }
150
151            $params[$paramName] = [
152                ParamValidator::PARAM_TYPE => $types,
153                ApiBase::PARAM_HELP_MSG => $paramConfig['help-message'],
154                ApiBase::PARAM_HELP_MSG_PER_VALUE => $helpMessages,
155                ParamValidator::PARAM_DEFAULT => $defaultProfile,
156            ];
157        }
158
159        return $params;
160    }
161
162    /**
163     * Build the search engine to use.
164     * If $params is provided then the following searchEngine options
165     * will be set:
166     *  - backend: which search backend to use
167     *  - limit: mandatory
168     *  - offset: optional
169     *  - namespace: mandatory
170     *  - search engine profiles defined by SearchApi::getSearchProfileParams()
171     * @param array|null $params API request params (must be sanitized by
172     * ApiBase::extractRequestParams() before)
173     * @return SearchEngine
174     */
175    public function buildSearchEngine( array $params = null ) {
176        $this->checkDependenciesSet();
177
178        if ( $params == null ) {
179            return $this->searchEngineFactory->create();
180        }
181
182        $type = $params['backend'] ?? null;
183        if ( $type === self::$BACKEND_NULL_PARAM ) {
184            $type = null;
185        }
186        $searchEngine = $this->searchEngineFactory->create( $type );
187        $searchEngine->setNamespaces( $params['namespace'] );
188        $searchEngine->setLimitOffset( $params['limit'], $params['offset'] ?? 0 );
189
190        // Initialize requested search profiles.
191        $configs = $this->getSearchProfileParams();
192        foreach ( $configs as $paramName => $paramConfig ) {
193            if ( isset( $params[$paramName] ) ) {
194                $searchEngine->setFeatureData(
195                    $paramConfig['profile-type'],
196                    $params[$paramName]
197                );
198            }
199        }
200        return $searchEngine;
201    }
202
203    /**
204     * @return array[] array of arrays mapping from parameter name to a two value map
205     *  containing 'help-message' and 'profile-type' keys.
206     */
207    abstract public function getSearchProfileParams();
208
209    /**
210     * @return IContextSource
211     */
212    abstract public function getContext();
213}