MediaWiki master
SpecialSearch.php
Go to the documentation of this file.
1<?php
9namespace MediaWiki\Specials;
10
18use MediaWiki\Languages\LanguageConverterFactory;
36use SearchEngine;
40
56 protected $profile;
57
59 protected $searchEngine;
60
62 protected $searchEngineType = null;
63
65 protected $extraParams = [];
66
71 protected $mPrefix;
72
73 protected int $limit;
74 protected int $offset;
75
79 protected $namespaces;
80
84 protected $fulltext;
85
89 protected $sort = SearchEngine::DEFAULT_SORT;
90
94 protected $runSuggestion = true;
95
100 protected $searchConfig;
101
102 private SearchEngineFactory $searchEngineFactory;
103 private NamespaceInfo $nsInfo;
104 private IContentHandlerFactory $contentHandlerFactory;
105 private InterwikiLookup $interwikiLookup;
106 private ReadOnlyMode $readOnlyMode;
107 private UserOptionsManager $userOptionsManager;
108 private LanguageConverterFactory $languageConverterFactory;
109 private RepoGroup $repoGroup;
110 private SearchResultThumbnailProvider $thumbnailProvider;
111 private TitleMatcher $titleMatcher;
112
117 private $loadStatus;
118
119 private const NAMESPACES_CURRENT = 'sense';
120
121 public function __construct(
123 SearchEngineFactory $searchEngineFactory,
124 NamespaceInfo $nsInfo,
125 IContentHandlerFactory $contentHandlerFactory,
126 InterwikiLookup $interwikiLookup,
127 ReadOnlyMode $readOnlyMode,
128 UserOptionsManager $userOptionsManager,
129 LanguageConverterFactory $languageConverterFactory,
130 RepoGroup $repoGroup,
131 SearchResultThumbnailProvider $thumbnailProvider,
132 TitleMatcher $titleMatcher
133 ) {
134 parent::__construct( 'Search' );
135 $this->searchConfig = $searchConfig;
136 $this->searchEngineFactory = $searchEngineFactory;
137 $this->nsInfo = $nsInfo;
138 $this->contentHandlerFactory = $contentHandlerFactory;
139 $this->interwikiLookup = $interwikiLookup;
140 $this->readOnlyMode = $readOnlyMode;
141 $this->userOptionsManager = $userOptionsManager;
142 $this->languageConverterFactory = $languageConverterFactory;
143 $this->repoGroup = $repoGroup;
144 $this->thumbnailProvider = $thumbnailProvider;
145 $this->titleMatcher = $titleMatcher;
146 }
147
153 public function execute( $par ) {
154 $request = $this->getRequest();
155 $out = $this->getOutput();
156
157 // Fetch the search term
158 $term = str_replace( "\n", " ", $request->getText( 'search' ) );
159
160 // Historically search terms have been accepted not only in the search query
161 // parameter, but also as part of the primary url. This can have PII implications
162 // in releasing page view data. As such issue a 301 redirect to the correct
163 // URL.
164 if ( $par !== null && $par !== '' && $term === '' ) {
165 $query = $request->getQueryValues();
166 unset( $query['title'] );
167 // Strip underscores from title parameter; most of the time we'll want
168 // text form here. But don't strip underscores from actual text params!
169 $query['search'] = str_replace( '_', ' ', $par );
170 $out->redirect( $this->getPageTitle()->getFullURL( $query ), 301 );
171 return;
172 }
173
174 // Need to load selected namespaces before handling nsRemember
175 $this->load();
176 // TODO: This performs database actions on GET request, which is going to
177 // be a problem for our multi-datacenter work.
178 if ( $request->getCheck( 'nsRemember' ) ) {
179 $this->saveNamespaces();
180 // Remove the token from the URL to prevent the user from inadvertently
181 // exposing it (e.g. by pasting it into a public wiki page) or undoing
182 // later settings changes (e.g. by reloading the page).
183 $query = $request->getQueryValues();
184 unset( $query['title'], $query['nsRemember'] );
185 $out->redirect( $this->getPageTitle()->getFullURL( $query ) );
186 return;
187 }
188
189 if ( !$request->getVal( 'fulltext' ) && !$request->getCheck( 'offset' ) ) {
190 $url = $this->goResult( $term );
191 if ( $url !== null ) {
192 // successful 'go'
193 $out->redirect( $url );
194 return;
195 }
196 // No match. If it could plausibly be a title
197 // run the No go match hook.
198 $title = Title::newFromText( $term );
199 if ( $title !== null ) {
200 $this->getHookRunner()->onSpecialSearchNogomatch( $title );
201 }
202 }
203
204 $this->setupPage( $term );
205
206 if ( $this->getConfig()->get( MainConfigNames::DisableTextSearch ) ) {
207 $searchForwardUrl = $this->getConfig()->get( MainConfigNames::SearchForwardUrl );
208 if ( $searchForwardUrl ) {
209 $url = str_replace( '$1', urlencode( $term ), $searchForwardUrl );
210 $out->redirect( $url );
211 } else {
212 $out->addHTML( Html::errorBox( Html::rawElement(
213 'p',
214 [ 'class' => 'mw-searchdisabled' ],
215 $this->msg( 'searchdisabled', [ 'mw:Special:MyLanguage/Manual:$wgSearchForwardUrl' ] )->parse()
216 ) ) );
217 $titleNs = count( $this->namespaces ) === 1 ? reset( $this->namespaces ) : null;
218 $title = Title::newFromText( $term, $titleNs );
219 $this->showCreateLink( $title, 0, null, null );
220 }
221
222 return;
223 }
224
225 $this->showResults( $term );
226 }
227
233 public function load() {
234 $this->loadStatus = new Status();
235
236 $request = $this->getRequest();
237 $this->searchEngineType = $request->getVal( 'srbackend' );
238
239 [ $this->limit, $this->offset ] = $request->getLimitOffsetForUser(
240 $this->getUser(),
241 20,
242 'searchlimit'
243 );
244 $this->mPrefix = $request->getVal( 'prefix', '' );
245 if ( $this->mPrefix !== '' ) {
246 $this->setExtraParam( 'prefix', $this->mPrefix );
247 }
248
249 $sort = $request->getVal( 'sort', SearchEngine::DEFAULT_SORT );
250 $validSorts = $this->getSearchEngine()->getValidSorts();
251 if ( !in_array( $sort, $validSorts ) ) {
252 $this->loadStatus->warning( 'search-invalid-sort-order', $sort,
253 implode( ', ', $validSorts ) );
254 } elseif ( $sort !== $this->sort ) {
255 $this->sort = $sort;
256 $this->setExtraParam( 'sort', $this->sort );
257 }
258
259 $user = $this->getUser();
260
261 # Extract manually requested namespaces
262 $nslist = $this->powerSearch( $request );
263 if ( $nslist === [] ) {
264 # Fallback to user preference
265 $nslist = $this->searchConfig->userNamespaces( $user );
266 }
267
268 $profile = null;
269 if ( $nslist === [] ) {
270 $profile = 'default';
271 }
272
273 $profile = $request->getVal( 'profile', $profile );
274 $profiles = $this->getSearchProfiles();
275 if ( $profile === null ) {
276 // BC with old request format
277 $profile = 'advanced';
278 foreach ( $profiles as $key => $data ) {
279 if ( $nslist === $data['namespaces'] && $key !== 'advanced' ) {
280 $profile = $key;
281 }
282 }
283 $this->namespaces = $nslist;
284 } elseif ( $profile === 'advanced' ) {
285 $this->namespaces = $nslist;
286 } elseif ( isset( $profiles[$profile]['namespaces'] ) ) {
287 $this->namespaces = $profiles[$profile]['namespaces'];
288 } else {
289 // Unknown profile requested
290 $this->loadStatus->warning( 'search-unknown-profile', $profile );
291 $profile = 'default';
292 $this->namespaces = $profiles['default']['namespaces'];
293 }
294
295 $this->fulltext = $request->getVal( 'fulltext' );
296 $this->runSuggestion = (bool)$request->getVal( 'runsuggestion', '1' );
297 $this->profile = $profile;
298 }
299
306 public function goResult( $term ) {
307 # If the string cannot be used to create a title
308 if ( Title::newFromText( $term ) === null ) {
309 return null;
310 }
311 # If there's an exact or very near match, jump right there.
312 $title = $this->titleMatcher->getNearMatch( $term );
313 if ( $title === null ) {
314 return null;
315 }
316 $url = null;
317 if ( !$this->getHookRunner()->onSpecialSearchGoResult( $term, $title, $url ) ) {
318 return null;
319 }
320
321 if (
322 // If there is a preference set to NOT redirect on exact page match
323 // then return null (which prevents direction)
324 !$this->redirectOnExactMatch()
325 // BUT ...
326 // ... ignore no-redirect preference if the exact page match is an interwiki link
327 && !$title->isExternal()
328 // ... ignore no-redirect preference if the exact page match is NOT in the main
329 // namespace AND there's a namespace in the search string
330 && !( $title->getNamespace() !== NS_MAIN && strpos( $term, ':' ) > 0 )
331 ) {
332 return null;
333 }
334
335 return $url ?? $title->getFullUrlForRedirect();
336 }
337
338 private function redirectOnExactMatch(): bool {
339 if ( !$this->getConfig()->get( MainConfigNames::SearchMatchRedirectPreference ) ) {
340 // If the preference for whether to redirect is disabled, use the default setting
341 return (bool)$this->userOptionsManager->getDefaultOption(
342 'search-match-redirect',
343 $this->getUser()
344 );
345 } else {
346 // Otherwise use the user's preference
347 return $this->userOptionsManager->getBoolOption( $this->getUser(), 'search-match-redirect' );
348 }
349 }
350
354 public function showResults( $term ) {
355 if ( $this->searchEngineType !== null ) {
356 $this->setExtraParam( 'srbackend', $this->searchEngineType );
357 }
358
359 $out = $this->getOutput();
360 $widgetOptions = $this->getConfig()->get( MainConfigNames::SpecialSearchFormOptions );
361 $formWidget = new SearchFormWidget(
362 new ServiceOptions(
364 $this->getConfig()
365 ),
366 $this,
367 $this->searchConfig,
368 $this->getHookContainer(),
369 $this->languageConverterFactory->getLanguageConverter( $this->getLanguage() ),
370 $this->nsInfo,
371 $this->getSearchProfiles()
372 );
373 $filePrefix = $this->getContentLanguage()->getFormattedNsText( NS_FILE ) . ':';
374 if ( trim( $term ) === '' || $filePrefix === trim( $term ) ) {
375 // Empty query -- straight view of search form
376 if ( !$this->getHookRunner()->onSpecialSearchResultsPrepend( $this, $out, $term ) ) {
377 # Hook requested termination
378 return;
379 }
380 $out->enableOOUI();
381 // The form also contains the 'Showing results 0 - 20 of 1234' so we can
382 // only do the form render here for the empty $term case. Rendering
383 // the form when a search is provided is repeated below.
384 $out->addHTML( $formWidget->render(
385 $this->profile, $term, 0, 0, false, $this->offset, $this->isPowerSearch(), $widgetOptions
386 ) );
387 return;
388 }
389
390 $engine = $this->getSearchEngine();
391 $engine->setFeatureData( 'rewrite', $this->runSuggestion );
392 $engine->setLimitOffset( $this->limit, $this->offset );
393 $engine->setNamespaces( $this->namespaces );
394 $engine->setSort( $this->sort );
395 $engine->prefix = $this->mPrefix;
396
397 $this->getHookRunner()->onSpecialSearchSetupEngine( $this, $this->profile, $engine );
398 if ( !$this->getHookRunner()->onSpecialSearchResultsPrepend( $this, $out, $term ) ) {
399 # Hook requested termination
400 return;
401 }
402
403 $titleNs = count( $this->namespaces ) === 1 ? reset( $this->namespaces ) : null;
404 $title = Title::newFromText( $term, $titleNs );
405 $languageConverter = $this->languageConverterFactory->getLanguageConverter( $this->getContentLanguage() );
406 if ( $languageConverter->hasVariants() ) {
407 // findVariantLink will replace the link arg as well but we want to keep our original
408 // search string, use a copy in the $variantTerm var so that $term remains intact.
409 $variantTerm = $term;
410 $languageConverter->findVariantLink( $variantTerm, $title );
411 }
412
413 $showSuggestion = $title === null || !$title->isKnown();
414 $engine->setShowSuggestion( $showSuggestion );
415
416 // fetch search results
417 $titleMatches = $engine->searchTitle( $term );
418 $textMatches = $engine->searchText( $term );
419
420 $textStatus = null;
421 if ( $textMatches instanceof Status ) {
422 $textStatus = $textMatches;
423 $textMatches = $textStatus->getValue();
424 }
425
426 if ( $textMatches && $textMatches->numRows() ) {
427 $engine->augmentSearchResults( $textMatches );
428 }
429
430 $this->getHookRunner()->onSpecialSearchResults( $term, $titleMatches, $textMatches );
431
432 // Get number of results
433 $titleMatchesNum = $textMatchesNum = $numTitleMatches = $numTextMatches = 0;
434 $approxTotalRes = false;
435 if ( $titleMatches ) {
436 $titleMatchesNum = $titleMatches->numRows();
437 $numTitleMatches = $titleMatches->getTotalHits();
438 $approxTotalRes = $titleMatches->isApproximateTotalHits();
439 }
440 if ( $textMatches ) {
441 $textMatchesNum = $textMatches->numRows();
442 $numTextMatches = $textMatches->getTotalHits();
443 $approxTotalRes = $approxTotalRes || $textMatches->isApproximateTotalHits();
444 }
445 $num = $titleMatchesNum + $textMatchesNum;
446 $totalRes = $numTitleMatches + $numTextMatches;
447
448 // start rendering the page
449 $out->enableOOUI();
450 $out->addHTML( $formWidget->render(
451 $this->profile, $term, $num, $totalRes, $approxTotalRes, $this->offset, $this->isPowerSearch(),
452 $widgetOptions
453 ) );
454
455 // did you mean... suggestions
456 if ( $textMatches ) {
457 $dymWidget = new DidYouMeanWidget( $this );
458 $out->addHTML( $dymWidget->render( $term, $textMatches ) );
459 }
460
461 $hasSearchErrors = $textStatus && $textStatus->getMessages() !== [];
462 $hasInlineIwResults = $textMatches &&
463 $textMatches->hasInterwikiResults( ISearchResultSet::INLINE_RESULTS );
464 $hasSecondaryIwResults = $textMatches &&
465 $textMatches->hasInterwikiResults( ISearchResultSet::SECONDARY_RESULTS );
466
467 $classNames = [ 'searchresults' ];
468 if ( $hasSecondaryIwResults ) {
469 $classNames[] = 'mw-searchresults-has-iw';
470 }
471 if ( $this->offset > 0 ) {
472 $classNames[] = 'mw-searchresults-has-offset';
473 }
474 $out->addHTML( Html::openElement( 'div', [ 'class' => $classNames ] ) );
475
476 $out->addHTML( '<div class="mw-search-results-info">' );
477
478 if ( $hasSearchErrors || $this->loadStatus->getMessages() ) {
479 if ( $textStatus === null ) {
480 $textStatus = $this->loadStatus;
481 } else {
482 $textStatus->merge( $this->loadStatus );
483 }
484 [ $error, $warning ] = $textStatus->splitByErrorType();
485 if ( $error->getMessages() ) {
486 $out->addHTML( Html::errorBox(
487 $error->getHTML( 'search-error' )
488 ) );
489 }
490 if ( $warning->getMessages() ) {
491 $out->addHTML( Html::warningBox(
492 $warning->getHTML( 'search-warning' )
493 ) );
494 }
495 }
496
497 // If we have no results and have not already displayed an error message
498 if ( $num === 0 && !$hasSearchErrors ) {
499 $out->wrapWikiMsg( "<p class=\"mw-search-nonefound\">\n$1</p>", [
500 $hasInlineIwResults ? 'search-nonefound-thiswiki' : 'search-nonefound',
501 wfEscapeWikiText( $term ),
502 $term
503 ] );
504 }
505
506 // Show the create link ahead
507 $this->showCreateLink( $title, $num, $titleMatches, $textMatches );
508
509 // Close <div class='mw-search-results-info'>
510 $out->addHTML( '</div>' );
511
512 // Although $num might be 0 there can still be secondary or inline
513 // results to display.
514 $linkRenderer = $this->getLinkRenderer();
515 $mainResultWidget = new FullSearchResultWidget(
516 $this,
517 $linkRenderer,
518 $this->getHookContainer(),
519 $this->repoGroup,
520 $this->thumbnailProvider,
521 $this->userOptionsManager
522 );
523
524 $sidebarResultWidget = new InterwikiSearchResultWidget( $this, $linkRenderer );
525 $sidebarResultsWidget = new InterwikiSearchResultSetWidget(
526 $this,
527 $sidebarResultWidget,
528 $linkRenderer,
529 $this->interwikiLookup,
530 $engine->getFeatureData( 'show-multimedia-search-results' ) ?? false
531 );
532
533 $widget = new BasicSearchResultSetWidget( $this, $mainResultWidget, $sidebarResultsWidget );
534
535 $out->addHTML( '<div class="mw-search-visualclear"></div>' );
536 $this->prevNextLinks( $totalRes, $textMatches, $term, 'mw-search-pager-top', $out );
537
538 $out->addHTML( $widget->render(
539 $term, $this->offset, $titleMatches, $textMatches
540 ) );
541
542 $out->addHTML( '<div class="mw-search-visualclear"></div>' );
543 $this->prevNextLinks( $totalRes, $textMatches, $term, 'mw-search-pager-bottom', $out );
544
545 // Close <div class='searchresults'>
546 $out->addHTML( "</div>" );
547
548 $this->getHookRunner()->onSpecialSearchResultsAppend( $this, $out, $term );
549 }
550
557 protected function showCreateLink( $title, $num, $titleMatches, $textMatches ) {
558 // show direct page/create link if applicable
559
560 // Check DBkey !== '' in case of fragment link only.
561 if ( $title === null || $title->getDBkey() === ''
562 || ( $titleMatches !== null && $titleMatches->searchContainedSyntax() )
563 || ( $textMatches !== null && $textMatches->searchContainedSyntax() )
564 ) {
565 // invalid title
566 // preserve the paragraph for margins etc...
567 $this->getOutput()->addHTML( '<p></p>' );
568
569 return;
570 }
571
572 $messageName = 'searchmenu-new-nocreate';
573 $linkClass = 'mw-search-createlink';
574
575 if ( !$title->isExternal() ) {
576 if ( $title->isKnown() ) {
577 $firstTitle = null;
578 if ( $titleMatches && $titleMatches->numRows() > 0 ) {
579 $firstTitle = $titleMatches->extractTitles()[0] ?? null;
580 } elseif ( $textMatches && $textMatches->numRows() > 0 ) {
581 $firstTitle = $textMatches->extractTitles()[0] ?? null;
582 }
583
584 if ( $firstTitle && $title->isSamePageAs( $firstTitle ) ) {
585 $messageName = '';
586 } else {
587 $messageName = 'searchmenu-exists';
588 $linkClass = 'mw-search-exists';
589 }
590 } elseif (
591 $this->contentHandlerFactory->getContentHandler( $title->getContentModel() )
592 ->supportsDirectEditing()
593 && $this->getAuthority()->probablyCan( 'edit', $title )
594 ) {
595 $messageName = 'searchmenu-new';
596 }
597 } else {
598 $messageName = 'searchmenu-new-external';
599 }
600
601 $params = [
602 $messageName,
603 wfEscapeWikiText( $title->getPrefixedText() ),
604 Message::numParam( $num )
605 ];
606 $this->getHookRunner()->onSpecialSearchCreateLink( $title, $params );
607
608 // Extensions using the hook might still return an empty $messageName
609 if ( $messageName ) {
610 $this->getOutput()->wrapWikiMsg( "<p class=\"$linkClass\">\n$1</p>", $params );
611 }
612 }
613
620 protected function setupPage( $term ) {
621 $out = $this->getOutput();
622
623 $this->setHeaders();
624 $this->outputHeader();
625 // TODO: Is this true? The namespace remember uses a user token
626 // on save.
627 $out->getMetadata()->setPreventClickjacking( false );
628 $this->addHelpLink( 'Help:Searching' );
629
630 if ( strval( $term ) !== '' ) {
631 $out->setPageTitleMsg( $this->msg( 'searchresults' ) );
632 $out->setHTMLTitle( $this->msg( 'pagetitle' )
633 ->plaintextParams( $this->msg( 'searchresults-title' )->plaintextParams( $term )->text() )
634 ->inContentLanguage()->text()
635 );
636 }
637
638 if ( $this->mPrefix !== '' ) {
639 $subtitle = $this->msg( 'search-filter-title-prefix' )->plaintextParams( $this->mPrefix );
640 $params = $this->powerSearchOptions();
641 unset( $params['prefix'] );
642 $params += [
643 'search' => $term,
644 'fulltext' => 1,
645 ];
646
647 $subtitle .= ' (';
648 $subtitle .= Html::element(
649 'a',
650 [
651 'href' => $this->getPageTitle()->getLocalURL( $params ),
652 'title' => $this->msg( 'search-filter-title-prefix-reset' )->text(),
653 ],
654 $this->msg( 'search-filter-title-prefix-reset' )->text()
655 );
656 $subtitle .= ')';
657 $out->setSubtitle( $subtitle );
658 }
659
660 $out->addJsConfigVars( [ 'searchTerm' => $term ] );
661 $out->addModules( 'mediawiki.special.search' );
662 $out->addModuleStyles( [
663 'mediawiki.special', 'mediawiki.special.search.styles',
664 'mediawiki.widgets.SearchInputWidget.styles',
665 // Special page makes use of Html::warningBox and Html::errorBox in multiple places.
666 'mediawiki.codex.messagebox.styles',
667 ] );
668 }
669
675 protected function isPowerSearch() {
676 return $this->profile === 'advanced';
677 }
678
686 protected function powerSearch( &$request ) {
687 $arr = [];
688 foreach ( $this->searchConfig->searchableNamespaces() as $ns => $_ ) {
689 if ( $request->getCheck( 'ns' . $ns ) ) {
690 $arr[] = $ns;
691 }
692 }
693
694 return $arr;
695 }
696
704 public function powerSearchOptions() {
705 $opt = [];
706 if ( $this->isPowerSearch() ) {
707 foreach ( $this->namespaces as $n ) {
708 $opt['ns' . $n] = 1;
709 }
710 } else {
711 $opt['profile'] = $this->profile;
712 }
713
714 return $opt + $this->extraParams;
715 }
716
722 protected function saveNamespaces() {
723 $user = $this->getUser();
724 $request = $this->getRequest();
725
726 if ( $user->isRegistered() &&
727 $user->matchEditToken(
728 $request->getVal( 'nsRemember' ),
729 'searchnamespace',
730 $request
731 ) && !$this->readOnlyMode->isReadOnly()
732 ) {
733 // Reset namespace preferences: namespaces are not searched
734 // when they're not mentioned in the URL parameters.
735 foreach ( $this->nsInfo->getValidNamespaces() as $n ) {
736 $this->userOptionsManager->setOption( $user, 'searchNs' . $n, false );
737 }
738 // The request parameters include all the namespaces to be searched.
739 // Even if they're the same as an existing profile, they're not eaten.
740 foreach ( $this->namespaces as $n ) {
741 $this->userOptionsManager->setOption( $user, 'searchNs' . $n, true );
742 }
743
744 DeferredUpdates::addCallableUpdate( static function () use ( $user ) {
745 $user->saveSettings();
746 } );
747
748 return true;
749 }
750
751 return false;
752 }
753
758 protected function getSearchProfiles() {
759 // Builds list of Search Types (profiles)
760 $nsAllSet = array_keys( $this->searchConfig->searchableNamespaces() );
761 $defaultNs = $this->searchConfig->defaultNamespaces();
762 $profiles = [
763 'default' => [
764 'message' => 'searchprofile-articles',
765 'tooltip' => 'searchprofile-articles-tooltip',
766 'namespaces' => $defaultNs,
767 'namespace-messages' => $this->searchConfig->namespacesAsText(
768 $defaultNs
769 ),
770 ],
771 'images' => [
772 'message' => 'searchprofile-images',
773 'tooltip' => 'searchprofile-images-tooltip',
774 'namespaces' => [ NS_FILE ],
775 ],
776 'all' => [
777 'message' => 'searchprofile-everything',
778 'tooltip' => 'searchprofile-everything-tooltip',
779 'namespaces' => $nsAllSet,
780 ],
781 'advanced' => [
782 'message' => 'searchprofile-advanced',
783 'tooltip' => 'searchprofile-advanced-tooltip',
784 'namespaces' => self::NAMESPACES_CURRENT,
785 ]
786 ];
787
788 $this->getHookRunner()->onSpecialSearchProfiles( $profiles );
789
790 foreach ( $profiles as &$data ) {
791 if ( !is_array( $data['namespaces'] ) ) {
792 continue;
793 }
794 sort( $data['namespaces'] );
795 }
796
797 return $profiles;
798 }
799
805 public function getSearchEngine() {
806 if ( $this->searchEngine === null ) {
807 $this->searchEngine = $this->searchEngineFactory->create( $this->searchEngineType );
808 }
809
810 return $this->searchEngine;
811 }
812
817 public function getProfile() {
818 return $this->profile;
819 }
820
825 public function getNamespaces() {
826 return $this->namespaces;
827 }
828
838 public function setExtraParam( $key, $value ) {
839 $this->extraParams[$key] = $value;
840 }
841
850 public function getPrefix() {
851 return $this->mPrefix;
852 }
853
861 private function prevNextLinks(
862 ?int $totalRes,
863 ?ISearchResultSet $textMatches,
864 string $term,
865 string $class,
866 OutputPage $out
867 ) {
868 if ( $totalRes > $this->limit || $this->offset ) {
869 // Allow matches to define the correct offset, as interleaved
870 // AB testing may require a different next page offset.
871 if ( $textMatches && $textMatches->getOffset() !== null ) {
872 $offset = $textMatches->getOffset();
873 } else {
874 $offset = $this->offset;
875 }
876
877 // use the rewritten search term for subsequent page searches
878 $newSearchTerm = $term;
879 if ( $textMatches && $textMatches->hasRewrittenQuery() ) {
880 $newSearchTerm = $textMatches->getQueryAfterRewrite();
881 }
882
883 $prevNext =
884 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable offset is not null
885 $this->buildPrevNextNavigation( $offset, $this->limit,
886 $this->powerSearchOptions() + [ 'search' => $newSearchTerm ],
887 $this->limit + $this->offset >= $totalRes );
888 $out->addHTML( "<div class='{$class}'>{$prevNext}</div>\n" );
889 }
890 }
891
893 protected function getGroupName() {
894 return 'pages';
895 }
896}
897
902class_alias( SpecialSearch::class, 'SpecialSearch' );
const NS_FILE
Definition Defines.php:57
const NS_MAIN
Definition Defines.php:51
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:68
A class for passing options to services.
Defer callable updates to run later in the PHP process.
Prioritized list of file repositories.
Definition RepoGroup.php:30
This class is a collection of static functions that serve two purposes:
Definition Html.php:43
A class containing constants representing the names of configuration variables.
const SearchForwardUrl
Name constant for the SearchForwardUrl setting, for use with Config::get()
const DisableTextSearch
Name constant for the DisableTextSearch setting, for use with Config::get()
const SpecialSearchFormOptions
Name constant for the SpecialSearchFormOptions setting, for use with Config::get()
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
This is one of the Core classes and should be read at least once by any new developers.
setSubtitle( $str)
Replace the subtitle with $str.
addJsConfigVars( $keys, $value=null)
Add one or more variables to be set in mw.config in JavaScript.
wrapWikiMsg( $wrap,... $msgSpecs)
This function takes a number of message/argument specifications, wraps them in some overall structure...
setPageTitleMsg(Message $msg)
"Page title" means the contents of <h1>.
addModules( $modules)
Load one or more ResourceLoader modules on this page.
redirect( $url, $responsecode='302')
Redirect to $url rather than displaying the normal page.
setHTMLTitle( $name)
"HTML title" means the contents of "<title>".
enableOOUI()
Add ResourceLoader module styles for OOUI and set up the PHP implementation of it for use with MediaW...
addHTML( $text)
Append $text to the body HTML.
addModuleStyles( $modules)
Load the styles of one or more style-only ResourceLoader modules on this page.
getMetadata()
Return a ParserOutput that can be used to set metadata properties for the current page.
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form,...
Renders a suggested search for the user, or tells the user a suggested search was run instead of the ...
Renders a 'full' multi-line search result with metadata.
Renders one or more ISearchResultSets into a sidebar grouped by interwiki prefix.
Service implementation of near match title search.
Parent class for all special pages.
getUser()
Shortcut to get the User executing this instance.
getPageTitle( $subpage=false)
Get a self-referential title object.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
Run text & title search and display the output.
null string $profile
Current search profile.
showCreateLink( $title, $num, $titleMatches, $textMatches)
getProfile()
Current search profile.
setupPage( $term)
Sets up everything for the HTML output page including styles, javascript, page title,...
getPrefix()
The prefix value send to Special:Search using the 'prefix' URI param It means that the user is willin...
string null $searchEngineType
Search engine type, if not default.
isPowerSearch()
Return true if current search is a power (advanced) search.
powerSearchOptions()
Reconstruct the 'power search' options for links TODO: Instead of exposing this publicly,...
string $mPrefix
The prefix url parameter.
setExtraParam( $key, $value)
Users of hook SpecialSearchSetupEngine can use this to add more params to links to not lose selection...
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
SearchEngine $searchEngine
Search engine.
saveNamespaces()
Save namespace preferences when we're supposed to.
__construct(SearchEngineConfig $searchConfig, SearchEngineFactory $searchEngineFactory, NamespaceInfo $nsInfo, IContentHandlerFactory $contentHandlerFactory, InterwikiLookup $interwikiLookup, ReadOnlyMode $readOnlyMode, UserOptionsManager $userOptionsManager, LanguageConverterFactory $languageConverterFactory, RepoGroup $repoGroup, SearchResultThumbnailProvider $thumbnailProvider, TitleMatcher $titleMatcher)
powerSearch(&$request)
Extract "power search" namespace settings from the request object, returning a list of index numbers ...
getNamespaces()
Current namespaces.
load()
Set up basic search parameters from the request and user settings.
SearchEngineConfig $searchConfig
Search engine configurations.
goResult( $term)
If an exact title match can be found, jump straight ahead to it.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Represents a title within MediaWiki.
Definition Title.php:70
A service class to control user options.
Configuration handling class for SearchEngine.
Factory class for SearchEngine.
Contain a class for special pages.
Determine whether a site is currently in read-only mode.
A set of SearchEngine results.
searchContainedSyntax()
Did the search contain search syntax? If so, Special:Search won't offer the user a link to a create a...
hasInterwikiResults( $type=self::SECONDARY_RESULTS)
Check if there are results on other wikis.
hasRewrittenQuery()
Some search modes will run an alternative query that it thinks gives a better result than the provide...
isApproximateTotalHits()
If getTotalHits() is supported determine whether this number is approximate or not.
extractTitles()
Extract all the titles in the result set.
getTotalHits()
Some search modes return a total hit count for the query in the entire article database.
Service interface for looking up Interwiki records.
element(SerializerNode $parent, SerializerNode $node, $contents)