MediaWiki REL1_35
ApiOpenSearch.php
Go to the documentation of this file.
1<?php
26
30class ApiOpenSearch extends ApiBase {
31 use SearchApi;
32
33 private $format = null;
34 private $fm = null;
35
37 private $allowedParams = null;
38
44 protected function getFormat() {
45 if ( $this->format === null ) {
46 $params = $this->extractRequestParams();
47 $format = $params['format'];
48
50 if ( !in_array( $format, $allowedParams['format'][ApiBase::PARAM_TYPE] ) ) {
52 }
53
54 if ( substr( $format, -2 ) === 'fm' ) {
55 $this->format = substr( $format, 0, -2 );
56 $this->fm = 'fm';
57 } else {
58 $this->format = $format;
59 $this->fm = '';
60 }
61 }
62 return $this->format;
63 }
64
65 public function getCustomPrinter() {
66 switch ( $this->getFormat() ) {
67 case 'json':
68 return new ApiOpenSearchFormatJson(
69 $this->getMain(), $this->fm, $this->getParameter( 'warningsaserror' )
70 );
71
72 case 'xml':
73 $printer = $this->getMain()->createPrinterByName( 'xml' . $this->fm );
74 '@phan-var ApiFormatXml $printer';
76 $printer->setRootElement( 'SearchSuggestion' );
77 return $printer;
78
79 default:
80 ApiBase::dieDebug( __METHOD__, "Unsupported format '{$this->getFormat()}'" );
81 }
82 }
83
84 public function execute() {
85 $params = $this->extractRequestParams();
86 $search = $params['search'];
87
88 // Open search results may be stored for a very long time
89 $this->getMain()->setCacheMaxAge( $this->getConfig()->get( 'SearchSuggestCacheExpiry' ) );
90 $this->getMain()->setCacheMode( 'public' );
91 $results = $this->search( $search, $params );
92
93 // Allow hooks to populate extracts and images
94 $this->getHookRunner()->onApiOpenSearchSuggest( $results );
95
96 // Trim extracts, if necessary
97 $length = $this->getConfig()->get( 'OpenSearchDescriptionLength' );
98 foreach ( $results as &$r ) {
99 if ( is_string( $r['extract'] ) && !$r['extract trimmed'] ) {
100 $r['extract'] = self::trimExtract( $r['extract'], $length );
101 }
102 }
103
104 // Populate result object
105 $this->populateResult( $search, $results );
106 }
107
116 private function search( $search, array $params ) {
117 $searchEngine = $this->buildSearchEngine( $params );
118 $titles = $searchEngine->extractTitles( $searchEngine->completionSearchWithVariants( $search ) );
119 $results = [];
120
121 if ( !$titles ) {
122 return $results;
123 }
124
125 // Special pages need unique integer ids in the return list, so we just
126 // assign them negative numbers because those won't clash with the
127 // always positive articleIds that non-special pages get.
128 $nextSpecialPageId = -1;
129
130 if ( $params['redirects'] === null ) {
131 // Backwards compatibility, don't resolve for JSON.
132 $resolveRedir = $this->getFormat() !== 'json';
133 } else {
134 $resolveRedir = $params['redirects'] === 'resolve';
135 }
136
137 if ( $resolveRedir ) {
138 // Query for redirects
139 $redirects = [];
140 $lb = new LinkBatch( $titles );
141 if ( !$lb->isEmpty() ) {
142 $db = $this->getDB();
143 $res = $db->select(
144 [ 'page', 'redirect' ],
145 [ 'page_namespace', 'page_title', 'rd_namespace', 'rd_title' ],
146 [
147 'rd_from = page_id',
148 'rd_interwiki IS NULL OR rd_interwiki = ' . $db->addQuotes( '' ),
149 $lb->constructSet( 'page', $db ),
150 ],
151 __METHOD__
152 );
153 foreach ( $res as $row ) {
154 $redirects[$row->page_namespace][$row->page_title] =
155 [ $row->rd_namespace, $row->rd_title ];
156 }
157 }
158
159 // Bypass any redirects
160 $seen = [];
161 foreach ( $titles as $title ) {
162 $ns = $title->getNamespace();
163 $dbkey = $title->getDBkey();
164 $from = null;
165 if ( isset( $redirects[$ns][$dbkey] ) ) {
166 list( $ns, $dbkey ) = $redirects[$ns][$dbkey];
167 $from = $title;
168 $title = Title::makeTitle( $ns, $dbkey );
169 }
170 if ( !isset( $seen[$ns][$dbkey] ) ) {
171 $seen[$ns][$dbkey] = true;
172 $resultId = $title->getArticleID();
173 if ( $resultId === 0 ) {
174 $resultId = $nextSpecialPageId;
175 $nextSpecialPageId -= 1;
176 }
177 $results[$resultId] = [
178 'title' => $title,
179 'redirect from' => $from,
180 'extract' => false,
181 'extract trimmed' => false,
182 'image' => false,
183 'url' => wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ),
184 ];
185 }
186 }
187 } else {
188 foreach ( $titles as $title ) {
189 $resultId = $title->getArticleID();
190 if ( $resultId === 0 ) {
191 $resultId = $nextSpecialPageId;
192 $nextSpecialPageId -= 1;
193 }
194 $results[$resultId] = [
195 'title' => $title,
196 'redirect from' => null,
197 'extract' => false,
198 'extract trimmed' => false,
199 'image' => false,
200 'url' => wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ),
201 ];
202 }
203 }
204
205 return $results;
206 }
207
212 protected function populateResult( $search, &$results ) {
213 $result = $this->getResult();
214
215 switch ( $this->getFormat() ) {
216 case 'json':
217 // http://www.opensearch.org/Specifications/OpenSearch/Extensions/Suggestions/1.1
218 $result->addArrayType( null, 'array' );
219 $result->addValue( null, 0, strval( $search ) );
220 $terms = [];
221 $descriptions = [];
222 $urls = [];
223 foreach ( $results as $r ) {
224 $terms[] = $r['title']->getPrefixedText();
225 $descriptions[] = strval( $r['extract'] );
226 $urls[] = $r['url'];
227 }
228 $result->addValue( null, 1, $terms );
229 $result->addValue( null, 2, $descriptions );
230 $result->addValue( null, 3, $urls );
231 break;
232
233 case 'xml':
234 // https://msdn.microsoft.com/en-us/library/cc891508(v=vs.85).aspx
235 $imageKeys = [
236 'source' => true,
237 'alt' => true,
238 'width' => true,
239 'height' => true,
240 'align' => true,
241 ];
242 $items = [];
243 foreach ( $results as $r ) {
244 $item = [
245 'Text' => $r['title']->getPrefixedText(),
246 'Url' => $r['url'],
247 ];
248 if ( is_string( $r['extract'] ) && $r['extract'] !== '' ) {
249 $item['Description'] = $r['extract'];
250 }
251 if ( is_array( $r['image'] ) && isset( $r['image']['source'] ) ) {
252 $item['Image'] = array_intersect_key( $r['image'], $imageKeys );
253 }
254 ApiResult::setSubelementsList( $item, array_keys( $item ) );
255 $items[] = $item;
256 }
257 ApiResult::setIndexedTagName( $items, 'Item' );
258 $result->addValue( null, 'version', '2.0' );
259 $result->addValue( null, 'xmlns', 'http://opensearch.org/searchsuggest2' );
260 $result->addValue( null, 'Query', strval( $search ) );
261 $result->addSubelementsList( null, 'Query' );
262 $result->addValue( null, 'Section', $items );
263 break;
264
265 default:
266 ApiBase::dieDebug( __METHOD__, "Unsupported format '{$this->getFormat()}'" );
267 }
268 }
269
270 public function getAllowedParams() {
271 if ( $this->allowedParams !== null ) {
273 }
274 $this->allowedParams = $this->buildCommonApiParams( false ) + [
275 'suggest' => [
276 ApiBase::PARAM_DFLT => false,
277 // Deprecated since 1.35
279 ],
280 'redirects' => [
281 ApiBase::PARAM_TYPE => [ 'return', 'resolve' ],
282 ],
283 'format' => [
284 ApiBase::PARAM_DFLT => 'json',
285 ApiBase::PARAM_TYPE => [ 'json', 'jsonfm', 'xml', 'xmlfm' ],
286 ],
287 'warningsaserror' => false,
288 ];
289
290 // Use open search specific default limit
291 $this->allowedParams['limit'][ApiBase::PARAM_DFLT] = $this->getConfig()->get(
292 'OpenSearchDefaultLimit'
293 );
294
296 }
297
298 public function getSearchProfileParams() {
299 return [
300 'profile' => [
301 'profile-type' => SearchEngine::COMPLETION_PROFILE_TYPE,
302 'help-message' => 'apihelp-query+prefixsearch-param-profile'
303 ],
304 ];
305 }
306
307 protected function getExamplesMessages() {
308 return [
309 'action=opensearch&search=Te'
310 => 'apihelp-opensearch-example-te',
311 ];
312 }
313
314 public function getHelpUrls() {
315 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Opensearch';
316 }
317
328 public static function trimExtract( $text, $length ) {
329 static $regex = null;
330
331 if ( $regex === null ) {
332 $endchars = [
333 '([^\d])\.\s', '\!\s', '\?\s', // regular ASCII
334 '。', // full-width ideographic full-stop
335 '.', '!', '?', // double-width roman forms
336 '。', // half-width ideographic full stop
337 ];
338 $endgroup = implode( '|', $endchars );
339 $end = "(?:$endgroup)";
340 $sentence = ".{{$length},}?$end+";
341 $regex = "/^($sentence)/u";
342 }
343
344 $matches = [];
345 if ( preg_match( $regex, $text, $matches ) ) {
346 return trim( $matches[1] );
347 } else {
348 // Just return the first line
349 return trim( explode( "\n", $text )[0] );
350 }
351 }
352
360 public static function getOpenSearchTemplate( $type ) {
361 $config = MediaWikiServices::getInstance()->getSearchEngineConfig();
362 $template = $config->getConfig()->get( 'OpenSearchTemplate' );
363
364 if ( $template && $type === 'application/x-suggestions+json' ) {
365 return $template;
366 }
367
368 $ns = implode( '|', $config->defaultNamespaces() );
369 if ( !$ns ) {
370 $ns = '0';
371 }
372
373 switch ( $type ) {
374 case 'application/x-suggestions+json':
375 return $config->getConfig()->get( 'CanonicalServer' ) . wfScript( 'api' )
376 . '?action=opensearch&search={searchTerms}&namespace=' . $ns;
377
378 case 'application/x-suggestions+xml':
379 return $config->getConfig()->get( 'CanonicalServer' ) . wfScript( 'api' )
380 . '?action=opensearch&format=xml&search={searchTerms}&namespace=' . $ns;
381
382 default:
383 throw new MWException( __METHOD__ . ": Unknown type '$type'" );
384 }
385 }
386}
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
buildSearchEngine(array $params=null)
Build the search engine to use.
buildCommonApiParams( $isScrollable=true)
The set of api parameters that are shared between api calls that call the SearchEngine.
Definition SearchApi.php:47
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:52
const PARAM_DEPRECATED
Definition ApiBase.php:98
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
Definition ApiBase.php:892
getDB()
Gets a default replica DB connection object Stable to override.
Definition ApiBase.php:650
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition ApiBase.php:1629
getMain()
Get the main module.
Definition ApiBase.php:515
const PARAM_TYPE
Definition ApiBase.php:78
const PARAM_DFLT
Definition ApiBase.php:70
getResult()
Get the result object.
Definition ApiBase.php:620
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:772
getHookRunner()
Get an ApiHookRunner for running core API hooks.
Definition ApiBase.php:717
static trimExtract( $text, $length)
Trim an extract to a sensible length.
array $allowedParams
list of api allowed params
getHelpUrls()
Return links to more detailed help pages about the module.
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
getCustomPrinter()
If the module may only be used with a certain format module, it should override this method to return...
getExamplesMessages()
Returns usage examples for this module.
populateResult( $search, &$results)
getFormat()
Get the output format.
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
static getOpenSearchTemplate( $type)
Fetch the template for a type.
search( $search, array $params)
Perform the search.
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition LinkBatch.php:35
MediaWiki exception.
MediaWikiServices is the service locator for the application scope of MediaWiki.
trait SearchApi
Traits for API components that use a SearchEngine.
Definition SearchApi.php:29
const PROTO_CURRENT
Definition Defines.php:212