MediaWiki 1.40.4
SearchHandler.php
Go to the documentation of this file.
1<?php
2
4
5use Config;
6use InvalidArgumentException;
19use SearchEngine;
22use SearchResult;
24use Status;
29
33class SearchHandler extends Handler {
34
36 private $searchEngineFactory;
37
39 private $searchEngineConfig;
40
42 private $searchResultThumbnailProvider;
43
45 private $permissionManager;
46
48 private $redirectLookup;
49
51 private $pageStore;
52
54 private $titleFormatter;
55
59 public const FULLTEXT_MODE = 'fulltext';
60
64 public const COMPLETION_MODE = 'completion';
65
69 private const SUPPORTED_MODES = [ self::FULLTEXT_MODE, self::COMPLETION_MODE ];
70
74 private $mode = null;
75
77 private const LIMIT = 50;
78
80 private const MAX_LIMIT = 100;
81
83 private const OFFSET = 0;
84
91 private $completionCacheExpiry;
92
103 public function __construct(
104 Config $config,
105 SearchEngineFactory $searchEngineFactory,
106 SearchEngineConfig $searchEngineConfig,
107 SearchResultThumbnailProvider $searchResultThumbnailProvider,
108 PermissionManager $permissionManager,
109 RedirectLookup $redirectLookup,
110 PageStore $pageStore,
111 TitleFormatter $titleFormatter
112 ) {
113 $this->searchEngineFactory = $searchEngineFactory;
114 $this->searchEngineConfig = $searchEngineConfig;
115 $this->searchResultThumbnailProvider = $searchResultThumbnailProvider;
116 $this->permissionManager = $permissionManager;
117 $this->redirectLookup = $redirectLookup;
118 $this->pageStore = $pageStore;
119 $this->titleFormatter = $titleFormatter;
120
121 // @todo Avoid injecting the entire config, see T246377
122 $this->completionCacheExpiry = $config->get( MainConfigNames::SearchSuggestCacheExpiry );
123 }
124
125 protected function postInitSetup() {
126 $this->mode = $this->getConfig()['mode'] ?? self::FULLTEXT_MODE;
127
128 if ( !in_array( $this->mode, self::SUPPORTED_MODES ) ) {
129 throw new InvalidArgumentException(
130 "Unsupported search mode `{$this->mode}` configured. Supported modes: " .
131 implode( ', ', self::SUPPORTED_MODES )
132 );
133 }
134 }
135
139 private function createSearchEngine() {
140 $limit = $this->getValidatedParams()['limit'];
141
142 $searchEngine = $this->searchEngineFactory->create();
143 $searchEngine->setNamespaces( $this->searchEngineConfig->defaultNamespaces() );
144 $searchEngine->setLimitOffset( $limit, self::OFFSET );
145 return $searchEngine;
146 }
147
148 public function needsWriteAccess() {
149 return false;
150 }
151
158 private function getSearchResultsOrThrow( $results ) {
159 if ( $results ) {
160 if ( $results instanceof Status ) {
161 $status = $results;
162 if ( !$status->isOK() ) {
163 [ $error ] = $status->splitByErrorType();
164 if ( $error->getErrors() ) { // Only throw for errors, suppress warnings (for now)
165 $errorMessages = $error->getMessage();
166 throw new LocalizedHttpException(
167 new MessageValue( "rest-search-error", [ $errorMessages->getKey() ] )
168 );
169 }
170 }
171 $statusValue = $status->getValue();
172 if ( $statusValue instanceof ISearchResultSet ) {
173 return $statusValue->extractResults();
174 }
175 } else {
176 return $results->extractResults();
177 }
178 }
179 return [];
180 }
181
189 private function doSearch( $searchEngine ) {
190 $query = $this->getValidatedParams()['q'];
191
192 if ( $this->mode == self::COMPLETION_MODE ) {
193 $completionSearch = $searchEngine->completionSearchWithVariants( $query );
194 return $this->buildPageObjects( $completionSearch->getSuggestions() );
195 } else {
196 $titleSearch = $searchEngine->searchTitle( $query );
197 $textSearch = $searchEngine->searchText( $query );
198
199 $titleSearchResults = $this->getSearchResultsOrThrow( $titleSearch );
200 $textSearchResults = $this->getSearchResultsOrThrow( $textSearch );
201
202 $mergedResults = array_merge( $titleSearchResults, $textSearchResults );
203 return $this->buildPageObjects( $mergedResults );
204 }
205 }
206
219 private function buildPageObjects( array $searchResponse ): array {
220 $pageInfos = [];
221 foreach ( $searchResponse as $response ) {
222 $isSearchResult = $response instanceof SearchResult;
223 if ( $isSearchResult ) {
224 if ( $response->isBrokenTitle() || $response->isMissingRevision() ) {
225 continue;
226 }
227 $title = $response->getTitle();
228 } else {
229 $title = $response->getSuggestedTitle();
230 }
231 $pageObj = $this->buildSinglePage( $title, $response );
232 if ( $pageObj ) {
233 $pageNsAndID = CacheKeyHelper::getKeyForPage( $pageObj['pageIdentity'] );
234 // This handles the edge case where we have both the redirect source and redirect target page come back
235 // in our search results. In such event, we prefer (and thus replace) with the redirect target page.
236 if ( isset( $pageInfos[$pageNsAndID] ) ) {
237 if ( $pageInfos[$pageNsAndID]['redirect'] !== null ) {
238 $pageInfos[$pageNsAndID]['result'] = $isSearchResult ? $response : null;
239 $pageInfos[$pageNsAndID]['suggestion'] = $isSearchResult ? null : $response;
240 }
241 continue;
242 }
243 $pageInfos[$pageNsAndID] = $pageObj;
244 }
245 }
246 return $pageInfos;
247 }
248
262 private function buildSinglePage( $title, $result ) {
263 $redirectTarget = $title->canExist() ? $this->redirectLookup->getRedirectTarget( $title ) : null;
264 // Our page has a redirect that is not in a virtual namespace and is not an interwiki link.
265 // See T301346, T303352
266 if ( $redirectTarget && $redirectTarget->getNamespace() > -1 && !$redirectTarget->isExternal() ) {
267 $redirectSource = $title;
268 $title = $this->pageStore->getPageForLink( $redirectTarget );
269 } else {
270 $redirectSource = null;
271 }
272 if ( !$title || !$this->getAuthority()->probablyCan( 'read', $title ) ) {
273 return false;
274 }
275 return [
276 'pageIdentity' => $title,
277 'suggestion' => $result instanceof SearchSuggestion ? $result : null,
278 'result' => $result instanceof SearchResult ? $result : null,
279 'redirect' => $redirectSource
280 ];
281 }
282
295 private function buildResultFromPageInfos( array $pageInfos, array $thumbsAndDesc ): array {
296 $pages = [];
297 foreach ( $pageInfos as $pageInfo ) {
298 [
299 'pageIdentity' => $page,
300 'suggestion' => $sugg,
301 'result' => $result,
302 'redirect' => $redirect
303 ] = $pageInfo;
304 $excerpt = $sugg ? $sugg->getText() : $result->getTextSnippet();
305 $id = ( $page instanceof PageIdentity && $page->canExist() ) ? $page->getId() : 0;
306 $pages[] = [
307 'id' => $id,
308 'key' => $this->titleFormatter->getPrefixedDBkey( $page ),
309 'title' => $this->titleFormatter->getPrefixedText( $page ),
310 'excerpt' => $excerpt ?: null,
311 'matched_title' => $redirect ? $this->titleFormatter->getPrefixedText( $redirect ) : null,
312 'description' => $id > 0 ? $thumbsAndDesc[$id]['description'] : null,
313 'thumbnail' => $id > 0 ? $thumbsAndDesc[$id]['thumbnail'] : null,
314 ];
315 }
316 return $pages;
317 }
318
326 private function serializeThumbnail( ?SearchResultThumbnail $thumbnail ): ?array {
327 if ( $thumbnail == null ) {
328 return null;
329 }
330
331 return [
332 'mimetype' => $thumbnail->getMimeType(),
333 'size' => $thumbnail->getSize(),
334 'width' => $thumbnail->getWidth(),
335 'height' => $thumbnail->getHeight(),
336 'duration' => $thumbnail->getDuration(),
337 'url' => $thumbnail->getUrl(),
338 ];
339 }
340
351 private function buildDescriptionsFromPageIdentities( array $pageIdentities ) {
352 $descriptions = array_fill_keys( array_keys( $pageIdentities ), null );
353
354 $this->getHookRunner()->onSearchResultProvideDescription( $pageIdentities, $descriptions );
355
356 return array_map( static function ( $description ) {
357 return [ 'description' => $description ];
358 }, $descriptions );
359 }
360
372 private function buildThumbnailsFromPageIdentities( array $pageIdentities ) {
373 $thumbnails = $this->searchResultThumbnailProvider->getThumbnails( $pageIdentities );
374 $thumbnails += array_fill_keys( array_keys( $pageIdentities ), null );
375
376 return array_map( function ( $thumbnail ) {
377 return [ 'thumbnail' => $this->serializeThumbnail( $thumbnail ) ];
378 }, $thumbnails );
379 }
380
385 public function execute() {
386 $searchEngine = $this->createSearchEngine();
387 $pageInfos = $this->doSearch( $searchEngine );
388
389 // We can only pass validated "real" PageIdentities to our hook handlers below
390 $pageIdentities = array_reduce(
391 array_values( $pageInfos ),
392 static function ( $realPages, $item ) {
393 $page = $item['pageIdentity'];
394 if ( $page instanceof PageIdentity && $page->exists() ) {
395 $realPages[$item['pageIdentity']->getId()] = $item['pageIdentity'];
396 }
397 return $realPages;
398 }, []
399 );
400
401 $descriptions = $this->buildDescriptionsFromPageIdentities( $pageIdentities );
402 $thumbs = $this->buildThumbnailsFromPageIdentities( $pageIdentities );
403
404 $thumbsAndDescriptions = [];
405 foreach ( $descriptions as $pageId => $description ) {
406 $thumbsAndDescriptions[$pageId] = $description + $thumbs[$pageId];
407 }
408
409 $result = $this->buildResultFromPageInfos( $pageInfos, $thumbsAndDescriptions );
410
411 $response = $this->getResponseFactory()->createJson( [ 'pages' => $result ] );
412
413 if ( $this->mode === self::COMPLETION_MODE && $this->completionCacheExpiry ) {
414 // Type-ahead completion matches should be cached by the client and
415 // in the CDN, especially for short prefixes.
416 // See also $wgSearchSuggestCacheExpiry and ApiOpenSearch
417 if ( $this->permissionManager->isEveryoneAllowed( 'read' ) ) {
418 $response->setHeader( 'Cache-Control', 'public, max-age=' . $this->completionCacheExpiry );
419 } else {
420 $response->setHeader( 'Cache-Control', 'no-store, max-age=0' );
421 }
422 }
423
424 return $response;
425 }
426
427 public function getParamSettings() {
428 return [
429 'q' => [
430 self::PARAM_SOURCE => 'query',
431 ParamValidator::PARAM_TYPE => 'string',
432 ParamValidator::PARAM_REQUIRED => true,
433 ],
434 'limit' => [
435 self::PARAM_SOURCE => 'query',
436 ParamValidator::PARAM_TYPE => 'integer',
437 ParamValidator::PARAM_REQUIRED => false,
438 ParamValidator::PARAM_DEFAULT => self::LIMIT,
439 IntegerDef::PARAM_MIN => 1,
440 IntegerDef::PARAM_MAX => self::MAX_LIMIT,
441 ],
442 ];
443 }
444}
getAuthority()
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition WebStart.php:88
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:20
getConfig()
Get the configuration array for the current route.
Definition Handler.php:177
getValidatedParams()
Fetch the validated parameters.
Definition Handler.php:367
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.
Definition Status.php:46
Value object representing a message for i18n.
Service for formatting and validating API parameters.
Type definition for integer types.
Interface for configuration instances.
Definition Config.php:30
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
A set of SearchEngine results.
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.