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