Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
98.84% |
171 / 173 |
|
88.24% |
15 / 17 |
CRAP | |
0.00% |
0 / 1 |
| SearchHandler | |
98.84% |
171 / 173 |
|
88.24% |
15 / 17 |
59 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
| postInitSetup | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
| createSearchEngine | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
| needsWriteAccess | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getSearchResultsOrThrow | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
6 | |||
| doSearch | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
| buildPageObjects | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
10 | |||
| buildSinglePage | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
9 | |||
| buildResultFromPageInfos | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
10 | |||
| serializeThumbnail | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
| getSpecialPageDescription | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
| buildDescriptionsFromPageIdentities | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
| buildThumbnailsFromPageIdentities | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
| execute | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
8 | |||
| getParamSettings | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
1 | |||
| getResponseBodySchemaFileName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getResponseBodyExampleFileName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace MediaWiki\Rest\Handler; |
| 4 | |
| 5 | use InvalidArgumentException; |
| 6 | use MediaWiki\Config\Config; |
| 7 | use MediaWiki\MainConfigNames; |
| 8 | use MediaWiki\Page\CacheKeyHelper; |
| 9 | use MediaWiki\Page\PageIdentity; |
| 10 | use MediaWiki\Page\PageStore; |
| 11 | use MediaWiki\Page\RedirectLookup; |
| 12 | use MediaWiki\Permissions\PermissionManager; |
| 13 | use MediaWiki\Rest\Handler; |
| 14 | use MediaWiki\Rest\Handler\Helper\RestStatusTrait; |
| 15 | use MediaWiki\Rest\LocalizedHttpException; |
| 16 | use MediaWiki\Rest\Response; |
| 17 | use MediaWiki\Rest\ResponseHeaders; |
| 18 | use MediaWiki\Search\Entity\SearchResultThumbnail; |
| 19 | use MediaWiki\Search\ISearchResultSet; |
| 20 | use MediaWiki\Search\SearchEngine; |
| 21 | use MediaWiki\Search\SearchEngineConfig; |
| 22 | use MediaWiki\Search\SearchEngineFactory; |
| 23 | use MediaWiki\Search\SearchResult; |
| 24 | use MediaWiki\Search\SearchResultThumbnailProvider; |
| 25 | use MediaWiki\Search\SearchSuggestion; |
| 26 | use MediaWiki\SpecialPage\SpecialPageFactory; |
| 27 | use MediaWiki\Title\TitleFormatter; |
| 28 | use StatusValue; |
| 29 | use Wikimedia\Message\MessageValue; |
| 30 | use Wikimedia\ParamValidator\ParamValidator; |
| 31 | use Wikimedia\ParamValidator\TypeDef\IntegerDef; |
| 32 | |
| 33 | /** |
| 34 | * Handler class for Core REST API endpoint that handles basic search |
| 35 | */ |
| 36 | class SearchHandler extends Handler { |
| 37 | use RestStatusTrait; |
| 38 | |
| 39 | private SearchEngineFactory $searchEngineFactory; |
| 40 | private SearchEngineConfig $searchEngineConfig; |
| 41 | private SearchResultThumbnailProvider $searchResultThumbnailProvider; |
| 42 | private PermissionManager $permissionManager; |
| 43 | private RedirectLookup $redirectLookup; |
| 44 | private PageStore $pageStore; |
| 45 | private TitleFormatter $titleFormatter; |
| 46 | private SpecialPageFactory $specialPageFactory; |
| 47 | |
| 48 | /** |
| 49 | * Search page body and titles. |
| 50 | */ |
| 51 | public const FULLTEXT_MODE = 'fulltext'; |
| 52 | |
| 53 | /** |
| 54 | * Search title completion matches. |
| 55 | */ |
| 56 | public const COMPLETION_MODE = 'completion'; |
| 57 | |
| 58 | /** |
| 59 | * Supported modes |
| 60 | */ |
| 61 | private const SUPPORTED_MODES = [ self::FULLTEXT_MODE, self::COMPLETION_MODE ]; |
| 62 | |
| 63 | /** |
| 64 | * @var string |
| 65 | */ |
| 66 | private $mode = null; |
| 67 | |
| 68 | /** Limit results to 50 pages by default */ |
| 69 | private const LIMIT = 50; |
| 70 | |
| 71 | /** Hard limit results to 100 pages */ |
| 72 | private const MAX_LIMIT = 100; |
| 73 | |
| 74 | /** Default to first page */ |
| 75 | private const OFFSET = 0; |
| 76 | |
| 77 | /** |
| 78 | * Expiry time for use as max-age value in the cache-control header |
| 79 | * of completion search responses. |
| 80 | * @see $wgSearchSuggestCacheExpiry |
| 81 | * @var int|null |
| 82 | */ |
| 83 | private $completionCacheExpiry; |
| 84 | |
| 85 | public function __construct( |
| 86 | Config $config, |
| 87 | SearchEngineFactory $searchEngineFactory, |
| 88 | SearchEngineConfig $searchEngineConfig, |
| 89 | SearchResultThumbnailProvider $searchResultThumbnailProvider, |
| 90 | PermissionManager $permissionManager, |
| 91 | RedirectLookup $redirectLookup, |
| 92 | PageStore $pageStore, |
| 93 | TitleFormatter $titleFormatter, |
| 94 | SpecialPageFactory $specialPageFactory, |
| 95 | ) { |
| 96 | $this->searchEngineFactory = $searchEngineFactory; |
| 97 | $this->searchEngineConfig = $searchEngineConfig; |
| 98 | $this->searchResultThumbnailProvider = $searchResultThumbnailProvider; |
| 99 | $this->permissionManager = $permissionManager; |
| 100 | $this->redirectLookup = $redirectLookup; |
| 101 | $this->pageStore = $pageStore; |
| 102 | $this->titleFormatter = $titleFormatter; |
| 103 | |
| 104 | // @todo Avoid injecting the entire config, see T246377 |
| 105 | $this->completionCacheExpiry = $config->get( MainConfigNames::SearchSuggestCacheExpiry ); |
| 106 | $this->specialPageFactory = $specialPageFactory; |
| 107 | } |
| 108 | |
| 109 | protected function postInitSetup() { |
| 110 | $this->mode = $this->getConfig()['mode'] ?? self::FULLTEXT_MODE; |
| 111 | |
| 112 | if ( !in_array( $this->mode, self::SUPPORTED_MODES ) ) { |
| 113 | throw new InvalidArgumentException( |
| 114 | "Unsupported search mode `{$this->mode}` configured. Supported modes: " . |
| 115 | implode( ', ', self::SUPPORTED_MODES ) |
| 116 | ); |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | /** |
| 121 | * @return SearchEngine |
| 122 | */ |
| 123 | private function createSearchEngine() { |
| 124 | $limit = $this->getValidatedParams()['limit']; |
| 125 | |
| 126 | $searchEngine = $this->searchEngineFactory->create(); |
| 127 | $searchEngine->setNamespaces( $this->searchEngineConfig->defaultNamespaces() ); |
| 128 | $searchEngine->setLimitOffset( $limit, self::OFFSET ); |
| 129 | return $searchEngine; |
| 130 | } |
| 131 | |
| 132 | /** @inheritDoc */ |
| 133 | public function needsWriteAccess() { |
| 134 | return false; |
| 135 | } |
| 136 | |
| 137 | /** |
| 138 | * Get SearchResults when results are either SearchResultSet or Status objects |
| 139 | * @param ISearchResultSet|StatusValue|null $results |
| 140 | * @return SearchResult[] |
| 141 | * @throws LocalizedHttpException |
| 142 | */ |
| 143 | private function getSearchResultsOrThrow( $results ) { |
| 144 | if ( $results ) { |
| 145 | if ( $results instanceof StatusValue ) { |
| 146 | $status = $results; |
| 147 | if ( !$status->isOK() ) { |
| 148 | if ( $status->getMessages( 'error' ) ) { // Only throw for errors, suppress warnings (for now) |
| 149 | $this->throwExceptionForStatus( $status, 'rest-search-error', 500 ); |
| 150 | } |
| 151 | } |
| 152 | $statusValue = $status->getValue(); |
| 153 | if ( $statusValue instanceof ISearchResultSet ) { |
| 154 | return $statusValue->extractResults(); |
| 155 | } |
| 156 | } else { |
| 157 | return $results->extractResults(); |
| 158 | } |
| 159 | } |
| 160 | return []; |
| 161 | } |
| 162 | |
| 163 | /** |
| 164 | * Execute search and return info about pages for further processing. |
| 165 | * |
| 166 | * @param SearchEngine $searchEngine |
| 167 | * @return array[] |
| 168 | * @throws LocalizedHttpException |
| 169 | */ |
| 170 | private function doSearch( $searchEngine ) { |
| 171 | $query = $this->getValidatedParams()['q']; |
| 172 | |
| 173 | if ( $this->mode == self::COMPLETION_MODE ) { |
| 174 | $completionSearch = $searchEngine->completionSearchWithVariants( $query ); |
| 175 | return $this->buildPageObjects( $completionSearch->getSuggestions() ); |
| 176 | } else { |
| 177 | $titleSearch = $searchEngine->searchTitle( $query ); |
| 178 | $textSearch = $searchEngine->searchText( $query ); |
| 179 | |
| 180 | $titleSearchResults = $this->getSearchResultsOrThrow( $titleSearch ); |
| 181 | $textSearchResults = $this->getSearchResultsOrThrow( $textSearch ); |
| 182 | |
| 183 | $mergedResults = array_merge( $titleSearchResults, $textSearchResults ); |
| 184 | return $this->buildPageObjects( $mergedResults ); |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | /** |
| 189 | * Build an array of pageInfo objects. |
| 190 | * @param SearchSuggestion[]|SearchResult[] $searchResponse |
| 191 | * |
| 192 | * @phpcs:ignore Generic.Files.LineLength |
| 193 | * @phan-return array{int:array{pageIdentity:PageIdentity,suggestion:?SearchSuggestion,result:?SearchResult,redirect:?PageIdentity}} $pageInfos |
| 194 | * @return array Associative array mapping pageID to pageInfo objects: |
| 195 | * - pageIdentity: PageIdentity of page to return as the match |
| 196 | * - suggestion: SearchSuggestion or null if $searchResponse is SearchResults[] |
| 197 | * - result: SearchResult or null if $searchResponse is SearchSuggestions[] |
| 198 | * - redirect: PageIdentity or null if the SearchResult|SearchSuggestion was not a redirect |
| 199 | */ |
| 200 | private function buildPageObjects( array $searchResponse ): array { |
| 201 | $pageInfos = []; |
| 202 | foreach ( $searchResponse as $response ) { |
| 203 | $isSearchResult = $response instanceof SearchResult; |
| 204 | if ( $isSearchResult ) { |
| 205 | if ( $response->isBrokenTitle() || $response->isMissingRevision() ) { |
| 206 | continue; |
| 207 | } |
| 208 | $title = $response->getTitle(); |
| 209 | } else { |
| 210 | $title = $response->getSuggestedTitle(); |
| 211 | } |
| 212 | $pageObj = $this->buildSinglePage( $title, $response ); |
| 213 | if ( $pageObj ) { |
| 214 | $pageNsAndID = CacheKeyHelper::getKeyForPage( $pageObj['pageIdentity'] ); |
| 215 | // This handles the edge case where we have both the redirect source and redirect target page come back |
| 216 | // in our search results. In such event, we prefer (and thus replace) with the redirect target page. |
| 217 | if ( isset( $pageInfos[$pageNsAndID] ) ) { |
| 218 | if ( $pageInfos[$pageNsAndID]['redirect'] !== null ) { |
| 219 | $pageInfos[$pageNsAndID]['result'] = $isSearchResult ? $response : null; |
| 220 | $pageInfos[$pageNsAndID]['suggestion'] = $isSearchResult ? null : $response; |
| 221 | } |
| 222 | continue; |
| 223 | } |
| 224 | $pageInfos[$pageNsAndID] = $pageObj; |
| 225 | } |
| 226 | } |
| 227 | return $pageInfos; |
| 228 | } |
| 229 | |
| 230 | /** |
| 231 | * Build one pageInfo object from either a SearchResult or SearchSuggestion. |
| 232 | * @param PageIdentity $title |
| 233 | * @param SearchResult|SearchSuggestion $result |
| 234 | * |
| 235 | * @phpcs:ignore Generic.Files.LineLength |
| 236 | * @phan-return (false|array{pageIdentity:PageIdentity,suggestion:?SearchSuggestion,result:?SearchResult,redirect:?PageIdentity,anchor:?string,description:?string}) $pageInfos |
| 237 | * @return bool|array Objects representing a given page: |
| 238 | * - pageIdentity: PageIdentity of page to return as the match |
| 239 | * - suggestion: SearchSuggestion or null if $searchResponse is SearchResults |
| 240 | * - result: SearchResult or null if $searchResponse is SearchSuggestions |
| 241 | * - redirect: PageIdentity|null depending on if the SearchResult|SearchSuggestion was a redirect |
| 242 | * - anchor: string|null if the SearchResult|SearchSuggestion was a redirect, this is the page anchor (if any) |
| 243 | */ |
| 244 | private function buildSinglePage( $title, $result ) { |
| 245 | $redirectTarget = $title->canExist() ? $this->redirectLookup->getRedirectTarget( $title ) : null; |
| 246 | // Our page has a redirect that is not in a virtual namespace and is not an interwiki link. |
| 247 | // See T301346, T303352 |
| 248 | if ( $redirectTarget && $redirectTarget->getNamespace() > -1 && !$redirectTarget->isExternal() ) { |
| 249 | $redirectSource = $title; |
| 250 | $anchor = $redirectTarget->getFragment(); |
| 251 | $title = $this->pageStore->getPageForLink( $redirectTarget ); |
| 252 | } else { |
| 253 | $redirectSource = null; |
| 254 | $anchor = null; |
| 255 | } |
| 256 | if ( !$title || !$this->getAuthority()->probablyCan( 'read', $title ) ) { |
| 257 | return false; |
| 258 | } |
| 259 | return [ |
| 260 | 'pageIdentity' => $title, |
| 261 | 'suggestion' => $result instanceof SearchSuggestion ? $result : null, |
| 262 | 'result' => $result instanceof SearchResult ? $result : null, |
| 263 | 'redirect' => $redirectSource, |
| 264 | 'anchor' => $anchor, |
| 265 | 'description' => $this->getSpecialPageDescription( $title ), |
| 266 | ]; |
| 267 | } |
| 268 | |
| 269 | /** |
| 270 | * Turn array of page info into serializable array with common information about the page |
| 271 | * @param array $pageInfos Page Info objects |
| 272 | * @param array $thumbsAndDesc Associative array mapping pageId to array of description and thumbnail |
| 273 | * @phpcs:ignore Generic.Files.LineLength |
| 274 | * @phan-param array<int,array{pageIdentity:PageIdentity,suggestion:SearchSuggestion,result:SearchResult,redirect:?PageIdentity,anchor:?string,description:?string}> $pageInfos |
| 275 | * @phan-param array<int,array{description:array,thumbnail:array}> $thumbsAndDesc |
| 276 | * |
| 277 | * @phpcs:ignore Generic.Files.LineLength |
| 278 | * @phan-return array<int,array{id:int,key:string,title:string,excerpt:?string,matched_title:?string,anchor:?string, description:?array, thumbnail:?array}> $pages |
| 279 | * @return array[] of [ id, key, title, excerpt, matched_title, anchor ] |
| 280 | */ |
| 281 | private function buildResultFromPageInfos( array $pageInfos, array $thumbsAndDesc ): array { |
| 282 | $pages = []; |
| 283 | foreach ( $pageInfos as $pageInfo ) { |
| 284 | [ |
| 285 | 'pageIdentity' => $page, |
| 286 | 'suggestion' => $sugg, |
| 287 | 'result' => $result, |
| 288 | 'redirect' => $redirect, |
| 289 | 'anchor' => $anchor, |
| 290 | 'description' => $description, |
| 291 | ] = $pageInfo; |
| 292 | $excerpt = $sugg ? $sugg->getText() : $result->getTextSnippet(); |
| 293 | $id = ( $page instanceof PageIdentity && $page->canExist() ) ? $page->getId() : 0; |
| 294 | $pages[] = [ |
| 295 | 'id' => $id, |
| 296 | 'key' => $this->titleFormatter->getPrefixedDBkey( $page ), |
| 297 | 'title' => $this->titleFormatter->getPrefixedText( $page ), |
| 298 | 'excerpt' => $excerpt ?: null, |
| 299 | 'matched_title' => $redirect ? $this->titleFormatter->getPrefixedText( $redirect ) : null, |
| 300 | 'anchor' => $anchor ?: null, |
| 301 | 'description' => $id > 0 ? $thumbsAndDesc[$id]['description'] : $description, |
| 302 | 'thumbnail' => $id > 0 ? $thumbsAndDesc[$id]['thumbnail'] : null, |
| 303 | ]; |
| 304 | } |
| 305 | return $pages; |
| 306 | } |
| 307 | |
| 308 | /** |
| 309 | * Converts SearchResultThumbnail object into serializable array |
| 310 | * |
| 311 | * @param SearchResultThumbnail|null $thumbnail |
| 312 | * |
| 313 | * @return array|null |
| 314 | */ |
| 315 | private function serializeThumbnail( ?SearchResultThumbnail $thumbnail ): ?array { |
| 316 | if ( $thumbnail == null ) { |
| 317 | return null; |
| 318 | } |
| 319 | |
| 320 | return [ |
| 321 | 'mimetype' => $thumbnail->getMimeType(), |
| 322 | 'width' => $thumbnail->getWidth(), |
| 323 | 'height' => $thumbnail->getHeight(), |
| 324 | 'duration' => $thumbnail->getDuration(), |
| 325 | 'url' => $thumbnail->getUrl(), |
| 326 | ]; |
| 327 | } |
| 328 | |
| 329 | /** |
| 330 | * Return the page description if this PageIdentity refers to a SpecialPage. |
| 331 | * @param PageIdentity $title |
| 332 | * @return string|null the special page description, null if unknown or not a special page. |
| 333 | */ |
| 334 | private function getSpecialPageDescription( PageIdentity $title ): ?string { |
| 335 | if ( $title->getNamespace() === NS_SPECIAL ) { |
| 336 | return $this->specialPageFactory |
| 337 | ->getPage( $title->getDBkey() ) |
| 338 | ?->getDescription() |
| 339 | ?->plain(); |
| 340 | } |
| 341 | return null; |
| 342 | } |
| 343 | |
| 344 | /** |
| 345 | * Turn page info into serializable array with description field for the page. |
| 346 | * |
| 347 | * The information about description should be provided by extension by implementing |
| 348 | * 'SearchResultProvideDescription' hook. Description is set to null if no extensions |
| 349 | * implement the hook. |
| 350 | * @param PageIdentity[] $pageIdentities |
| 351 | * |
| 352 | * @return array |
| 353 | */ |
| 354 | private function buildDescriptionsFromPageIdentities( array $pageIdentities ) { |
| 355 | $descriptions = array_fill_keys( array_keys( $pageIdentities ), null ); |
| 356 | |
| 357 | $this->getHookRunner()->onSearchResultProvideDescription( $pageIdentities, $descriptions ); |
| 358 | |
| 359 | return array_map( static function ( $description ) { |
| 360 | return [ 'description' => $description ]; |
| 361 | }, $descriptions ); |
| 362 | } |
| 363 | |
| 364 | /** |
| 365 | * Turn page info into serializable array with thumbnail information for the page. |
| 366 | * |
| 367 | * The information about thumbnail should be provided by extension by implementing |
| 368 | * 'SearchResultProvideThumbnail' hook. Thumbnail is set to null if no extensions implement |
| 369 | * the hook. |
| 370 | * |
| 371 | * @param PageIdentity[] $pageIdentities |
| 372 | * |
| 373 | * @return array |
| 374 | */ |
| 375 | private function buildThumbnailsFromPageIdentities( array $pageIdentities ) { |
| 376 | $thumbnails = $this->searchResultThumbnailProvider->getThumbnails( $pageIdentities ); |
| 377 | $thumbnails += array_fill_keys( array_keys( $pageIdentities ), null ); |
| 378 | |
| 379 | return array_map( function ( $thumbnail ) { |
| 380 | return [ 'thumbnail' => $this->serializeThumbnail( $thumbnail ) ]; |
| 381 | }, $thumbnails ); |
| 382 | } |
| 383 | |
| 384 | /** |
| 385 | * @return Response |
| 386 | * @throws LocalizedHttpException |
| 387 | */ |
| 388 | public function execute() { |
| 389 | $searchEngine = $this->createSearchEngine(); |
| 390 | $pageInfos = $this->doSearch( $searchEngine ); |
| 391 | |
| 392 | // We can only pass validated "real" PageIdentities to our hook handlers below |
| 393 | $pageIdentities = array_reduce( |
| 394 | array_values( $pageInfos ), |
| 395 | static function ( $realPages, $item ) { |
| 396 | $page = $item['pageIdentity']; |
| 397 | if ( $page instanceof PageIdentity && $page->exists() ) { |
| 398 | $realPages[$item['pageIdentity']->getId()] = $item['pageIdentity']; |
| 399 | } |
| 400 | return $realPages; |
| 401 | }, [] |
| 402 | ); |
| 403 | |
| 404 | $descriptions = $this->buildDescriptionsFromPageIdentities( $pageIdentities ); |
| 405 | $thumbs = $this->buildThumbnailsFromPageIdentities( $pageIdentities ); |
| 406 | |
| 407 | $thumbsAndDescriptions = []; |
| 408 | foreach ( $descriptions as $pageId => $description ) { |
| 409 | $thumbsAndDescriptions[$pageId] = $description + $thumbs[$pageId]; |
| 410 | } |
| 411 | |
| 412 | $result = $this->buildResultFromPageInfos( $pageInfos, $thumbsAndDescriptions ); |
| 413 | |
| 414 | $response = $this->getResponseFactory()->createJson( [ 'pages' => $result ] ); |
| 415 | |
| 416 | if ( $this->mode === self::COMPLETION_MODE && $this->completionCacheExpiry ) { |
| 417 | // Type-ahead completion matches should be cached by the client and |
| 418 | // in the CDN, especially for short prefixes. |
| 419 | // See also $wgSearchSuggestCacheExpiry and ApiOpenSearch |
| 420 | if ( $this->permissionManager->isEveryoneAllowed( 'read' ) ) { |
| 421 | $cacheControl = 'public, max-age=' . $this->completionCacheExpiry; |
| 422 | } else { |
| 423 | $cacheControl = 'no-store, max-age=0'; |
| 424 | } |
| 425 | $response->setHeader( ResponseHeaders::CACHE_CONTROL, $cacheControl ); |
| 426 | } |
| 427 | $searchId = $searchEngine->getFeatureData( SearchEngine::SEARCH_ID ); |
| 428 | if ( $searchId ) { |
| 429 | // if the search backend provides a search id propagate it via headers. |
| 430 | $response->setHeader( 'X-Search-ID', $searchId ); |
| 431 | } |
| 432 | |
| 433 | return $response; |
| 434 | } |
| 435 | |
| 436 | /** @inheritDoc */ |
| 437 | public function getParamSettings() { |
| 438 | return [ |
| 439 | 'q' => [ |
| 440 | self::PARAM_SOURCE => 'query', |
| 441 | ParamValidator::PARAM_TYPE => 'string', |
| 442 | ParamValidator::PARAM_REQUIRED => true, |
| 443 | Handler::PARAM_DESCRIPTION => new MessageValue( 'rest-param-desc-search-q' ), |
| 444 | Handler::PARAM_EXAMPLE => 'jupiter', |
| 445 | ], |
| 446 | 'limit' => [ |
| 447 | self::PARAM_SOURCE => 'query', |
| 448 | ParamValidator::PARAM_TYPE => 'integer', |
| 449 | ParamValidator::PARAM_REQUIRED => false, |
| 450 | ParamValidator::PARAM_DEFAULT => self::LIMIT, |
| 451 | IntegerDef::PARAM_MIN => 1, |
| 452 | IntegerDef::PARAM_MAX => self::MAX_LIMIT, |
| 453 | Handler::PARAM_DESCRIPTION => new MessageValue( 'rest-param-desc-search-limit' ), |
| 454 | Handler::PARAM_EXAMPLE => 20, |
| 455 | ], |
| 456 | ]; |
| 457 | } |
| 458 | |
| 459 | public function getResponseBodySchemaFileName( string $method ): ?string { |
| 460 | return __DIR__ . '/Schema/SearchResults.json'; |
| 461 | } |
| 462 | |
| 463 | public function getResponseBodyExampleFileName( string $method ): ?string { |
| 464 | return __DIR__ . '/Example/SearchResults.json'; |
| 465 | } |
| 466 | } |