MediaWiki master
SearchHandler.php
Go to the documentation of this file.
1<?php
2
4
5use InvalidArgumentException;
15use MediaWiki\Rest\Handler\Helper\RestStatusTrait;
21use SearchEngine;
24use SearchResult;
26use StatusValue;
29
33class SearchHandler extends Handler {
34 use RestStatusTrait;
35
37 private $searchEngineFactory;
38
40 private $searchEngineConfig;
41
43 private $searchResultThumbnailProvider;
44
46 private $permissionManager;
47
49 private $redirectLookup;
50
52 private $pageStore;
53
55 private $titleFormatter;
56
60 public const FULLTEXT_MODE = 'fulltext';
61
65 public const COMPLETION_MODE = 'completion';
66
70 private const SUPPORTED_MODES = [ self::FULLTEXT_MODE, self::COMPLETION_MODE ];
71
75 private $mode = null;
76
78 private const LIMIT = 50;
79
81 private const MAX_LIMIT = 100;
82
84 private const OFFSET = 0;
85
92 private $completionCacheExpiry;
93
104 public function __construct(
105 Config $config,
106 SearchEngineFactory $searchEngineFactory,
107 SearchEngineConfig $searchEngineConfig,
108 SearchResultThumbnailProvider $searchResultThumbnailProvider,
109 PermissionManager $permissionManager,
110 RedirectLookup $redirectLookup,
111 PageStore $pageStore,
112 TitleFormatter $titleFormatter
113 ) {
114 $this->searchEngineFactory = $searchEngineFactory;
115 $this->searchEngineConfig = $searchEngineConfig;
116 $this->searchResultThumbnailProvider = $searchResultThumbnailProvider;
117 $this->permissionManager = $permissionManager;
118 $this->redirectLookup = $redirectLookup;
119 $this->pageStore = $pageStore;
120 $this->titleFormatter = $titleFormatter;
121
122 // @todo Avoid injecting the entire config, see T246377
123 $this->completionCacheExpiry = $config->get( MainConfigNames::SearchSuggestCacheExpiry );
124 }
125
126 protected function postInitSetup() {
127 $this->mode = $this->getConfig()['mode'] ?? self::FULLTEXT_MODE;
128
129 if ( !in_array( $this->mode, self::SUPPORTED_MODES ) ) {
130 throw new InvalidArgumentException(
131 "Unsupported search mode `{$this->mode}` configured. Supported modes: " .
132 implode( ', ', self::SUPPORTED_MODES )
133 );
134 }
135 }
136
140 private function createSearchEngine() {
141 $limit = $this->getValidatedParams()['limit'];
142
143 $searchEngine = $this->searchEngineFactory->create();
144 $searchEngine->setNamespaces( $this->searchEngineConfig->defaultNamespaces() );
145 $searchEngine->setLimitOffset( $limit, self::OFFSET );
146 return $searchEngine;
147 }
148
149 public function needsWriteAccess() {
150 return false;
151 }
152
159 private function getSearchResultsOrThrow( $results ) {
160 if ( $results ) {
161 if ( $results instanceof StatusValue ) {
162 $status = $results;
163 if ( !$status->isOK() ) {
164 [ $error ] = $status->splitByErrorType();
165 if ( $error->getErrors() ) { // Only throw for errors, suppress warnings (for now)
166 $this->throwExceptionForStatus( $status, 'rest-search-error', 500 );
167 }
168 }
169 $statusValue = $status->getValue();
170 if ( $statusValue instanceof ISearchResultSet ) {
171 return $statusValue->extractResults();
172 }
173 } else {
174 return $results->extractResults();
175 }
176 }
177 return [];
178 }
179
187 private function doSearch( $searchEngine ) {
188 $query = $this->getValidatedParams()['q'];
189
190 if ( $this->mode == self::COMPLETION_MODE ) {
191 $completionSearch = $searchEngine->completionSearchWithVariants( $query );
192 return $this->buildPageObjects( $completionSearch->getSuggestions() );
193 } else {
194 $titleSearch = $searchEngine->searchTitle( $query );
195 $textSearch = $searchEngine->searchText( $query );
196
197 $titleSearchResults = $this->getSearchResultsOrThrow( $titleSearch );
198 $textSearchResults = $this->getSearchResultsOrThrow( $textSearch );
199
200 $mergedResults = array_merge( $titleSearchResults, $textSearchResults );
201 return $this->buildPageObjects( $mergedResults );
202 }
203 }
204
217 private function buildPageObjects( array $searchResponse ): array {
218 $pageInfos = [];
219 foreach ( $searchResponse as $response ) {
220 $isSearchResult = $response instanceof SearchResult;
221 if ( $isSearchResult ) {
222 if ( $response->isBrokenTitle() || $response->isMissingRevision() ) {
223 continue;
224 }
225 $title = $response->getTitle();
226 } else {
227 $title = $response->getSuggestedTitle();
228 }
229 $pageObj = $this->buildSinglePage( $title, $response );
230 if ( $pageObj ) {
231 $pageNsAndID = CacheKeyHelper::getKeyForPage( $pageObj['pageIdentity'] );
232 // This handles the edge case where we have both the redirect source and redirect target page come back
233 // in our search results. In such event, we prefer (and thus replace) with the redirect target page.
234 if ( isset( $pageInfos[$pageNsAndID] ) ) {
235 if ( $pageInfos[$pageNsAndID]['redirect'] !== null ) {
236 $pageInfos[$pageNsAndID]['result'] = $isSearchResult ? $response : null;
237 $pageInfos[$pageNsAndID]['suggestion'] = $isSearchResult ? null : $response;
238 }
239 continue;
240 }
241 $pageInfos[$pageNsAndID] = $pageObj;
242 }
243 }
244 return $pageInfos;
245 }
246
260 private function buildSinglePage( $title, $result ) {
261 $redirectTarget = $title->canExist() ? $this->redirectLookup->getRedirectTarget( $title ) : null;
262 // Our page has a redirect that is not in a virtual namespace and is not an interwiki link.
263 // See T301346, T303352
264 if ( $redirectTarget && $redirectTarget->getNamespace() > -1 && !$redirectTarget->isExternal() ) {
265 $redirectSource = $title;
266 $title = $this->pageStore->getPageForLink( $redirectTarget );
267 } else {
268 $redirectSource = null;
269 }
270 if ( !$title || !$this->getAuthority()->probablyCan( 'read', $title ) ) {
271 return false;
272 }
273 return [
274 'pageIdentity' => $title,
275 'suggestion' => $result instanceof SearchSuggestion ? $result : null,
276 'result' => $result instanceof SearchResult ? $result : null,
277 'redirect' => $redirectSource
278 ];
279 }
280
293 private function buildResultFromPageInfos( array $pageInfos, array $thumbsAndDesc ): array {
294 $pages = [];
295 foreach ( $pageInfos as $pageInfo ) {
296 [
297 'pageIdentity' => $page,
298 'suggestion' => $sugg,
299 'result' => $result,
300 'redirect' => $redirect
301 ] = $pageInfo;
302 $excerpt = $sugg ? $sugg->getText() : $result->getTextSnippet();
303 $id = ( $page instanceof PageIdentity && $page->canExist() ) ? $page->getId() : 0;
304 $pages[] = [
305 'id' => $id,
306 'key' => $this->titleFormatter->getPrefixedDBkey( $page ),
307 'title' => $this->titleFormatter->getPrefixedText( $page ),
308 'excerpt' => $excerpt ?: null,
309 'matched_title' => $redirect ? $this->titleFormatter->getPrefixedText( $redirect ) : null,
310 'description' => $id > 0 ? $thumbsAndDesc[$id]['description'] : null,
311 'thumbnail' => $id > 0 ? $thumbsAndDesc[$id]['thumbnail'] : null,
312 ];
313 }
314 return $pages;
315 }
316
324 private function serializeThumbnail( ?SearchResultThumbnail $thumbnail ): ?array {
325 if ( $thumbnail == null ) {
326 return null;
327 }
328
329 return [
330 'mimetype' => $thumbnail->getMimeType(),
331 'width' => $thumbnail->getWidth(),
332 'height' => $thumbnail->getHeight(),
333 'duration' => $thumbnail->getDuration(),
334 'url' => $thumbnail->getUrl(),
335 ];
336 }
337
348 private function buildDescriptionsFromPageIdentities( array $pageIdentities ) {
349 $descriptions = array_fill_keys( array_keys( $pageIdentities ), null );
350
351 $this->getHookRunner()->onSearchResultProvideDescription( $pageIdentities, $descriptions );
352
353 return array_map( static function ( $description ) {
354 return [ 'description' => $description ];
355 }, $descriptions );
356 }
357
369 private function buildThumbnailsFromPageIdentities( array $pageIdentities ) {
370 $thumbnails = $this->searchResultThumbnailProvider->getThumbnails( $pageIdentities );
371 $thumbnails += array_fill_keys( array_keys( $pageIdentities ), null );
372
373 return array_map( function ( $thumbnail ) {
374 return [ 'thumbnail' => $this->serializeThumbnail( $thumbnail ) ];
375 }, $thumbnails );
376 }
377
382 public function execute() {
383 $searchEngine = $this->createSearchEngine();
384 $pageInfos = $this->doSearch( $searchEngine );
385
386 // We can only pass validated "real" PageIdentities to our hook handlers below
387 $pageIdentities = array_reduce(
388 array_values( $pageInfos ),
389 static function ( $realPages, $item ) {
390 $page = $item['pageIdentity'];
391 if ( $page instanceof PageIdentity && $page->exists() ) {
392 $realPages[$item['pageIdentity']->getId()] = $item['pageIdentity'];
393 }
394 return $realPages;
395 }, []
396 );
397
398 $descriptions = $this->buildDescriptionsFromPageIdentities( $pageIdentities );
399 $thumbs = $this->buildThumbnailsFromPageIdentities( $pageIdentities );
400
401 $thumbsAndDescriptions = [];
402 foreach ( $descriptions as $pageId => $description ) {
403 $thumbsAndDescriptions[$pageId] = $description + $thumbs[$pageId];
404 }
405
406 $result = $this->buildResultFromPageInfos( $pageInfos, $thumbsAndDescriptions );
407
408 $response = $this->getResponseFactory()->createJson( [ 'pages' => $result ] );
409
410 if ( $this->mode === self::COMPLETION_MODE && $this->completionCacheExpiry ) {
411 // Type-ahead completion matches should be cached by the client and
412 // in the CDN, especially for short prefixes.
413 // See also $wgSearchSuggestCacheExpiry and ApiOpenSearch
414 if ( $this->permissionManager->isEveryoneAllowed( 'read' ) ) {
415 $response->setHeader( 'Cache-Control', 'public, max-age=' . $this->completionCacheExpiry );
416 } else {
417 $response->setHeader( 'Cache-Control', 'no-store, max-age=0' );
418 }
419 }
420
421 return $response;
422 }
423
424 public function getParamSettings() {
425 return [
426 'q' => [
427 self::PARAM_SOURCE => 'query',
428 ParamValidator::PARAM_TYPE => 'string',
429 ParamValidator::PARAM_REQUIRED => true,
430 ],
431 'limit' => [
432 self::PARAM_SOURCE => 'query',
433 ParamValidator::PARAM_TYPE => 'integer',
434 ParamValidator::PARAM_REQUIRED => false,
435 ParamValidator::PARAM_DEFAULT => self::LIMIT,
436 IntegerDef::PARAM_MIN => 1,
437 IntegerDef::PARAM_MAX => self::MAX_LIMIT,
438 ],
439 ];
440 }
441}
getAuthority()
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Helper class for mapping value objects representing basic entities to cache keys.
A class containing constants representing the names of configuration variables.
const SearchSuggestCacheExpiry
Name constant for the SearchSuggestCacheExpiry setting, for use with Config::get()
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Handler class for Core REST API endpoint that handles basic search.
needsWriteAccess()
Indicates whether this route requires write access.
const FULLTEXT_MODE
Search page body and titles.
__construct(Config $config, SearchEngineFactory $searchEngineFactory, SearchEngineConfig $searchEngineConfig, SearchResultThumbnailProvider $searchResultThumbnailProvider, PermissionManager $permissionManager, RedirectLookup $redirectLookup, PageStore $pageStore, TitleFormatter $titleFormatter)
const COMPLETION_MODE
Search title completion matches.
postInitSetup()
The handler can override this to do any necessary setup after init() is called to inject the dependen...
getParamSettings()
Fetch ParamValidator settings for parameters.
Base class for REST route handlers.
Definition Handler.php:21
getConfig()
Get the configuration array for the current route.
Definition Handler.php:184
getValidatedParams()
Fetch the validated parameters.
Definition Handler.php:549
Class that stores information about thumbnail, e.
Configuration handling class for SearchEngine.
Factory class for SearchEngine.
Contain a class for special pages.
NOTE: this class is being refactored into an abstract base class.
A search suggestion.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Service for formatting and validating API parameters.
Type definition for integer types.
A set of SearchEngine results.
Interface for configuration instances.
Definition Config.php:32
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
Interface for objects (potentially) representing an editable wiki page.
exists()
Checks if the page currently exists.
Service for resolving a wiki page redirect.
A title formatter service for MediaWiki.
Copyright (C) 2011-2020 Wikimedia Foundation and others.