MediaWiki master
ApiOpenSearch.php
Go to the documentation of this file.
1<?php
25namespace MediaWiki\Api;
26
27use InvalidArgumentException;
33use SearchEngine;
37
41class ApiOpenSearch extends ApiBase {
42 use \MediaWiki\Api\SearchApi;
43
45 private $format = null;
47 private $fm = null;
48
49 private LinkBatchFactory $linkBatchFactory;
50 private UrlUtils $urlUtils;
51
52 public function __construct(
53 ApiMain $mainModule,
54 string $moduleName,
55 LinkBatchFactory $linkBatchFactory,
56 SearchEngineConfig $searchEngineConfig,
57 SearchEngineFactory $searchEngineFactory,
58 UrlUtils $urlUtils
59 ) {
60 parent::__construct( $mainModule, $moduleName );
61 $this->linkBatchFactory = $linkBatchFactory;
62 // Services needed in SearchApi trait
63 $this->searchEngineConfig = $searchEngineConfig;
64 $this->searchEngineFactory = $searchEngineFactory;
65 $this->urlUtils = $urlUtils;
66 }
67
73 protected function getFormat() {
74 if ( $this->format === null ) {
75 $format = $this->getParameter( 'format' );
76
77 if ( str_ends_with( $format, 'fm' ) ) {
78 $this->format = substr( $format, 0, -2 );
79 $this->fm = 'fm';
80 } else {
81 $this->format = $format;
82 $this->fm = '';
83 }
84 }
85 return $this->format;
86 }
87
88 public function getCustomPrinter() {
89 switch ( $this->getFormat() ) {
90 case 'json':
91 return new ApiOpenSearchFormatJson(
92 $this->getMain(), $this->fm, $this->getParameter( 'warningsaserror' )
93 );
94
95 case 'xml':
96 $printer = $this->getMain()->createPrinterByName( 'xml' . $this->fm );
97 '@phan-var ApiFormatXml $printer';
99 $printer->setRootElement( 'SearchSuggestion' );
100 return $printer;
101
102 default:
103 ApiBase::dieDebug( __METHOD__, "Unsupported format '{$this->getFormat()}'" );
104 }
105 }
106
107 public function execute() {
108 $params = $this->extractRequestParams();
109 $search = $params['search'];
110
111 // Open search results may be stored for a very long time
112 $this->getMain()->setCacheMaxAge(
114 $this->getMain()->setCacheMode( 'public' );
115 $results = $this->search( $search, $params );
116
117 // Allow hooks to populate extracts and images
118 $this->getHookRunner()->onApiOpenSearchSuggest( $results );
119
120 // Trim extracts, if necessary
122 foreach ( $results as &$r ) {
123 if ( is_string( $r['extract'] ) && !$r['extract trimmed'] ) {
124 $r['extract'] = self::trimExtract( $r['extract'], $length );
125 }
126 }
127
128 // Populate result object
129 $this->populateResult( $search, $results );
130 }
131
140 private function search( $search, array $params ) {
141 $searchEngine = $this->buildSearchEngine( $params );
142 $titles = $searchEngine->extractTitles( $searchEngine->completionSearchWithVariants( $search ) );
143 $results = [];
144
145 if ( !$titles ) {
146 return $results;
147 }
148
149 // Special pages need unique integer ids in the return list, so we just
150 // assign them negative numbers because those won't clash with the
151 // always positive articleIds that non-special pages get.
152 $nextSpecialPageId = -1;
153
154 if ( $params['redirects'] === null ) {
155 // Backwards compatibility, don't resolve for JSON.
156 $resolveRedir = $this->getFormat() !== 'json';
157 } else {
158 $resolveRedir = $params['redirects'] === 'resolve';
159 }
160
161 if ( $resolveRedir ) {
162 // Query for redirects
163 $redirects = [];
164 $lb = $this->linkBatchFactory->newLinkBatch( $titles );
165 if ( !$lb->isEmpty() ) {
166 $db = $this->getDB();
167 $res = $db->newSelectQueryBuilder()
168 ->select( [ 'page_namespace', 'page_title', 'rd_namespace', 'rd_title' ] )
169 ->from( 'page' )
170 ->join( 'redirect', null, [ 'rd_from = page_id' ] )
171 ->where( [
172 'rd_interwiki' => '',
173 $lb->constructSet( 'page', $db )
174 ] )
175 ->caller( __METHOD__ )
176 ->fetchResultSet();
177 foreach ( $res as $row ) {
178 $redirects[$row->page_namespace][$row->page_title] =
179 [ $row->rd_namespace, $row->rd_title ];
180 }
181 }
182
183 // Bypass any redirects
184 $seen = [];
185 foreach ( $titles as $title ) {
186 $ns = $title->getNamespace();
187 $dbkey = $title->getDBkey();
188 $from = null;
189 if ( isset( $redirects[$ns][$dbkey] ) ) {
190 [ $ns, $dbkey ] = $redirects[$ns][$dbkey];
191 $from = $title;
192 $title = Title::makeTitle( $ns, $dbkey );
193 }
194 if ( !isset( $seen[$ns][$dbkey] ) ) {
195 $seen[$ns][$dbkey] = true;
196 $resultId = $title->getArticleID();
197 if ( $resultId === 0 ) {
198 $resultId = $nextSpecialPageId;
199 $nextSpecialPageId--;
200 }
201 $results[$resultId] = [
202 'title' => $title,
203 'redirect from' => $from,
204 'extract' => false,
205 'extract trimmed' => false,
206 'image' => false,
207 'url' => (string)$this->urlUtils->expand( $title->getFullURL(), PROTO_CURRENT ),
208 ];
209 }
210 }
211 } else {
212 foreach ( $titles as $title ) {
213 $resultId = $title->getArticleID();
214 if ( $resultId === 0 ) {
215 $resultId = $nextSpecialPageId;
216 $nextSpecialPageId--;
217 }
218 $results[$resultId] = [
219 'title' => $title,
220 'redirect from' => null,
221 'extract' => false,
222 'extract trimmed' => false,
223 'image' => false,
224 'url' => (string)$this->urlUtils->expand( $title->getFullURL(), PROTO_CURRENT ),
225 ];
226 }
227 }
228
229 return $results;
230 }
231
236 protected function populateResult( $search, &$results ) {
237 $result = $this->getResult();
238
239 switch ( $this->getFormat() ) {
240 case 'json':
241 // http://www.opensearch.org/Specifications/OpenSearch/Extensions/Suggestions/1.1
242 $result->addArrayType( null, 'array' );
243 $result->addValue( null, 0, strval( $search ) );
244 $terms = [];
245 $descriptions = [];
246 $urls = [];
247 foreach ( $results as $r ) {
248 $terms[] = $r['title']->getPrefixedText();
249 $descriptions[] = strval( $r['extract'] );
250 $urls[] = $r['url'];
251 }
252 $result->addValue( null, 1, $terms );
253 $result->addValue( null, 2, $descriptions );
254 $result->addValue( null, 3, $urls );
255 break;
256
257 case 'xml':
258 // https://msdn.microsoft.com/en-us/library/cc891508(v=vs.85).aspx
259 $imageKeys = [
260 'source' => true,
261 'alt' => true,
262 'width' => true,
263 'height' => true,
264 'align' => true,
265 ];
266 $items = [];
267 foreach ( $results as $r ) {
268 $item = [
269 'Text' => $r['title']->getPrefixedText(),
270 'Url' => $r['url'],
271 ];
272 if ( is_string( $r['extract'] ) && $r['extract'] !== '' ) {
273 $item['Description'] = $r['extract'];
274 }
275 if ( is_array( $r['image'] ) && isset( $r['image']['source'] ) ) {
276 $item['Image'] = array_intersect_key( $r['image'], $imageKeys );
277 }
278 ApiResult::setSubelementsList( $item, array_keys( $item ) );
279 $items[] = $item;
280 }
281 ApiResult::setIndexedTagName( $items, 'Item' );
282 $result->addValue( null, 'version', '2.0' );
283 $result->addValue( null, 'xmlns', 'http://opensearch.org/searchsuggest2' );
284 $result->addValue( null, 'Query', strval( $search ) );
285 $result->addSubelementsList( null, 'Query' );
286 $result->addValue( null, 'Section', $items );
287 break;
288
289 default:
290 ApiBase::dieDebug( __METHOD__, "Unsupported format '{$this->getFormat()}'" );
291 }
292 }
293
294 public function getAllowedParams() {
295 $allowedParams = $this->buildCommonApiParams( false ) + [
296 'suggest' => [
297 ParamValidator::PARAM_DEFAULT => false,
298 // Deprecated since 1.35
299 ParamValidator::PARAM_DEPRECATED => true,
300 ],
301 'redirects' => [
302 ParamValidator::PARAM_TYPE => [ 'return', 'resolve' ],
304 ApiBase::PARAM_HELP_MSG_APPEND => [ 'apihelp-opensearch-param-redirects-append' ],
305 ],
306 'format' => [
307 ParamValidator::PARAM_DEFAULT => 'json',
308 ParamValidator::PARAM_TYPE => [ 'json', 'jsonfm', 'xml', 'xmlfm' ],
309 ],
310 'warningsaserror' => false,
311 ];
312
313 // Use open search specific default limit
314 $allowedParams['limit'][ParamValidator::PARAM_DEFAULT] = $this->getConfig()->get(
316 );
317
318 return $allowedParams;
319 }
320
321 public function getSearchProfileParams() {
322 return [
323 'profile' => [
324 'profile-type' => SearchEngine::COMPLETION_PROFILE_TYPE,
325 'help-message' => 'apihelp-query+prefixsearch-param-profile'
326 ],
327 ];
328 }
329
330 protected function getExamplesMessages() {
331 return [
332 'action=opensearch&search=Te'
333 => 'apihelp-opensearch-example-te',
334 ];
335 }
336
337 public function getHelpUrls() {
338 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Opensearch';
339 }
340
351 public static function trimExtract( $text, $length ) {
352 static $regex = null;
353
354 if ( $regex === null ) {
355 $endchars = [
356 '([^\d])\.\s', '\!\s', '\?\s', // regular ASCII
357 '。', // full-width ideographic full-stop
358 '.', '!', '?', // double-width roman forms
359 '。', // half-width ideographic full stop
360 ];
361 $endgroup = implode( '|', $endchars );
362 $end = "(?:$endgroup)";
363 $sentence = ".{{$length},}?$end+";
364 $regex = "/^($sentence)/u";
365 }
366
367 $matches = [];
368 if ( preg_match( $regex, $text, $matches ) ) {
369 return trim( $matches[1] );
370 } else {
371 // Just return the first line
372 return trim( explode( "\n", $text )[0] );
373 }
374 }
375
382 public static function getOpenSearchTemplate( $type ) {
383 $services = MediaWikiServices::getInstance();
384 $canonicalServer = $services->getMainConfig()->get( MainConfigNames::CanonicalServer );
385 $searchEngineConfig = $services->getSearchEngineConfig();
386 $ns = implode( '|', $searchEngineConfig->defaultNamespaces() );
387 if ( !$ns ) {
388 $ns = '0';
389 }
390
391 switch ( $type ) {
392 case 'application/x-suggestions+json':
393 return $canonicalServer .
394 wfScript( 'api' ) . '?action=opensearch&search={searchTerms}&namespace=' . $ns;
395
396 case 'application/x-suggestions+xml':
397 return $canonicalServer .
398 wfScript( 'api' ) .
399 '?action=opensearch&format=xml&search={searchTerms}&namespace=' . $ns;
400
401 default:
402 throw new InvalidArgumentException( __METHOD__ . ": Unknown type '$type'" );
403 }
404 }
405}
406
408class_alias( ApiOpenSearch::class, 'ApiOpenSearch' );
const PROTO_CURRENT
Definition Defines.php:215
wfScript( $script='index')
Get the URL path to a MediaWiki entry point.
array $params
The job parameters.
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:76
getHookRunner()
Get an ApiHookRunner for running core API hooks.
Definition ApiBase.php:795
getMain()
Get the main module.
Definition ApiBase.php:589
getResult()
Get the result object.
Definition ApiBase.php:710
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, or 'string' with PARAM_ISMULTI,...
Definition ApiBase.php:224
getDB()
Gets a default replica DB connection object.
Definition ApiBase.php:734
const PARAM_HELP_MSG_APPEND
((string|array|Message)[]) Specify additional i18n messages to append to the normal message for this ...
Definition ApiBase.php:192
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition ApiBase.php:1774
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:851
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
Definition ApiBase.php:973
This is the main API class, used for both external and internal processing.
Definition ApiMain.php:78
__construct(ApiMain $mainModule, string $moduleName, LinkBatchFactory $linkBatchFactory, SearchEngineConfig $searchEngineConfig, SearchEngineFactory $searchEngineFactory, UrlUtils $urlUtils)
getExamplesMessages()
Returns usage examples for this module.
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
getHelpUrls()
Return links to more detailed help pages about the module.
static trimExtract( $text, $length)
Trim an extract to a sensible length.
static getOpenSearchTemplate( $type)
Fetch the template for a type.
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
populateResult( $search, &$results)
getCustomPrinter()
If the module may only be used with a certain format module, it should override this method to return...
getFormat()
Get the output format.
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
static setSubelementsList(array &$arr, $names)
Causes the elements with the specified names to be output as subelements rather than attributes.
A class containing constants representing the names of configuration variables.
const OpenSearchDefaultLimit
Name constant for the OpenSearchDefaultLimit setting, for use with Config::get()
const OpenSearchDescriptionLength
Name constant for the OpenSearchDescriptionLength setting, for use with Config::get()
const CanonicalServer
Name constant for the CanonicalServer setting, for use with Config::get()
const SearchSuggestCacheExpiry
Name constant for the SearchSuggestCacheExpiry setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
Represents a title within MediaWiki.
Definition Title.php:78
A service to expand, parse, and otherwise manipulate URLs.
Definition UrlUtils.php:16
Configuration handling class for SearchEngine.
Factory class for SearchEngine.
Contain a class for special pages.
Service for formatting and validating API parameters.
buildCommonApiParams( $isScrollable=true)
The set of api parameters that are shared between api calls that call the SearchEngine.
Definition SearchApi.php:72
buildSearchEngine(?array $params=null)
Build the search engine to use.