MediaWiki REL1_39
SearchHandler.php
Go to the documentation of this file.
1<?php
2
4
5use Config;
6use InvalidArgumentException;
18use SearchEngine;
21use SearchResult;
23use Status;
28
32class SearchHandler extends Handler {
33
35 private $searchEngineFactory;
36
38 private $searchEngineConfig;
39
41 private $permissionManager;
42
44 private $redirectLookup;
45
47 private $pageStore;
48
50 private $titleFormatter;
51
55 public const FULLTEXT_MODE = 'fulltext';
56
60 public const COMPLETION_MODE = 'completion';
61
65 private const SUPPORTED_MODES = [ self::FULLTEXT_MODE, self::COMPLETION_MODE ];
66
70 private $mode = null;
71
73 private const LIMIT = 50;
74
76 private const MAX_LIMIT = 100;
77
79 private const OFFSET = 0;
80
87 private $completionCacheExpiry;
88
98 public function __construct(
99 Config $config,
100 SearchEngineFactory $searchEngineFactory,
101 SearchEngineConfig $searchEngineConfig,
102 PermissionManager $permissionManager,
103 RedirectLookup $redirectLookup,
104 PageStore $pageStore,
105 TitleFormatter $titleFormatter
106 ) {
107 $this->searchEngineFactory = $searchEngineFactory;
108 $this->searchEngineConfig = $searchEngineConfig;
109 $this->permissionManager = $permissionManager;
110 $this->redirectLookup = $redirectLookup;
111 $this->pageStore = $pageStore;
112 $this->titleFormatter = $titleFormatter;
113
114 // @todo Avoid injecting the entire config, see T246377
115 $this->completionCacheExpiry = $config->get( MainConfigNames::SearchSuggestCacheExpiry );
116 }
117
118 protected function postInitSetup() {
119 $this->mode = $this->getConfig()['mode'] ?? self::FULLTEXT_MODE;
120
121 if ( !in_array( $this->mode, self::SUPPORTED_MODES ) ) {
122 throw new InvalidArgumentException(
123 "Unsupported search mode `{$this->mode}` configured. Supported modes: " .
124 implode( ', ', self::SUPPORTED_MODES )
125 );
126 }
127 }
128
132 private function createSearchEngine() {
133 $limit = $this->getValidatedParams()['limit'];
134
135 $searchEngine = $this->searchEngineFactory->create();
136 $searchEngine->setNamespaces( $this->searchEngineConfig->defaultNamespaces() );
137 $searchEngine->setLimitOffset( $limit, self::OFFSET );
138 return $searchEngine;
139 }
140
141 public function needsWriteAccess() {
142 return false;
143 }
144
151 private function getSearchResultsOrThrow( $results ) {
152 if ( $results ) {
153 if ( $results instanceof Status ) {
154 $status = $results;
155 if ( !$status->isOK() ) {
156 list( $error ) = $status->splitByErrorType();
157 if ( $error->getErrors() ) { // Only throw for errors, suppress warnings (for now)
158 $errorMessages = $error->getMessage();
159 throw new LocalizedHttpException(
160 new MessageValue( "rest-search-error", [ $errorMessages->getKey() ] )
161 );
162 }
163 }
164 $statusValue = $status->getValue();
165 if ( $statusValue instanceof ISearchResultSet ) {
166 return $statusValue->extractResults();
167 }
168 } else {
169 return $results->extractResults();
170 }
171 }
172 return [];
173 }
174
182 private function doSearch( $searchEngine ) {
183 $query = $this->getValidatedParams()['q'];
184
185 if ( $this->mode == self::COMPLETION_MODE ) {
186 $completionSearch = $searchEngine->completionSearchWithVariants( $query );
187 return $this->buildPageObjects( $completionSearch->getSuggestions() );
188 } else {
189 $titleSearch = $searchEngine->searchTitle( $query );
190 $textSearch = $searchEngine->searchText( $query );
191
192 $titleSearchResults = $this->getSearchResultsOrThrow( $titleSearch );
193 $textSearchResults = $this->getSearchResultsOrThrow( $textSearch );
194
195 $mergedResults = array_merge( $titleSearchResults, $textSearchResults );
196 return $this->buildPageObjects( $mergedResults );
197 }
198 }
199
212 private function buildPageObjects( array $searchResponse ): array {
213 $pageInfos = [];
214 foreach ( $searchResponse as $response ) {
215 $isSearchResult = $response instanceof SearchResult;
216 if ( $isSearchResult ) {
217 if ( $response->isBrokenTitle() || $response->isMissingRevision() ) {
218 continue;
219 }
220 $title = $response->getTitle();
221 } else {
222 $title = $response->getSuggestedTitle();
223 }
224 $pageObj = $this->buildSinglePage( $title, $response );
225 if ( $pageObj ) {
226 $pageNsAndID = CacheKeyHelper::getKeyForPage( $pageObj['pageIdentity'] );
227 // This handles the edge case where we have both the redirect source and redirect target page come back
228 // in our search results. In such event, we prefer (and thus replace) with the redirect target page.
229 if ( isset( $pageInfos[$pageNsAndID] ) ) {
230 if ( $pageInfos[$pageNsAndID]['redirect'] !== null ) {
231 $pageInfos[$pageNsAndID]['result'] = $isSearchResult ? $response : null;
232 $pageInfos[$pageNsAndID]['suggestion'] = $isSearchResult ? null : $response;
233 }
234 continue;
235 }
236 $pageInfos[$pageNsAndID] = $pageObj;
237 }
238 }
239 return $pageInfos;
240 }
241
255 private function buildSinglePage( $title, $result ) {
256 $redirectTarget = $title->canExist() ? $this->redirectLookup->getRedirectTarget( $title ) : null;
257 // Our page has a redirect that is not in a virtual namespace and is not an interwiki link.
258 // See T301346, T303352
259 if ( $redirectTarget && $redirectTarget->getNamespace() > -1 && !$redirectTarget->isExternal() ) {
260 $redirectSource = $title;
261 $title = $this->pageStore->getPageForLink( $redirectTarget );
262 } else {
263 $redirectSource = null;
264 }
265 if ( !$title || !$this->getAuthority()->probablyCan( 'read', $title ) ) {
266 return false;
267 }
268 return [
269 'pageIdentity' => $title,
270 'suggestion' => $result instanceof SearchSuggestion ? $result : null,
271 'result' => $result instanceof SearchResult ? $result : null,
272 'redirect' => $redirectSource
273 ];
274 }
275
288 private function buildResultFromPageInfos( array $pageInfos, array $thumbsAndDesc ): array {
289 $pages = [];
290 foreach ( $pageInfos as $pageInfo ) {
291 [
292 'pageIdentity' => $page,
293 'suggestion' => $sugg,
294 'result' => $result,
295 'redirect' => $redirect
296 ] = $pageInfo;
297 $excerpt = $sugg ? $sugg->getText() : $result->getTextSnippet();
298 $id = ( $page instanceof PageIdentity && $page->canExist() ) ? $page->getId() : 0;
299 $pages[] = [
300 'id' => $id,
301 'key' => $this->titleFormatter->getPrefixedDBkey( $page ),
302 'title' => $this->titleFormatter->getPrefixedText( $page ),
303 'excerpt' => $excerpt ?: null,
304 'matched_title' => $redirect ? $this->titleFormatter->getPrefixedText( $redirect ) : null,
305 'description' => $id > 0 ? $thumbsAndDesc[$id]['description'] : null,
306 'thumbnail' => $id > 0 ? $thumbsAndDesc[$id]['thumbnail'] : null,
307 ];
308 }
309 return $pages;
310 }
311
319 private function serializeThumbnail( ?SearchResultThumbnail $thumbnail ): ?array {
320 if ( $thumbnail == null ) {
321 return null;
322 }
323
324 return [
325 'mimetype' => $thumbnail->getMimeType(),
326 'size' => $thumbnail->getSize(),
327 'width' => $thumbnail->getWidth(),
328 'height' => $thumbnail->getHeight(),
329 'duration' => $thumbnail->getDuration(),
330 'url' => $thumbnail->getUrl(),
331 ];
332 }
333
344 private function buildDescriptionsFromPageIdentities( array $pageIdentities ) {
345 $descriptions = array_fill_keys( array_keys( $pageIdentities ), null );
346
347 $this->getHookRunner()->onSearchResultProvideDescription( $pageIdentities, $descriptions );
348
349 return array_map( static function ( $description ) {
350 return [ 'description' => $description ];
351 }, $descriptions );
352 }
353
365 private function buildThumbnailsFromPageIdentities( array $pageIdentities ) {
366 $thumbnails = array_fill_keys( array_keys( $pageIdentities ), null );
367
368 $this->getHookRunner()->onSearchResultProvideThumbnail( $pageIdentities, $thumbnails );
369
370 return array_map( function ( $thumbnail ) {
371 return [ 'thumbnail' => $this->serializeThumbnail( $thumbnail ) ];
372 }, $thumbnails );
373 }
374
379 public function execute() {
380 $searchEngine = $this->createSearchEngine();
381 $pageInfos = $this->doSearch( $searchEngine );
382
383 // We can only pass validated "real" PageIdentities to our hook handlers below
384 $pageIdentities = array_reduce(
385 array_values( $pageInfos ),
386 static function ( $realPages, $item ) {
387 $page = $item['pageIdentity'];
388 if ( $page instanceof PageIdentity && $page->exists() ) {
389 $realPages[$item['pageIdentity']->getId()] = $item['pageIdentity'];
390 }
391 return $realPages;
392 }, []
393 );
394
395 $descriptions = $this->buildDescriptionsFromPageIdentities( $pageIdentities );
396 $thumbs = $this->buildThumbnailsFromPageIdentities( $pageIdentities );
397
398 $thumbsAndDescriptions = [];
399 foreach ( $descriptions as $pageId => $description ) {
400 $thumbsAndDescriptions[$pageId] = $description + $thumbs[$pageId];
401 }
402
403 $result = $this->buildResultFromPageInfos( $pageInfos, $thumbsAndDescriptions );
404
405 $response = $this->getResponseFactory()->createJson( [ 'pages' => $result ] );
406
407 if ( $this->mode === self::COMPLETION_MODE && $this->completionCacheExpiry ) {
408 // Type-ahead completion matches should be cached by the client and
409 // in the CDN, especially for short prefixes.
410 // See also $wgSearchSuggestCacheExpiry and ApiOpenSearch
411 if ( $this->permissionManager->isEveryoneAllowed( 'read' ) ) {
412 $response->setHeader( 'Cache-Control', 'public, max-age=' . $this->completionCacheExpiry );
413 } else {
414 $response->setHeader( 'Cache-Control', 'no-store, max-age=0' );
415 }
416 }
417
418 return $response;
419 }
420
421 public function getParamSettings() {
422 return [
423 'q' => [
424 self::PARAM_SOURCE => 'query',
425 ParamValidator::PARAM_TYPE => 'string',
426 ParamValidator::PARAM_REQUIRED => true,
427 ],
428 'limit' => [
429 self::PARAM_SOURCE => 'query',
430 ParamValidator::PARAM_TYPE => 'integer',
431 ParamValidator::PARAM_REQUIRED => false,
432 ParamValidator::PARAM_DEFAULT => self::LIMIT,
433 IntegerDef::PARAM_MIN => 1,
434 IntegerDef::PARAM_MAX => self::MAX_LIMIT,
435 ],
436 ];
437 }
438}
getAuthority()
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition WebStart.php:82
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.
__construct(Config $config, SearchEngineFactory $searchEngineFactory, SearchEngineConfig $searchEngineConfig, PermissionManager $permissionManager, RedirectLookup $redirectLookup, PageStore $pageStore, TitleFormatter $titleFormatter)
needsWriteAccess()
Indicates whether this route requires write access.
const FULLTEXT_MODE
Search page body and titles.
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:168
getValidatedParams()
Fetch the validated parameters.
Definition Handler.php:336
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.
Search suggestion.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
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.