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