MediaWiki  1.34.0
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  // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
255  if ( is_array( $r['image'] ) && isset( $r['image']['source'] ) ) {
256  $item['Image'] = array_intersect_key( $r['image'], $imageKeys );
257  }
258  ApiResult::setSubelementsList( $item, array_keys( $item ) );
259  $items[] = $item;
260  }
261  ApiResult::setIndexedTagName( $items, 'Item' );
262  $result->addValue( null, 'version', '2.0' );
263  $result->addValue( null, 'xmlns', 'http://opensearch.org/searchsuggest2' );
264  $result->addValue( null, 'Query', strval( $search ) );
265  $result->addSubelementsList( null, 'Query' );
266  $result->addValue( null, 'Section', $items );
267  break;
268 
269  default:
270  ApiBase::dieDebug( __METHOD__, "Unsupported format '{$this->getFormat()}'" );
271  }
272  }
273 
274  public function getAllowedParams() {
275  if ( $this->allowedParams !== null ) {
276  return $this->allowedParams;
277  }
278  $this->allowedParams = $this->buildCommonApiParams( false ) + [
279  'suggest' => false,
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:63
ApiOpenSearch\search
search( $search, array $params)
Perform the search.
Definition: ApiOpenSearch.php:119
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:34
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:117
ApiOpenSearch\getFormat
getFormat()
Get the output format.
Definition: ApiOpenSearch.php:44
ApiBase\PARAM_TYPE
const PARAM_TYPE
(string|string[]) Either an array of allowed value strings, or a string type as described below.
Definition: ApiBase.php:94
ApiBase\getResult
getResult()
Get the result object.
Definition: ApiBase.php:640
ApiOpenSearch
Definition: ApiOpenSearch.php:30
ApiBase\getDB
getDB()
Gets a default replica DB connection object.
Definition: ApiBase.php:668
$res
$res
Definition: testCompression.php:52
ApiBase
This abstract class implements many basic API functions, and is the base of all API classes.
Definition: ApiBase.php:42
SearchEngine\COMPLETION_PROFILE_TYPE
const COMPLETION_PROFILE_TYPE
Profile type for completionSearch.
Definition: SearchEngine.php:63
MWException
MediaWiki exception.
Definition: MWException.php:26
wfScript
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
Definition: GlobalFunctions.php:2642
$matches
$matches
Definition: NoLocalSettings.php:24
ApiOpenSearch\execute
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
Definition: ApiOpenSearch.php:83
buildSearchEngine
buildSearchEngine(array $params=null)
Build the search engine to use.
Definition: SearchApi.php:153
PROTO_CURRENT
const PROTO_CURRENT
Definition: Defines.php:202
ApiBase\extractRequestParams
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:761
$title
$title
Definition: testCompression.php:34
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:586
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:616
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:215
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:274
ApiOpenSearch\$fm
$fm
Definition: ApiOpenSearch.php:34
ApiBase\PARAM_DFLT
const PARAM_DFLT
(null|boolean|integer|string) Default value of the parameter.
Definition: ApiBase.php:55
ApiBase\getParameter
getParameter( $paramName, $parseLimit=true)
Get a value for the given parameter.
Definition: ApiBase.php:876
ApiBase\getMain
getMain()
Get the main module.
Definition: ApiBase.php:536
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
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
ApiBase\dieDebug
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition: ApiBase.php:2220
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:565
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:491
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:48