MediaWiki  master
SpecialSearch.php
Go to the documentation of this file.
1 <?php
38 
43 class SpecialSearch extends SpecialPage {
52  protected $profile;
53 
55  protected $searchEngine;
56 
58  protected $searchEngineType = null;
59 
61  protected $extraParams = [];
62 
67  protected $mPrefix;
68 
72  protected $limit, $offset;
73 
77  protected $namespaces;
78 
82  protected $fulltext;
83 
88 
92  protected $runSuggestion = true;
93 
98  protected $searchConfig;
99 
101  private $searchEngineFactory;
102 
104  private $nsInfo;
105 
107  private $contentHandlerFactory;
108 
110  private $interwikiLookup;
111 
113  private $readOnlyMode;
114 
116  private $userOptionsManager;
117 
119  private $languageConverterFactory;
120 
122  private $repoGroup;
123 
125  private $thumbnailProvider;
126 
131  private $loadStatus;
132 
133  private const NAMESPACES_CURRENT = 'sense';
134 
147  public function __construct(
149  SearchEngineFactory $searchEngineFactory,
150  NamespaceInfo $nsInfo,
151  IContentHandlerFactory $contentHandlerFactory,
152  InterwikiLookup $interwikiLookup,
153  ReadOnlyMode $readOnlyMode,
154  UserOptionsManager $userOptionsManager,
155  LanguageConverterFactory $languageConverterFactory,
156  RepoGroup $repoGroup,
157  SearchResultThumbnailProvider $thumbnailProvider
158  ) {
159  parent::__construct( 'Search' );
160  $this->searchConfig = $searchConfig;
161  $this->searchEngineFactory = $searchEngineFactory;
162  $this->nsInfo = $nsInfo;
163  $this->contentHandlerFactory = $contentHandlerFactory;
164  $this->interwikiLookup = $interwikiLookup;
165  $this->readOnlyMode = $readOnlyMode;
166  $this->userOptionsManager = $userOptionsManager;
167  $this->languageConverterFactory = $languageConverterFactory;
168  $this->repoGroup = $repoGroup;
169  $this->thumbnailProvider = $thumbnailProvider;
170  }
171 
177  public function execute( $par ) {
178  $request = $this->getRequest();
179  $out = $this->getOutput();
180 
181  // Fetch the search term
182  $term = str_replace( "\n", " ", $request->getText( 'search' ) );
183 
184  // Historically search terms have been accepted not only in the search query
185  // parameter, but also as part of the primary url. This can have PII implications
186  // in releasing page view data. As such issue a 301 redirect to the correct
187  // URL.
188  if ( $par !== null && $par !== '' && $term === '' ) {
189  $query = $request->getValues();
190  unset( $query['title'] );
191  // Strip underscores from title parameter; most of the time we'll want
192  // text form here. But don't strip underscores from actual text params!
193  $query['search'] = str_replace( '_', ' ', $par );
194  $out->redirect( $this->getPageTitle()->getFullURL( $query ), 301 );
195  return;
196  }
197 
198  // Need to load selected namespaces before handling nsRemember
199  $this->load();
200  // TODO: This performs database actions on GET request, which is going to
201  // be a problem for our multi-datacenter work.
202  if ( $request->getCheck( 'nsRemember' ) ) {
203  $this->saveNamespaces();
204  // Remove the token from the URL to prevent the user from inadvertently
205  // exposing it (e.g. by pasting it into a public wiki page) or undoing
206  // later settings changes (e.g. by reloading the page).
207  $query = $request->getValues();
208  unset( $query['title'], $query['nsRemember'] );
209  $out->redirect( $this->getPageTitle()->getFullURL( $query ) );
210  return;
211  }
212 
213  if ( !$request->getVal( 'fulltext' ) && !$request->getCheck( 'offset' ) ) {
214  $url = $this->goResult( $term );
215  if ( $url !== null ) {
216  // successful 'go'
217  $out->redirect( $url );
218  return;
219  }
220  // No match. If it could plausibly be a title
221  // run the No go match hook.
222  $title = Title::newFromText( $term );
223  if ( $title !== null ) {
224  $this->getHookRunner()->onSpecialSearchNogomatch( $title );
225  }
226  }
227 
228  $this->setupPage( $term );
229 
230  if ( $this->getConfig()->get( MainConfigNames::DisableTextSearch ) ) {
231  $searchForwardUrl = $this->getConfig()->get( MainConfigNames::SearchForwardUrl );
232  if ( $searchForwardUrl ) {
233  $url = str_replace( '$1', urlencode( $term ), $searchForwardUrl );
234  $out->redirect( $url );
235  } else {
236  $out->addHTML( $this->showGoogleSearch( $term ) );
237  }
238 
239  return;
240  }
241 
242  $this->showResults( $term );
243  }
244 
253  private function showGoogleSearch( $term ) {
254  return "<fieldset>" .
255  "<legend>" .
256  $this->msg( 'search-external' )->escaped() .
257  "</legend>" .
258  "<p class='mw-searchdisabled'>" .
259  $this->msg( 'searchdisabled' )->escaped() .
260  "</p>" .
261  // googlesearch is part of $wgRawHtmlMessages and safe to use as is here
262  $this->msg( 'googlesearch' )->rawParams(
263  htmlspecialchars( $term ),
264  'UTF-8',
265  $this->msg( 'searchbutton' )->escaped()
266  )->text() .
267  "</fieldset>";
268  }
269 
275  public function load() {
276  $this->loadStatus = new Status();
277 
278  $request = $this->getRequest();
279  $this->searchEngineType = $request->getVal( 'srbackend' );
280 
281  [ $this->limit, $this->offset ] = $request->getLimitOffsetForUser(
282  $this->getUser(),
283  20,
284  'searchlimit'
285  );
286  $this->mPrefix = $request->getVal( 'prefix', '' );
287  if ( $this->mPrefix !== '' ) {
288  $this->setExtraParam( 'prefix', $this->mPrefix );
289  }
290 
291  $sort = $request->getVal( 'sort', SearchEngine::DEFAULT_SORT );
292  $validSorts = $this->getSearchEngine()->getValidSorts();
293  if ( !in_array( $sort, $validSorts ) ) {
294  $this->loadStatus->warning( 'search-invalid-sort-order', $sort,
295  implode( ', ', $validSorts ) );
296  } elseif ( $sort !== $this->sort ) {
297  $this->sort = $sort;
298  $this->setExtraParam( 'sort', $this->sort );
299  }
300 
301  $user = $this->getUser();
302 
303  # Extract manually requested namespaces
304  $nslist = $this->powerSearch( $request );
305  if ( $nslist === [] ) {
306  # Fallback to user preference
307  $nslist = $this->searchConfig->userNamespaces( $user );
308  }
309 
310  $profile = null;
311  if ( $nslist === [] ) {
312  $profile = 'default';
313  }
314 
315  $profile = $request->getVal( 'profile', $profile );
316  $profiles = $this->getSearchProfiles();
317  if ( $profile === null ) {
318  // BC with old request format
319  $profile = 'advanced';
320  foreach ( $profiles as $key => $data ) {
321  if ( $nslist === $data['namespaces'] && $key !== 'advanced' ) {
322  $profile = $key;
323  }
324  }
325  $this->namespaces = $nslist;
326  } elseif ( $profile === 'advanced' ) {
327  $this->namespaces = $nslist;
328  } elseif ( isset( $profiles[$profile]['namespaces'] ) ) {
329  $this->namespaces = $profiles[$profile]['namespaces'];
330  } else {
331  // Unknown profile requested
332  $this->loadStatus->warning( 'search-unknown-profile', $profile );
333  $profile = 'default';
334  $this->namespaces = $profiles['default']['namespaces'];
335  }
336 
337  $this->fulltext = $request->getVal( 'fulltext' );
338  $this->runSuggestion = (bool)$request->getVal( 'runsuggestion', '1' );
339  $this->profile = $profile;
340  }
341 
348  public function goResult( $term ) {
349  # If the string cannot be used to create a title
350  if ( Title::newFromText( $term ) === null ) {
351  return null;
352  }
353  # If there's an exact or very near match, jump right there.
354  $title = $this->getSearchEngine()
355  ->getNearMatcher( $this->getConfig() )->getNearMatch( $term );
356  if ( $title === null ) {
357  return null;
358  }
359  $url = null;
360  if ( !$this->getHookRunner()->onSpecialSearchGoResult( $term, $title, $url ) ) {
361  return null;
362  }
363 
364  if (
365  // If there is a preference set to NOT redirect on exact page match
366  // then return null (which prevents direction)
367  !$this->redirectOnExactMatch()
368  // BUT ...
369  // ... ignore no-redirect preference if the exact page match is an interwiki link
370  && !$title->isExternal()
371  // ... ignore no-redirect preference if the exact page match is NOT in the main
372  // namespace AND there's a namespace in the search string
373  && !( $title->getNamespace() !== NS_MAIN && strpos( $term, ':' ) > 0 )
374  ) {
375  return null;
376  }
377 
378  return $url ?? $title->getFullUrlForRedirect();
379  }
380 
381  private function redirectOnExactMatch() {
382  if ( !$this->getConfig()->get( MainConfigNames::SearchMatchRedirectPreference ) ) {
383  // If the preference for whether to redirect is disabled, use the default setting
384  $defaultOptions = $this->userOptionsManager->getDefaultOptions();
385  return $defaultOptions['search-match-redirect'];
386  } else {
387  // Otherwise use the user's preference
388  return $this->userOptionsManager->getOption( $this->getUser(), 'search-match-redirect' );
389  }
390  }
391 
395  public function showResults( $term ) {
396  if ( $this->searchEngineType !== null ) {
397  $this->setExtraParam( 'srbackend', $this->searchEngineType );
398  }
399 
400  $out = $this->getOutput();
401  $widgetOptions = $this->getConfig()->get( MainConfigNames::SpecialSearchFormOptions );
403  $this,
404  $this->searchConfig,
405  $this->getHookContainer(),
406  $this->languageConverterFactory->getLanguageConverter( $this->getLanguage() ),
407  $this->nsInfo,
408  $this->getSearchProfiles()
409  );
410  $filePrefix = $this->getContentLanguage()->getFormattedNsText( NS_FILE ) . ':';
411  if ( trim( $term ) === '' || $filePrefix === trim( $term ) ) {
412  // Empty query -- straight view of search form
413  if ( !$this->getHookRunner()->onSpecialSearchResultsPrepend( $this, $out, $term ) ) {
414  # Hook requested termination
415  return;
416  }
417  $out->enableOOUI();
418  // The form also contains the 'Showing results 0 - 20 of 1234' so we can
419  // only do the form render here for the empty $term case. Rendering
420  // the form when a search is provided is repeated below.
421  $out->addHTML( $formWidget->render(
422  $this->profile, $term, 0, 0, $this->offset, $this->isPowerSearch(), $widgetOptions
423  ) );
424  return;
425  }
426 
427  $engine = $this->getSearchEngine();
428  $engine->setFeatureData( 'rewrite', $this->runSuggestion );
429  $engine->setLimitOffset( $this->limit, $this->offset );
430  $engine->setNamespaces( $this->namespaces );
431  $engine->setSort( $this->sort );
432  $engine->prefix = $this->mPrefix;
433 
434  $this->getHookRunner()->onSpecialSearchSetupEngine( $this, $this->profile, $engine );
435  if ( !$this->getHookRunner()->onSpecialSearchResultsPrepend( $this, $out, $term ) ) {
436  # Hook requested termination
437  return;
438  }
439 
440  $title = Title::newFromText( $term );
441  $languageConverter = $this->languageConverterFactory->getLanguageConverter( $this->getContentLanguage() );
442  if ( $languageConverter->hasVariants() ) {
443  // findVariantLink will replace the link arg as well but we want to keep our original
444  // search string, use a copy in the $variantTerm var so that $term remains intact.
445  $variantTerm = $term;
446  $languageConverter->findVariantLink( $variantTerm, $title );
447  }
448 
449  $showSuggestion = $title === null || !$title->isKnown();
450  $engine->setShowSuggestion( $showSuggestion );
451 
452  $rewritten = $engine->replacePrefixes( $term );
453  if ( $rewritten !== $term ) {
454  wfDeprecatedMsg( 'SearchEngine::replacePrefixes() was overridden by ' .
455  get_class( $engine ) . ', this is deprecated since MediaWiki 1.32',
456  '1.32', false, false );
457  }
458 
459  // fetch search results
460  $titleMatches = $engine->searchTitle( $rewritten );
461  $textMatches = $engine->searchText( $rewritten );
462 
463  $textStatus = null;
464  if ( $textMatches instanceof Status ) {
465  $textStatus = $textMatches;
466  $textMatches = $textStatus->getValue();
467  }
468 
469  // Get number of results
470  $titleMatchesNum = $textMatchesNum = $numTitleMatches = $numTextMatches = 0;
471  if ( $titleMatches ) {
472  $titleMatchesNum = $titleMatches->numRows();
473  $numTitleMatches = $titleMatches->getTotalHits();
474  }
475  if ( $textMatches ) {
476  $textMatchesNum = $textMatches->numRows();
477  $numTextMatches = $textMatches->getTotalHits();
478  if ( $textMatchesNum > 0 ) {
479  $engine->augmentSearchResults( $textMatches );
480  }
481  }
482  $num = $titleMatchesNum + $textMatchesNum;
483  $totalRes = $numTitleMatches + $numTextMatches;
484 
485  // start rendering the page
486  $out->enableOOUI();
487  $out->addHTML( $formWidget->render(
488  $this->profile, $term, $num, $totalRes, $this->offset, $this->isPowerSearch(), $widgetOptions
489  ) );
490 
491  // did you mean... suggestions
492  if ( $textMatches ) {
493  $dymWidget = new MediaWiki\Search\SearchWidgets\DidYouMeanWidget( $this );
494  $out->addHTML( $dymWidget->render( $term, $textMatches ) );
495  }
496 
497  $hasSearchErrors = $textStatus && $textStatus->getErrors() !== [];
498  $hasInlineIwResults = $textMatches &&
500  $hasSecondaryIwResults = $textMatches &&
502 
503  $classNames = [ 'searchresults' ];
504  if ( $hasSecondaryIwResults ) {
505  $classNames[] = 'mw-searchresults-has-iw';
506  }
507  if ( $this->offset > 0 ) {
508  $classNames[] = 'mw-searchresults-has-offset';
509  }
510  $out->addHTML( '<div class="' . implode( ' ', $classNames ) . '">' );
511 
512  $out->addHTML( '<div class="mw-search-results-info">' );
513 
514  if ( $hasSearchErrors || $this->loadStatus->getErrors() ) {
515  if ( $textStatus === null ) {
516  $textStatus = $this->loadStatus;
517  } else {
518  $textStatus->merge( $this->loadStatus );
519  }
520  [ $error, $warning ] = $textStatus->splitByErrorType();
521  if ( $error->getErrors() ) {
522  $out->addHTML( Html::errorBox(
523  $error->getHTML( 'search-error' )
524  ) );
525  }
526  if ( $warning->getErrors() ) {
527  $out->addHTML( Html::warningBox(
528  $warning->getHTML( 'search-warning' )
529  ) );
530  }
531  }
532 
533  // If we have no results and have not already displayed an error message
534  if ( $num === 0 && !$hasSearchErrors ) {
535  $wikiId = WikiMap::getCurrentWikiId();
536  $localizedWikiName = $this->msg( "project-localized-name-{$wikiId}" )->text();
537  $out->wrapWikiMsg( "<p class=\"mw-search-nonefound\">\n$1</p>", [
538  $hasInlineIwResults ? 'search-nonefound-thiswiki' : 'search-nonefound',
539  wfEscapeWikiText( $term ),
540  $term,
541  $localizedWikiName
542  ] );
543  }
544 
545  // Show the create link ahead
546  $this->showCreateLink( $title, $num, $titleMatches, $textMatches );
547 
548  $this->getHookRunner()->onSpecialSearchResults( $term, $titleMatches, $textMatches );
549 
550  // Close <div class='mw-search-results-info'>
551  $out->addHtml( '</div>' );
552 
553  // Although $num might be 0 there can still be secondary or inline
554  // results to display.
555  $linkRenderer = $this->getLinkRenderer();
556  $mainResultWidget = new FullSearchResultWidget(
557  $this,
558  $linkRenderer,
559  $this->getHookContainer(),
560  $this->repoGroup,
561  $this->thumbnailProvider,
562  $this->userOptionsManager
563  );
564 
565  // Default (null) on. Can be explicitly disabled.
566  if ( $engine->getFeatureData( 'enable-new-crossproject-page' ) !== false ) {
567  $sidebarResultWidget = new InterwikiSearchResultWidget( $this, $linkRenderer );
568  $sidebarResultsWidget = new InterwikiSearchResultSetWidget(
569  $this,
570  $sidebarResultWidget,
571  $linkRenderer,
572  $this->interwikiLookup,
573  $engine->getFeatureData( 'show-multimedia-search-results' )
574  );
575  } else {
576  $sidebarResultWidget = new SimpleSearchResultWidget( $this, $linkRenderer );
577  $sidebarResultsWidget = new SimpleSearchResultSetWidget(
578  $this,
579  $sidebarResultWidget,
580  $linkRenderer,
581  $this->interwikiLookup
582  );
583  }
584 
585  $widget = new BasicSearchResultSetWidget( $this, $mainResultWidget, $sidebarResultsWidget );
586 
587  $out->addHTML( $widget->render(
588  $term, $this->offset, $titleMatches, $textMatches
589  ) );
590 
591  $out->addHTML( '<div class="mw-search-visualclear"></div>' );
592  $this->prevNextLinks( $totalRes, $textMatches, $term, $out );
593 
594  // Close <div class='searchresults'>
595  $out->addHTML( "</div>" );
596 
597  $this->getHookRunner()->onSpecialSearchResultsAppend( $this, $out, $term );
598  }
599 
606  protected function showCreateLink( $title, $num, $titleMatches, $textMatches ) {
607  // show direct page/create link if applicable
608 
609  // Check DBkey !== '' in case of fragment link only.
610  if ( $title === null || $title->getDBkey() === ''
611  || ( $titleMatches !== null && $titleMatches->searchContainedSyntax() )
612  || ( $textMatches !== null && $textMatches->searchContainedSyntax() )
613  ) {
614  // invalid title
615  // preserve the paragraph for margins etc...
616  $this->getOutput()->addHTML( '<p></p>' );
617 
618  return;
619  }
620 
621  $messageName = 'searchmenu-new-nocreate';
622  $linkClass = 'mw-search-createlink';
623 
624  if ( !$title->isExternal() ) {
625  if ( $title->isKnown() ) {
626  $messageName = 'searchmenu-exists';
627  $linkClass = 'mw-search-exists';
628  } elseif (
629  $this->contentHandlerFactory->getContentHandler( $title->getContentModel() )
630  ->supportsDirectEditing()
631  && $this->getAuthority()->probablyCan( 'edit', $title )
632  ) {
633  $messageName = 'searchmenu-new';
634  }
635  }
636 
637  $params = [
638  $messageName,
639  wfEscapeWikiText( $title->getPrefixedText() ),
640  Message::numParam( $num )
641  ];
642  $this->getHookRunner()->onSpecialSearchCreateLink( $title, $params );
643 
644  // Extensions using the hook might still return an empty $messageName
645  // @phan-suppress-next-line PhanRedundantCondition Set by hook
646  if ( $messageName ) {
647  $this->getOutput()->wrapWikiMsg( "<p class=\"$linkClass\">\n$1</p>", $params );
648  } else {
649  // preserve the paragraph for margins etc...
650  $this->getOutput()->addHTML( '<p></p>' );
651  }
652  }
653 
660  protected function setupPage( $term ) {
661  $out = $this->getOutput();
662 
663  $this->setHeaders();
664  $this->outputHeader();
665  // TODO: Is this true? The namespace remember uses a user token
666  // on save.
667  $out->setPreventClickjacking( false );
668  $this->addHelpLink( 'Help:Searching' );
669 
670  if ( strval( $term ) !== '' ) {
671  $out->setPageTitle( $this->msg( 'searchresults' ) );
672  $out->setHTMLTitle( $this->msg( 'pagetitle' )
673  ->plaintextParams( $this->msg( 'searchresults-title' )->plaintextParams( $term )->text() )
674  ->inContentLanguage()->text()
675  );
676  }
677 
678  if ( $this->mPrefix !== '' ) {
679  $subtitle = $this->msg( 'search-filter-title-prefix' )->plaintextParams( $this->mPrefix );
680  $params = $this->powerSearchOptions();
681  unset( $params['prefix'] );
682  $params += [
683  'search' => $term,
684  'fulltext' => 1,
685  ];
686 
687  $subtitle .= ' (';
688  $subtitle .= Xml::element(
689  'a',
690  [
691  'href' => $this->getPageTitle()->getLocalURL( $params ),
692  'title' => $this->msg( 'search-filter-title-prefix-reset' )->text(),
693  ],
694  $this->msg( 'search-filter-title-prefix-reset' )->text()
695  );
696  $subtitle .= ')';
697  $out->setSubtitle( $subtitle );
698  }
699 
700  $out->addJsConfigVars( [ 'searchTerm' => $term ] );
701  $out->addModules( 'mediawiki.special.search' );
702  $out->addModuleStyles( [
703  'mediawiki.special', 'mediawiki.special.search.styles', 'mediawiki.ui', 'mediawiki.ui.button',
704  'mediawiki.ui.input', 'mediawiki.widgets.SearchInputWidget.styles',
705  ] );
706  }
707 
713  protected function isPowerSearch() {
714  return $this->profile === 'advanced';
715  }
716 
724  protected function powerSearch( &$request ) {
725  $arr = [];
726  foreach ( $this->searchConfig->searchableNamespaces() as $ns => $name ) {
727  if ( $request->getCheck( 'ns' . $ns ) ) {
728  $arr[] = $ns;
729  }
730  }
731 
732  return $arr;
733  }
734 
742  public function powerSearchOptions() {
743  $opt = [];
744  if ( $this->isPowerSearch() ) {
745  foreach ( $this->namespaces as $n ) {
746  $opt['ns' . $n] = 1;
747  }
748  } else {
749  $opt['profile'] = $this->profile;
750  }
751 
752  return $opt + $this->extraParams;
753  }
754 
760  protected function saveNamespaces() {
761  $user = $this->getUser();
762  $request = $this->getRequest();
763 
764  if ( $user->isRegistered() &&
765  $user->matchEditToken(
766  $request->getVal( 'nsRemember' ),
767  'searchnamespace',
768  $request
769  ) && !$this->readOnlyMode->isReadOnly()
770  ) {
771  // Reset namespace preferences: namespaces are not searched
772  // when they're not mentioned in the URL parameters.
773  foreach ( $this->nsInfo->getValidNamespaces() as $n ) {
774  $this->userOptionsManager->setOption( $user, 'searchNs' . $n, false );
775  }
776  // The request parameters include all the namespaces to be searched.
777  // Even if they're the same as an existing profile, they're not eaten.
778  foreach ( $this->namespaces as $n ) {
779  $this->userOptionsManager->setOption( $user, 'searchNs' . $n, true );
780  }
781 
782  DeferredUpdates::addCallableUpdate( static function () use ( $user ) {
783  $user->saveSettings();
784  } );
785 
786  return true;
787  }
788 
789  return false;
790  }
791 
796  protected function getSearchProfiles() {
797  // Builds list of Search Types (profiles)
798  $nsAllSet = array_keys( $this->searchConfig->searchableNamespaces() );
799  $defaultNs = $this->searchConfig->defaultNamespaces();
800  $profiles = [
801  'default' => [
802  'message' => 'searchprofile-articles',
803  'tooltip' => 'searchprofile-articles-tooltip',
804  'namespaces' => $defaultNs,
805  'namespace-messages' => $this->searchConfig->namespacesAsText(
806  $defaultNs
807  ),
808  ],
809  'images' => [
810  'message' => 'searchprofile-images',
811  'tooltip' => 'searchprofile-images-tooltip',
812  'namespaces' => [ NS_FILE ],
813  ],
814  'all' => [
815  'message' => 'searchprofile-everything',
816  'tooltip' => 'searchprofile-everything-tooltip',
817  'namespaces' => $nsAllSet,
818  ],
819  'advanced' => [
820  'message' => 'searchprofile-advanced',
821  'tooltip' => 'searchprofile-advanced-tooltip',
822  'namespaces' => self::NAMESPACES_CURRENT,
823  ]
824  ];
825 
826  $this->getHookRunner()->onSpecialSearchProfiles( $profiles );
827 
828  foreach ( $profiles as &$data ) {
829  if ( !is_array( $data['namespaces'] ) ) {
830  continue;
831  }
832  sort( $data['namespaces'] );
833  }
834 
835  return $profiles;
836  }
837 
843  public function getSearchEngine() {
844  if ( $this->searchEngine === null ) {
845  $this->searchEngine = $this->searchEngineFactory->create( $this->searchEngineType );
846  }
847 
848  return $this->searchEngine;
849  }
850 
855  public function getProfile() {
856  return $this->profile;
857  }
858 
863  public function getNamespaces() {
864  return $this->namespaces;
865  }
866 
876  public function setExtraParam( $key, $value ) {
877  $this->extraParams[$key] = $value;
878  }
879 
888  public function getPrefix() {
889  return $this->mPrefix;
890  }
891 
898  private function prevNextLinks( ?int $totalRes, ?ISearchResultSet $textMatches, string $term, OutputPage $out ) {
899  if ( $totalRes > $this->limit || $this->offset ) {
900  // Allow matches to define the correct offset, as interleaved
901  // AB testing may require a different next page offset.
902  if ( $textMatches && $textMatches->getOffset() !== null ) {
903  $offset = $textMatches->getOffset();
904  } else {
906  }
907 
908  // use the rewritten search term for subsequent page searches
909  $newSearchTerm = $term;
910  if ( $textMatches && $textMatches->hasRewrittenQuery() ) {
911  $newSearchTerm = $textMatches->getQueryAfterRewrite();
912  }
913 
914  $prevNext =
915  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable offset is not null
916  $this->buildPrevNextNavigation( $offset, $this->limit,
917  $this->powerSearchOptions() + [ 'search' => $newSearchTerm ],
918  $this->limit + $this->offset >= $totalRes );
919  $out->addHTML( "<div class='mw-search-pager-bottom'>{$prevNext}</div>\n" );
920  }
921  }
922 
923  protected function getGroupName() {
924  return 'pages';
925  }
926 }
const NS_FILE
Definition: Defines.php:70
const NS_MAIN
Definition: Defines.php:64
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add an update to the pending update queue that invokes the specified callback when run.
static warningBox( $html, $className='')
Return a warning box.
Definition: Html.php:775
static errorBox( $html, $heading='', $className='')
Return an error box.
Definition: Html.php:788
An interface for creating language converters.
A class containing constants representing the names of configuration variables.
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.
Renders one or more ISearchResultSets into a sidebar grouped by interwiki prefix.
A service class to control user options.
static numParam( $num)
Definition: Message.php:1146
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
This is one of the Core classes and should be read at least once by any new developers.
Definition: OutputPage.php:56
setPageTitle( $name)
"Page title" means the contents of <h1>.
redirect( $url, $responsecode='302')
Redirect to $url rather than displaying the normal page.
Definition: OutputPage.php:425
wrapWikiMsg( $wrap,... $msgSpecs)
This function takes a number of message/argument specifications, wraps them in some overall structure...
addModuleStyles( $modules)
Load the styles of one or more style-only ResourceLoader modules on this page.
Definition: OutputPage.php:648
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...
setPreventClickjacking(bool $enable)
Set the prevent-clickjacking flag.
addHTML( $text)
Append $text to the body HTML.
addJsConfigVars( $keys, $value=null)
Add one or more variables to be set in mw.config in JavaScript.
addModules( $modules)
Load one or more ResourceLoader modules on this page.
Definition: OutputPage.php:622
setSubtitle( $str)
Replace the subtitle with $str.
A service class for fetching the wiki's current read-only mode.
Prioritized list of file repositories.
Definition: RepoGroup.php:29
Configuration handling class for SearchEngine.
Factory class for SearchEngine.
const DEFAULT_SORT
Parent class for all special pages.
Definition: SpecialPage.php:44
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getOutput()
Get the OutputPage being used for this instance.
getUser()
Shortcut to get the User executing this instance.
buildPrevNextNavigation( $offset, $limit, array $query=[], $atend=false, $subpage=false)
Generate (prev x| next x) (20|50|100...) type links for paging.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
getPageTitle( $subpage=false)
Get a self-referential title object.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
getContentLanguage()
Shortcut to get content language.
implements Special:Search - Run text & title search and display the output
string null $searchEngineType
Search engine type, if not default.
goResult( $term)
If an exact title match can be found, jump straight ahead to it.
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...
string $mPrefix
The prefix url parameter.
null string $profile
Current search profile.
load()
Set up basic search parameters from the request and user settings.
SearchEngineConfig $searchConfig
Search engine configurations.
getPrefix()
The prefix value send to Special:Search using the 'prefix' URI param It means that the user is willin...
saveNamespaces()
Save namespace preferences when we're supposed to.
getProfile()
Current search profile.
SearchEngine $searchEngine
Search engine.
__construct(SearchEngineConfig $searchConfig, SearchEngineFactory $searchEngineFactory, NamespaceInfo $nsInfo, IContentHandlerFactory $contentHandlerFactory, InterwikiLookup $interwikiLookup, ReadOnlyMode $readOnlyMode, UserOptionsManager $userOptionsManager, LanguageConverterFactory $languageConverterFactory, RepoGroup $repoGroup, SearchResultThumbnailProvider $thumbnailProvider)
isPowerSearch()
Return true if current search is a power (advanced) search.
getNamespaces()
Current namespaces.
powerSearchOptions()
Reconstruct the 'power search' options for links TODO: Instead of exposing this publicly,...
showResults( $term)
powerSearch(&$request)
Extract "power search" namespace settings from the request object, returning a list of index numbers ...
array $extraParams
For links.
execute( $par)
Entry point.
setupPage( $term)
Sets up everything for the HTML output page including styles, javascript, page title,...
showCreateLink( $title, $num, $titleMatches, $textMatches)
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:45
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:373
static getCurrentWikiId()
Definition: WikiMap.php:303
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:43
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.
const INLINE_RESULTS
Identifier for interwiki results that can be displayed even if no existing main wiki results exist.
const SECONDARY_RESULTS
Identifier for interwiki results that are displayed only together with existing main wiki results.
hasRewrittenQuery()
Some search modes will run an alternative query that it thinks gives a better result than the provide...
getTotalHits()
Some search modes return a total hit count for the query in the entire article database.
Service interface for looking up Interwiki records.