MediaWiki REL1_37
ApiOpenSearch.php
Go to the documentation of this file.
1<?php
27
31class ApiOpenSearch extends ApiBase {
32 use SearchApi;
33
34 private $format = null;
35 private $fm = null;
36
38 private $allowedParams = null;
39
42
50 public function __construct(
51 ApiMain $mainModule,
52 $moduleName,
54 SearchEngineConfig $searchEngineConfig,
56 ) {
57 parent::__construct( $mainModule, $moduleName );
58 $this->linkBatchFactory = $linkBatchFactory;
59 // Services needed in SearchApi trait
60 $this->searchEngineConfig = $searchEngineConfig;
61 $this->searchEngineFactory = $searchEngineFactory;
62 }
63
69 protected function getFormat() {
70 if ( $this->format === null ) {
71 $params = $this->extractRequestParams();
72 $format = $params['format'];
73
75 if ( !in_array( $format, $allowedParams['format'][ApiBase::PARAM_TYPE] ) ) {
77 }
78
79 if ( substr( $format, -2 ) === 'fm' ) {
80 $this->format = substr( $format, 0, -2 );
81 $this->fm = 'fm';
82 } else {
83 $this->format = $format;
84 $this->fm = '';
85 }
86 }
87 return $this->format;
88 }
89
90 public function getCustomPrinter() {
91 switch ( $this->getFormat() ) {
92 case 'json':
93 return new ApiOpenSearchFormatJson(
94 $this->getMain(), $this->fm, $this->getParameter( 'warningsaserror' )
95 );
96
97 case 'xml':
98 $printer = $this->getMain()->createPrinterByName( 'xml' . $this->fm );
99 '@phan-var ApiFormatXml $printer';
101 $printer->setRootElement( 'SearchSuggestion' );
102 return $printer;
103
104 default:
105 ApiBase::dieDebug( __METHOD__, "Unsupported format '{$this->getFormat()}'" );
106 }
107 }
108
109 public function execute() {
110 $params = $this->extractRequestParams();
111 $search = $params['search'];
112
113 // Open search results may be stored for a very long time
114 $this->getMain()->setCacheMaxAge( $this->getConfig()->get( 'SearchSuggestCacheExpiry' ) );
115 $this->getMain()->setCacheMode( 'public' );
116 $results = $this->search( $search, $params );
117
118 // Allow hooks to populate extracts and images
119 $this->getHookRunner()->onApiOpenSearchSuggest( $results );
120
121 // Trim extracts, if necessary
122 $length = $this->getConfig()->get( 'OpenSearchDescriptionLength' );
123 foreach ( $results as &$r ) {
124 if ( is_string( $r['extract'] ) && !$r['extract trimmed'] ) {
125 $r['extract'] = self::trimExtract( $r['extract'], $length );
126 }
127 }
128
129 // Populate result object
130 $this->populateResult( $search, $results );
131 }
132
141 private function search( $search, array $params ) {
142 $searchEngine = $this->buildSearchEngine( $params );
143 $titles = $searchEngine->extractTitles( $searchEngine->completionSearchWithVariants( $search ) );
144 $results = [];
145
146 if ( !$titles ) {
147 return $results;
148 }
149
150 // Special pages need unique integer ids in the return list, so we just
151 // assign them negative numbers because those won't clash with the
152 // always positive articleIds that non-special pages get.
153 $nextSpecialPageId = -1;
154
155 if ( $params['redirects'] === null ) {
156 // Backwards compatibility, don't resolve for JSON.
157 $resolveRedir = $this->getFormat() !== 'json';
158 } else {
159 $resolveRedir = $params['redirects'] === 'resolve';
160 }
161
162 if ( $resolveRedir ) {
163 // Query for redirects
164 $redirects = [];
165 $lb = $this->linkBatchFactory->newLinkBatch( $titles );
166 if ( !$lb->isEmpty() ) {
167 $db = $this->getDB();
168 $res = $db->select(
169 [ 'page', 'redirect' ],
170 [ 'page_namespace', 'page_title', 'rd_namespace', 'rd_title' ],
171 [
172 'rd_from = page_id',
173 'rd_interwiki IS NULL OR rd_interwiki = ' . $db->addQuotes( '' ),
174 $lb->constructSet( 'page', $db ),
175 ],
176 __METHOD__
177 );
178 foreach ( $res as $row ) {
179 $redirects[$row->page_namespace][$row->page_title] =
180 [ $row->rd_namespace, $row->rd_title ];
181 }
182 }
183
184 // Bypass any redirects
185 $seen = [];
186 foreach ( $titles as $title ) {
187 $ns = $title->getNamespace();
188 $dbkey = $title->getDBkey();
189 $from = null;
190 if ( isset( $redirects[$ns][$dbkey] ) ) {
191 list( $ns, $dbkey ) = $redirects[$ns][$dbkey];
192 $from = $title;
193 $title = Title::makeTitle( $ns, $dbkey );
194 }
195 if ( !isset( $seen[$ns][$dbkey] ) ) {
196 $seen[$ns][$dbkey] = true;
197 $resultId = $title->getArticleID();
198 if ( $resultId === 0 ) {
199 $resultId = $nextSpecialPageId;
200 $nextSpecialPageId -= 1;
201 }
202 $results[$resultId] = [
203 'title' => $title,
204 'redirect from' => $from,
205 'extract' => false,
206 'extract trimmed' => false,
207 'image' => false,
208 'url' => wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ),
209 ];
210 }
211 }
212 } else {
213 foreach ( $titles as $title ) {
214 $resultId = $title->getArticleID();
215 if ( $resultId === 0 ) {
216 $resultId = $nextSpecialPageId;
217 $nextSpecialPageId -= 1;
218 }
219 $results[$resultId] = [
220 'title' => $title,
221 'redirect from' => null,
222 'extract' => false,
223 'extract trimmed' => false,
224 'image' => false,
225 'url' => wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ),
226 ];
227 }
228 }
229
230 return $results;
231 }
232
237 protected function populateResult( $search, &$results ) {
238 $result = $this->getResult();
239
240 switch ( $this->getFormat() ) {
241 case 'json':
242 // http://www.opensearch.org/Specifications/OpenSearch/Extensions/Suggestions/1.1
243 $result->addArrayType( null, 'array' );
244 $result->addValue( null, 0, strval( $search ) );
245 $terms = [];
246 $descriptions = [];
247 $urls = [];
248 foreach ( $results as $r ) {
249 $terms[] = $r['title']->getPrefixedText();
250 $descriptions[] = strval( $r['extract'] );
251 $urls[] = $r['url'];
252 }
253 $result->addValue( null, 1, $terms );
254 $result->addValue( null, 2, $descriptions );
255 $result->addValue( null, 3, $urls );
256 break;
257
258 case 'xml':
259 // https://msdn.microsoft.com/en-us/library/cc891508(v=vs.85).aspx
260 $imageKeys = [
261 'source' => true,
262 'alt' => true,
263 'width' => true,
264 'height' => true,
265 'align' => true,
266 ];
267 $items = [];
268 foreach ( $results as $r ) {
269 $item = [
270 'Text' => $r['title']->getPrefixedText(),
271 'Url' => $r['url'],
272 ];
273 if ( is_string( $r['extract'] ) && $r['extract'] !== '' ) {
274 $item['Description'] = $r['extract'];
275 }
276 if ( is_array( $r['image'] ) && isset( $r['image']['source'] ) ) {
277 $item['Image'] = array_intersect_key( $r['image'], $imageKeys );
278 }
279 ApiResult::setSubelementsList( $item, array_keys( $item ) );
280 $items[] = $item;
281 }
282 ApiResult::setIndexedTagName( $items, 'Item' );
283 $result->addValue( null, 'version', '2.0' );
284 $result->addValue( null, 'xmlns', 'http://opensearch.org/searchsuggest2' );
285 $result->addValue( null, 'Query', strval( $search ) );
286 $result->addSubelementsList( null, 'Query' );
287 $result->addValue( null, 'Section', $items );
288 break;
289
290 default:
291 ApiBase::dieDebug( __METHOD__, "Unsupported format '{$this->getFormat()}'" );
292 }
293 }
294
295 public function getAllowedParams() {
296 if ( $this->allowedParams !== null ) {
298 }
299 $this->allowedParams = $this->buildCommonApiParams( false ) + [
300 'suggest' => [
301 ApiBase::PARAM_DFLT => false,
302 // Deprecated since 1.35
304 ],
305 'redirects' => [
306 ApiBase::PARAM_TYPE => [ 'return', 'resolve' ],
307 ],
308 'format' => [
309 ApiBase::PARAM_DFLT => 'json',
310 ApiBase::PARAM_TYPE => [ 'json', 'jsonfm', 'xml', 'xmlfm' ],
311 ],
312 'warningsaserror' => false,
313 ];
314
315 // Use open search specific default limit
316 $this->allowedParams['limit'][ApiBase::PARAM_DFLT] = $this->getConfig()->get(
317 'OpenSearchDefaultLimit'
318 );
319
321 }
322
323 public function getSearchProfileParams() {
324 return [
325 'profile' => [
326 'profile-type' => SearchEngine::COMPLETION_PROFILE_TYPE,
327 'help-message' => 'apihelp-query+prefixsearch-param-profile'
328 ],
329 ];
330 }
331
332 protected function getExamplesMessages() {
333 return [
334 'action=opensearch&search=Te'
335 => 'apihelp-opensearch-example-te',
336 ];
337 }
338
339 public function getHelpUrls() {
340 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Opensearch';
341 }
342
353 public static function trimExtract( $text, $length ) {
354 static $regex = null;
355
356 if ( $regex === null ) {
357 $endchars = [
358 '([^\d])\.\s', '\!\s', '\?\s', // regular ASCII
359 '。', // full-width ideographic full-stop
360 '.', '!', '?', // double-width roman forms
361 '。', // half-width ideographic full stop
362 ];
363 $endgroup = implode( '|', $endchars );
364 $end = "(?:$endgroup)";
365 $sentence = ".{{$length},}?$end+";
366 $regex = "/^($sentence)/u";
367 }
368
369 $matches = [];
370 if ( preg_match( $regex, $text, $matches ) ) {
371 return trim( $matches[1] );
372 } else {
373 // Just return the first line
374 return trim( explode( "\n", $text )[0] );
375 }
376 }
377
385 public static function getOpenSearchTemplate( $type ) {
386 $config = MediaWikiServices::getInstance()->getSearchEngineConfig();
387 $template = $config->getConfig()->get( 'OpenSearchTemplate' );
388
389 if ( $template && $type === 'application/x-suggestions+json' ) {
390 return $template;
391 }
392
393 $ns = implode( '|', $config->defaultNamespaces() );
394 if ( !$ns ) {
395 $ns = '0';
396 }
397
398 switch ( $type ) {
399 case 'application/x-suggestions+json':
400 return $config->getConfig()->get( 'CanonicalServer' ) . wfScript( 'api' )
401 . '?action=opensearch&search={searchTerms}&namespace=' . $ns;
402
403 case 'application/x-suggestions+xml':
404 return $config->getConfig()->get( 'CanonicalServer' ) . wfScript( 'api' )
405 . '?action=opensearch&format=xml&search={searchTerms}&namespace=' . $ns;
406
407 default:
408 throw new MWException( __METHOD__ . ": Unknown type '$type'" );
409 }
410 }
411}
const PROTO_CURRENT
Definition Defines.php:195
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:63
SearchEngineFactory null $searchEngineFactory
Definition SearchApi.php:33
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:55
const PARAM_DEPRECATED
Definition ApiBase.php:101
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
Definition ApiBase.php:884
getDB()
Gets a default replica DB connection object.
Definition ApiBase.php:651
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition ApiBase.php:1633
getMain()
Get the main module.
Definition ApiBase.php:513
const PARAM_TYPE
Definition ApiBase.php:81
const PARAM_DFLT
Definition ApiBase.php:73
getResult()
Get the result object.
Definition ApiBase.php:628
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:764
getHookRunner()
Get an ApiHookRunner for running core API hooks.
Definition ApiBase.php:710
This is the main API class, used for both external and internal processing.
Definition ApiMain.php:49
static trimExtract( $text, $length)
Trim an extract to a sensible length.
LinkBatchFactory $linkBatchFactory
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)
__construct(ApiMain $mainModule, $moduleName, LinkBatchFactory $linkBatchFactory, SearchEngineConfig $searchEngineConfig, SearchEngineFactory $searchEngineFactory)
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.
MediaWiki exception.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Configuration handling class for SearchEngine.
Factory class for SearchEngine.
trait SearchApi
Traits for API components that use a SearchEngine.
Definition SearchApi.php:27