MediaWiki REL1_34
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';
75 $printer->setRootElement( 'SearchSuggestion' );
76 return $printer;
77
78 default:
79 ApiBase::dieDebug( __METHOD__, "Unsupported format '{$this->getFormat()}'" );
80 }
81 }
82
83 public function execute() {
84 $params = $this->extractRequestParams();
85 $search = $params['search'];
86 $suggest = $params['suggest'];
87 $results = [];
88 if ( !$suggest || $this->getConfig()->get( 'EnableOpenSearchSuggest' ) ) {
89 // Open search results may be stored for a very long time
90 $this->getMain()->setCacheMaxAge( $this->getConfig()->get( 'SearchSuggestCacheExpiry' ) );
91 $this->getMain()->setCacheMode( 'public' );
92 $results = $this->search( $search, $params );
93
94 // Allow hooks to populate extracts and images
95 Hooks::run( 'ApiOpenSearchSuggest', [ &$results ] );
96
97 // Trim extracts, if necessary
98 $length = $this->getConfig()->get( 'OpenSearchDescriptionLength' );
99 foreach ( $results as &$r ) {
100 // @phan-suppress-next-line PhanTypeInvalidDimOffset
101 if ( is_string( $r['extract'] ) && !$r['extract trimmed'] ) {
102 $r['extract'] = self::trimExtract( $r['extract'], $length );
103 }
104 }
105 }
106
107 // Populate result object
108 $this->populateResult( $search, $results );
109 }
110
119 private function search( $search, array $params ) {
120 $searchEngine = $this->buildSearchEngine( $params );
121 $titles = $searchEngine->extractTitles( $searchEngine->completionSearchWithVariants( $search ) );
122 $results = [];
123
124 if ( !$titles ) {
125 return $results;
126 }
127
128 // Special pages need unique integer ids in the return list, so we just
129 // assign them negative numbers because those won't clash with the
130 // always positive articleIds that non-special pages get.
131 $nextSpecialPageId = -1;
132
133 if ( $params['redirects'] === null ) {
134 // Backwards compatibility, don't resolve for JSON.
135 $resolveRedir = $this->getFormat() !== 'json';
136 } else {
137 $resolveRedir = $params['redirects'] === 'resolve';
138 }
139
140 if ( $resolveRedir ) {
141 // Query for redirects
142 $redirects = [];
143 $lb = new LinkBatch( $titles );
144 if ( !$lb->isEmpty() ) {
145 $db = $this->getDB();
146 $res = $db->select(
147 [ 'page', 'redirect' ],
148 [ 'page_namespace', 'page_title', 'rd_namespace', 'rd_title' ],
149 [
150 'rd_from = page_id',
151 'rd_interwiki IS NULL OR rd_interwiki = ' . $db->addQuotes( '' ),
152 $lb->constructSet( 'page', $db ),
153 ],
154 __METHOD__
155 );
156 foreach ( $res as $row ) {
157 $redirects[$row->page_namespace][$row->page_title] =
158 [ $row->rd_namespace, $row->rd_title ];
159 }
160 }
161
162 // Bypass any redirects
163 $seen = [];
164 foreach ( $titles as $title ) {
165 $ns = $title->getNamespace();
166 $dbkey = $title->getDBkey();
167 $from = null;
168 if ( isset( $redirects[$ns][$dbkey] ) ) {
169 list( $ns, $dbkey ) = $redirects[$ns][$dbkey];
170 $from = $title;
171 $title = Title::makeTitle( $ns, $dbkey );
172 }
173 if ( !isset( $seen[$ns][$dbkey] ) ) {
174 $seen[$ns][$dbkey] = true;
175 $resultId = $title->getArticleID();
176 if ( $resultId === 0 ) {
177 $resultId = $nextSpecialPageId;
178 $nextSpecialPageId -= 1;
179 }
180 $results[$resultId] = [
181 'title' => $title,
182 'redirect from' => $from,
183 'extract' => false,
184 'extract trimmed' => false,
185 'image' => false,
186 'url' => wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ),
187 ];
188 }
189 }
190 } else {
191 foreach ( $titles as $title ) {
192 $resultId = $title->getArticleID();
193 if ( $resultId === 0 ) {
194 $resultId = $nextSpecialPageId;
195 $nextSpecialPageId -= 1;
196 }
197 $results[$resultId] = [
198 'title' => $title,
199 'redirect from' => null,
200 'extract' => false,
201 'extract trimmed' => false,
202 'image' => false,
203 'url' => wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ),
204 ];
205 }
206 }
207
208 return $results;
209 }
210
215 protected function populateResult( $search, &$results ) {
216 $result = $this->getResult();
217
218 switch ( $this->getFormat() ) {
219 case 'json':
220 // http://www.opensearch.org/Specifications/OpenSearch/Extensions/Suggestions/1.1
221 $result->addArrayType( null, 'array' );
222 $result->addValue( null, 0, strval( $search ) );
223 $terms = [];
224 $descriptions = [];
225 $urls = [];
226 foreach ( $results as $r ) {
227 $terms[] = $r['title']->getPrefixedText();
228 $descriptions[] = strval( $r['extract'] );
229 $urls[] = $r['url'];
230 }
231 $result->addValue( null, 1, $terms );
232 $result->addValue( null, 2, $descriptions );
233 $result->addValue( null, 3, $urls );
234 break;
235
236 case 'xml':
237 // https://msdn.microsoft.com/en-us/library/cc891508(v=vs.85).aspx
238 $imageKeys = [
239 'source' => true,
240 'alt' => true,
241 'width' => true,
242 'height' => true,
243 'align' => true,
244 ];
245 $items = [];
246 foreach ( $results as $r ) {
247 $item = [
248 'Text' => $r['title']->getPrefixedText(),
249 'Url' => $r['url'],
250 ];
251 if ( is_string( $r['extract'] ) && $r['extract'] !== '' ) {
252 $item['Description'] = $r['extract'];
253 }
254 if ( is_array( $r['image'] ) && isset( $r['image']['source'] ) ) {
255 $item['Image'] = array_intersect_key( $r['image'], $imageKeys );
256 }
257 ApiResult::setSubelementsList( $item, array_keys( $item ) );
258 $items[] = $item;
259 }
260 ApiResult::setIndexedTagName( $items, 'Item' );
261 $result->addValue( null, 'version', '2.0' );
262 $result->addValue( null, 'xmlns', 'http://opensearch.org/searchsuggest2' );
263 $result->addValue( null, 'Query', strval( $search ) );
264 $result->addSubelementsList( null, 'Query' );
265 $result->addValue( null, 'Section', $items );
266 break;
267
268 default:
269 ApiBase::dieDebug( __METHOD__, "Unsupported format '{$this->getFormat()}'" );
270 }
271 }
272
273 public function getAllowedParams() {
274 if ( $this->allowedParams !== null ) {
276 }
277 $this->allowedParams = $this->buildCommonApiParams( false ) + [
278 'suggest' => false,
279 'redirects' => [
280 ApiBase::PARAM_TYPE => [ 'return', 'resolve' ],
281 ],
282 'format' => [
283 ApiBase::PARAM_DFLT => 'json',
284 ApiBase::PARAM_TYPE => [ 'json', 'jsonfm', 'xml', 'xmlfm' ],
285 ],
286 'warningsaserror' => false,
287 ];
288
289 // Use open search specific default limit
290 $this->allowedParams['limit'][ApiBase::PARAM_DFLT] = $this->getConfig()->get(
291 'OpenSearchDefaultLimit'
292 );
293
295 }
296
297 public function getSearchProfileParams() {
298 return [
299 'profile' => [
300 'profile-type' => SearchEngine::COMPLETION_PROFILE_TYPE,
301 'help-message' => 'apihelp-query+prefixsearch-param-profile'
302 ],
303 ];
304 }
305
306 protected function getExamplesMessages() {
307 return [
308 'action=opensearch&search=Te'
309 => 'apihelp-opensearch-example-te',
310 ];
311 }
312
313 public function getHelpUrls() {
314 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Opensearch';
315 }
316
327 public static function trimExtract( $text, $length ) {
328 static $regex = null;
329
330 if ( $regex === null ) {
331 $endchars = [
332 '([^\d])\.\s', '\!\s', '\?\s', // regular ASCII
333 '。', // full-width ideographic full-stop
334 '.', '!', '?', // double-width roman forms
335 '。', // half-width ideographic full stop
336 ];
337 $endgroup = implode( '|', $endchars );
338 $end = "(?:$endgroup)";
339 $sentence = ".{{$length},}?$end+";
340 $regex = "/^($sentence)/u";
341 }
342
343 $matches = [];
344 if ( preg_match( $regex, $text, $matches ) ) {
345 return trim( $matches[1] );
346 } else {
347 // Just return the first line
348 return trim( explode( "\n", $text )[0] );
349 }
350 }
351
359 public static function getOpenSearchTemplate( $type ) {
360 $config = MediaWikiServices::getInstance()->getSearchEngineConfig();
361 $template = $config->getConfig()->get( 'OpenSearchTemplate' );
362
363 if ( $template && $type === 'application/x-suggestions+json' ) {
364 return $template;
365 }
366
367 $ns = implode( '|', $config->defaultNamespaces() );
368 if ( !$ns ) {
369 $ns = '0';
370 }
371
372 switch ( $type ) {
373 case 'application/x-suggestions+json':
374 return $config->getConfig()->get( 'CanonicalServer' ) . wfScript( 'api' )
375 . '?action=opensearch&search={searchTerms}&namespace=' . $ns;
376
377 case 'application/x-suggestions+xml':
378 return $config->getConfig()->get( 'CanonicalServer' ) . wfScript( 'api' )
379 . '?action=opensearch&format=xml&search={searchTerms}&namespace=' . $ns;
380
381 default:
382 throw new MWException( __METHOD__ . ": Unknown type '$type'" );
383 }
384 }
385}
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:42
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
Definition ApiBase.php:876
getDB()
Gets a default replica DB connection object.
Definition ApiBase.php:668
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition ApiBase.php:2220
getMain()
Get the main module.
Definition ApiBase.php:536
const PARAM_TYPE
(string|string[]) Either an array of allowed value strings, or a string type as described below.
Definition ApiBase.php:94
const PARAM_DFLT
(null|boolean|integer|string) Default value of the parameter.
Definition ApiBase.php:55
getResult()
Get the result object.
Definition ApiBase.php:640
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:761
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:34
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:211