MediaWiki  master
ApiOpenSearch.php
Go to the documentation of this file.
1 <?php
29 
33 class ApiOpenSearch extends ApiBase {
34  use SearchApi;
35 
36  private $format = null;
37  private $fm = null;
38 
40  private $allowedParams = null;
41 
44 
52  public function __construct(
53  ApiMain $mainModule,
54  $moduleName,
56  SearchEngineConfig $searchEngineConfig,
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 
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->select(
172  [ 'page', 'redirect' ],
173  [ 'page_namespace', 'page_title', 'rd_namespace', 'rd_title' ],
174  [
175  'rd_from = page_id',
176  'rd_interwiki IS NULL OR rd_interwiki = ' . $db->addQuotes( '' ),
177  $lb->constructSet( 'page', $db ),
178  ],
179  __METHOD__
180  );
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 -= 1;
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 -= 1;
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' ],
310  ],
311  'format' => [
312  ParamValidator::PARAM_DEFAULT => 'json',
313  ParamValidator::PARAM_TYPE => [ 'json', 'jsonfm', 'xml', 'xmlfm' ],
314  ],
315  'warningsaserror' => false,
316  ];
317 
318  // Use open search specific default limit
319  $this->allowedParams['limit'][ParamValidator::PARAM_DEFAULT] = $this->getConfig()->get(
320  MainConfigNames::OpenSearchDefaultLimit
321  );
322 
323  return $this->allowedParams;
324  }
325 
326  public function getSearchProfileParams() {
327  return [
328  'profile' => [
329  'profile-type' => SearchEngine::COMPLETION_PROFILE_TYPE,
330  'help-message' => 'apihelp-query+prefixsearch-param-profile'
331  ],
332  ];
333  }
334 
335  protected function getExamplesMessages() {
336  return [
337  'action=opensearch&search=Te'
338  => 'apihelp-opensearch-example-te',
339  ];
340  }
341 
342  public function getHelpUrls() {
343  return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Opensearch';
344  }
345 
356  public static function trimExtract( $text, $length ) {
357  static $regex = null;
358 
359  if ( $regex === null ) {
360  $endchars = [
361  '([^\d])\.\s', '\!\s', '\?\s', // regular ASCII
362  '。', // full-width ideographic full-stop
363  '.', '!', '?', // double-width roman forms
364  '。', // half-width ideographic full stop
365  ];
366  $endgroup = implode( '|', $endchars );
367  $end = "(?:$endgroup)";
368  $sentence = ".{{$length},}?$end+";
369  $regex = "/^($sentence)/u";
370  }
371 
372  $matches = [];
373  if ( preg_match( $regex, $text, $matches ) ) {
374  return trim( $matches[1] );
375  } else {
376  // Just return the first line
377  return trim( explode( "\n", $text )[0] );
378  }
379  }
380 
388  public static function getOpenSearchTemplate( $type ) {
389  $config = MediaWikiServices::getInstance()->getSearchEngineConfig();
390  $template = $config->getConfig()->get( MainConfigNames::OpenSearchTemplate );
391 
392  if ( $template && $type === 'application/x-suggestions+json' ) {
393  return $template;
394  }
395 
396  $ns = implode( '|', $config->defaultNamespaces() );
397  if ( !$ns ) {
398  $ns = '0';
399  }
400 
401  switch ( $type ) {
402  case 'application/x-suggestions+json':
403  return $config->getConfig()->get( MainConfigNames::CanonicalServer ) .
404  wfScript( 'api' ) . '?action=opensearch&search={searchTerms}&namespace=' . $ns;
405 
406  case 'application/x-suggestions+xml':
407  return $config->getConfig()->get( MainConfigNames::CanonicalServer ) .
408  wfScript( 'api' ) .
409  '?action=opensearch&format=xml&search={searchTerms}&namespace=' . $ns;
410 
411  default:
412  throw new MWException( __METHOD__ . ": Unknown type '$type'" );
413  }
414  }
415 }
const PROTO_CURRENT
Definition: Defines.php:197
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...
$matches
buildSearchEngine(array $params=null)
Build the search engine to use.
Definition: SearchApi.php:177
buildCommonApiParams( $isScrollable=true)
The set of api parameters that are shared between api calls that call the SearchEngine.
Definition: SearchApi.php:66
SearchEngineFactory null $searchEngineFactory
Definition: SearchApi.php:36
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:1663
getMain()
Get the main module.
Definition: ApiBase.php:514
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.
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.
static setSubelementsList(array &$arr, $names)
Causes the elements with the specified names to be output as subelements rather than attributes.
Definition: ApiResult.php:553
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Definition: ApiResult.php:604
MediaWiki exception.
Definition: MWException.php:29
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.
const COMPLETION_PROFILE_TYPE
Profile type for completionSearch.
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