MediaWiki  master
ApiOpenSearch.php
Go to the documentation of this file.
1 <?php
27 
31 class 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 ) {
297  return $this->allowedParams;
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 
320  return $this->allowedParams;
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 }
ApiMain
This is the main API class, used for both external and internal processing.
Definition: ApiMain.php:49
ContextSource\getConfig
getConfig()
Definition: ContextSource.php:72
ApiOpenSearch\search
search( $search, array $params)
Perform the search.
Definition: ApiOpenSearch.php:141
ApiOpenSearch\$allowedParams
array $allowedParams
list of api allowed params
Definition: ApiOpenSearch.php:38
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:186
ApiOpenSearch\getFormat
getFormat()
Get the output format.
Definition: ApiOpenSearch.php:69
ApiBase\PARAM_TYPE
const PARAM_TYPE
Definition: ApiBase.php:72
ApiBase\getResult
getResult()
Get the result object.
Definition: ApiBase.php:571
ApiOpenSearch\$linkBatchFactory
LinkBatchFactory $linkBatchFactory
Definition: ApiOpenSearch.php:41
ApiOpenSearch
Definition: ApiOpenSearch.php:31
$searchEngineFactory
SearchEngineFactory null $searchEngineFactory
Definition: SearchApi.php:33
ApiOpenSearch\__construct
__construct(ApiMain $mainModule, $moduleName, LinkBatchFactory $linkBatchFactory, SearchEngineConfig $searchEngineConfig, SearchEngineFactory $searchEngineFactory)
Definition: ApiOpenSearch.php:50
SearchEngineFactory
Factory class for SearchEngine.
Definition: SearchEngineFactory.php:12
ApiBase\getDB
getDB()
Gets a default replica DB connection object.
Definition: ApiBase.php:594
$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:55
SearchEngine\COMPLETION_PROFILE_TYPE
const COMPLETION_PROFILE_TYPE
Profile type for completionSearch.
Definition: SearchEngine.php:72
ApiBase\PARAM_DEPRECATED
const PARAM_DEPRECATED
Definition: ApiBase.php:77
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:2306
$matches
$matches
Definition: NoLocalSettings.php:24
MediaWiki\Cache\LinkBatchFactory
Definition: LinkBatchFactory.php:39
ApiOpenSearch\execute
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
Definition: ApiOpenSearch.php:109
buildSearchEngine
buildSearchEngine(array $params=null)
Build the search engine to use.
Definition: SearchApi.php:174
ApiBase\extractRequestParams
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:707
$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:90
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:680
ApiOpenSearch\$format
$format
Definition: ApiOpenSearch.php:34
PROTO_CURRENT
const PROTO_CURRENT
Definition: Defines.php:195
ApiResult\setIndexedTagName
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Definition: ApiResult.php:607
ApiOpenSearch\getSearchProfileParams
getSearchProfileParams()
Definition: ApiOpenSearch.php:323
ApiOpenSearch\getOpenSearchTemplate
static getOpenSearchTemplate( $type)
Fetch the template for a type.
Definition: ApiOpenSearch.php:385
ApiOpenSearch\populateResult
populateResult( $search, &$results)
Definition: ApiOpenSearch.php:237
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:295
ApiOpenSearch\$fm
$fm
Definition: ApiOpenSearch.php:35
ApiBase\PARAM_DFLT
const PARAM_DFLT
Definition: ApiBase.php:70
ApiBase\getParameter
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
Definition: ApiBase.php:827
SearchEngineConfig
Configuration handling class for SearchEngine.
Definition: SearchEngineConfig.php:12
ApiBase\getMain
getMain()
Get the main module.
Definition: ApiBase.php:456
ApiOpenSearch\trimExtract
static trimExtract( $text, $length)
Trim an extract to a sensible length.
Definition: ApiOpenSearch.php:353
ApiOpenSearch\getHelpUrls
getHelpUrls()
Return links to more detailed help pages about the module.
Definition: ApiOpenSearch.php:339
ApiBase\getHookRunner
getHookRunner()
Get an ApiHookRunner for running core API hooks.
Definition: ApiBase.php:653
ApiBase\dieDebug
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition: ApiBase.php:1569
SearchApi
trait SearchApi
Traits for API components that use a SearchEngine.
Definition: SearchApi.php:27
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:556
ApiOpenSearch\getExamplesMessages
getExamplesMessages()
Returns usage examples for this module.
Definition: ApiOpenSearch.php:332
wfExpandUrl
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
Definition: GlobalFunctions.php:474
buildCommonApiParams
buildCommonApiParams( $isScrollable=true)
The set of api parameters that are shared between api calls that call the SearchEngine.
Definition: SearchApi.php:63
$type
$type
Definition: testCompression.php:52