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';
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 ) {
275  return $this->allowedParams;
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 
294  return $this->allowedParams;
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 }
const PARAM_TYPE
(string|string[]) Either an array of allowed value strings, or a string type as described below...
Definition: ApiBase.php:94
populateResult( $search, &$results)
getResult()
Get the result object.
Definition: ApiBase.php:640
getMain()
Get the main module.
Definition: ApiBase.php:536
const PARAM_DFLT
(null|boolean|integer|string) Default value of the parameter.
Definition: ApiBase.php:55
trait SearchApi
Traits for API components that use a SearchEngine.
Definition: SearchApi.php:29
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
buildCommonApiParams( $isScrollable=true)
The set of api parameters that are shared between api calls that call the SearchEngine.
Definition: SearchApi.php:47
getDB()
Gets a default replica DB connection object.
Definition: ApiBase.php:668
search( $search, array $params)
Perform the search.
const PROTO_CURRENT
Definition: Defines.php:202
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.
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Definition: ApiResult.php:616
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
Definition: ApiBase.php:876
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition: LinkBatch.php:34
static getOpenSearchTemplate( $type)
Fetch the template for a type.
getFormat()
Get the output format.
const COMPLETION_PROFILE_TYPE
Profile type for completionSearch.
static setSubelementsList(array &$arr, $names)
Causes the elements with the specified names to be output as subelements rather than attributes...
Definition: ApiResult.php:565
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:586
buildSearchEngine(array $params=null)
Build the search engine to use.
Definition: SearchApi.php:153
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition: ApiBase.php:2212
This abstract class implements many basic API functions, and is the base of all API classes...
Definition: ApiBase.php:42
array $allowedParams
list of api allowed params
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
$matches