MediaWiki  master
ApiOpenSearch.php
Go to the documentation of this file.
1 <?php
26 
30 class 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 ) {
272  return $this->allowedParams;
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 
295  return $this->allowedParams;
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 }
ContextSource\getConfig
getConfig()
Definition: ContextSource.php:67
ApiOpenSearch\search
search( $search, array $params)
Perform the search.
Definition: ApiOpenSearch.php:116
ApiOpenSearch\$allowedParams
array $allowedParams
list of api allowed params
Definition: ApiOpenSearch.php:37
LinkBatch
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition: LinkBatch.php:35
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:154
ApiOpenSearch\getFormat
getFormat()
Get the output format.
Definition: ApiOpenSearch.php:44
ApiBase\PARAM_TYPE
const PARAM_TYPE
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition: ApiBase.php:71
ApiBase\getResult
getResult()
Get the result object.
Definition: ApiBase.php:565
ApiOpenSearch
Definition: ApiOpenSearch.php:30
ApiBase\getDB
getDB()
Gets a default replica DB connection object Stable to override.
Definition: ApiBase.php:595
$res
$res
Definition: testCompression.php:57
ApiBase
This abstract class implements many basic API functions, and is the base of all API classes.
Definition: ApiBase.php:52
SearchEngine\COMPLETION_PROFILE_TYPE
const COMPLETION_PROFILE_TYPE
Profile type for completionSearch.
Definition: SearchEngine.php:72
ApiBase\PARAM_DEPRECATED
const PARAM_DEPRECATED
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition: ApiBase.php:76
MWException
MediaWiki exception.
Definition: MWException.php:29
wfScript
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
Definition: GlobalFunctions.php:2534
$matches
$matches
Definition: NoLocalSettings.php:24
ApiOpenSearch\execute
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
Definition: ApiOpenSearch.php:84
buildSearchEngine
buildSearchEngine(array $params=null)
Build the search engine to use.
Definition: SearchApi.php:153
PROTO_CURRENT
const PROTO_CURRENT
Definition: Defines.php:211
ApiBase\extractRequestParams
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:717
$title
$title
Definition: testCompression.php:38
ApiOpenSearch\getCustomPrinter
getCustomPrinter()
If the module may only be used with a certain format module, it should override this method to return...
Definition: ApiOpenSearch.php:65
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:592
ApiOpenSearch\$format
$format
Definition: ApiOpenSearch.php:33
ApiResult\setIndexedTagName
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Definition: ApiResult.php:604
ApiOpenSearch\getSearchProfileParams
getSearchProfileParams()
Definition: ApiOpenSearch.php:298
ApiOpenSearch\getOpenSearchTemplate
static getOpenSearchTemplate( $type)
Fetch the template for a type.
Definition: ApiOpenSearch.php:360
ApiOpenSearch\populateResult
populateResult( $search, &$results)
Definition: ApiOpenSearch.php:212
ApiOpenSearchFormatJson
Definition: ApiOpenSearchFormatJson.php:28
ApiOpenSearch\getAllowedParams
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
Definition: ApiOpenSearch.php:270
ApiOpenSearch\$fm
$fm
Definition: ApiOpenSearch.php:34
ApiBase\PARAM_DFLT
const PARAM_DFLT
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition: ApiBase.php:69
ApiBase\getParameter
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
Definition: ApiBase.php:837
ApiBase\getMain
getMain()
Get the main module.
Definition: ApiBase.php:460
ApiOpenSearch\trimExtract
static trimExtract( $text, $length)
Trim an extract to a sensible length.
Definition: ApiOpenSearch.php:328
ApiOpenSearch\getHelpUrls
getHelpUrls()
Return links to more detailed help pages about the module.
Definition: ApiOpenSearch.php:314
ApiBase\getHookRunner
getHookRunner()
Get an ApiHookRunner for running core API hooks.
Definition: ApiBase.php:662
ApiBase\dieDebug
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition: ApiBase.php:1574
SearchApi
trait SearchApi
Traits for API components that use a SearchEngine.
Definition: SearchApi.php:29
ApiResult\setSubelementsList
static setSubelementsList(array &$arr, $names)
Causes the elements with the specified names to be output as subelements rather than attributes.
Definition: ApiResult.php:553
ApiOpenSearch\getExamplesMessages
getExamplesMessages()
Returns usage examples for this module.
Definition: ApiOpenSearch.php:307
wfExpandUrl
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
Definition: GlobalFunctions.php:490
buildCommonApiParams
buildCommonApiParams( $isScrollable=true)
The set of api parameters that are shared between api calls that call the SearchEngine.
Definition: SearchApi.php:47
$type
$type
Definition: testCompression.php:52