MediaWiki  master
SpecialSearch.php
Go to the documentation of this file.
1 <?php
41 
46 class SpecialSearch extends SpecialPage {
55  protected $profile;
56 
58  protected $searchEngine;
59 
61  protected $searchEngineType = null;
62 
64  protected $extraParams = [];
65 
70  protected $mPrefix;
71 
75  protected $limit, $offset;
76 
80  protected $namespaces;
81 
85  protected $fulltext;
86 
91 
95  protected $runSuggestion = true;
96 
101  protected $searchConfig;
102 
104  private $searchEngineFactory;
105 
107  private $nsInfo;
108 
110  private $contentHandlerFactory;
111 
113  private $interwikiLookup;
114 
116  private $readOnlyMode;
117 
119  private $userOptionsManager;
120 
122  private $languageConverterFactory;
123 
125  private $repoGroup;
126 
128  private $thumbnailProvider;
129 
130  private TitleMatcher $titleMatcher;
131 
136  private $loadStatus;
137 
138  private const NAMESPACES_CURRENT = 'sense';
139 
153  public function __construct(
155  SearchEngineFactory $searchEngineFactory,
156  NamespaceInfo $nsInfo,
157  IContentHandlerFactory $contentHandlerFactory,
158  InterwikiLookup $interwikiLookup,
159  ReadOnlyMode $readOnlyMode,
160  UserOptionsManager $userOptionsManager,
161  LanguageConverterFactory $languageConverterFactory,
162  RepoGroup $repoGroup,
163  SearchResultThumbnailProvider $thumbnailProvider,
164  TitleMatcher $titleMatcher
165  ) {
166  parent::__construct( 'Search' );
167  $this->searchConfig = $searchConfig;
168  $this->searchEngineFactory = $searchEngineFactory;
169  $this->nsInfo = $nsInfo;
170  $this->contentHandlerFactory = $contentHandlerFactory;
171  $this->interwikiLookup = $interwikiLookup;
172  $this->readOnlyMode = $readOnlyMode;
173  $this->userOptionsManager = $userOptionsManager;
174  $this->languageConverterFactory = $languageConverterFactory;
175  $this->repoGroup = $repoGroup;
176  $this->thumbnailProvider = $thumbnailProvider;
177  $this->titleMatcher = $titleMatcher;
178  }
179 
185  public function execute( $par ) {
186  $request = $this->getRequest();
187  $out = $this->getOutput();
188 
189  // Fetch the search term
190  $term = str_replace( "\n", " ", $request->getText( 'search' ) );
191 
192  // Historically search terms have been accepted not only in the search query
193  // parameter, but also as part of the primary url. This can have PII implications
194  // in releasing page view data. As such issue a 301 redirect to the correct
195  // URL.
196  if ( $par !== null && $par !== '' && $term === '' ) {
197  $query = $request->getValues();
198  unset( $query['title'] );
199  // Strip underscores from title parameter; most of the time we'll want
200  // text form here. But don't strip underscores from actual text params!
201  $query['search'] = str_replace( '_', ' ', $par );
202  $out->redirect( $this->getPageTitle()->getFullURL( $query ), 301 );
203  return;
204  }
205 
206  // Need to load selected namespaces before handling nsRemember
207  $this->load();
208  // TODO: This performs database actions on GET request, which is going to
209  // be a problem for our multi-datacenter work.
210  if ( $request->getCheck( 'nsRemember' ) ) {
211  $this->saveNamespaces();
212  // Remove the token from the URL to prevent the user from inadvertently
213  // exposing it (e.g. by pasting it into a public wiki page) or undoing
214  // later settings changes (e.g. by reloading the page).
215  $query = $request->getValues();
216  unset( $query['title'], $query['nsRemember'] );
217  $out->redirect( $this->getPageTitle()->getFullURL( $query ) );
218  return;
219  }
220 
221  if ( !$request->getVal( 'fulltext' ) && !$request->getCheck( 'offset' ) ) {
222  $url = $this->goResult( $term );
223  if ( $url !== null ) {
224  // successful 'go'
225  $out->redirect( $url );
226  return;
227  }
228  // No match. If it could plausibly be a title
229  // run the No go match hook.
230  $title = Title::newFromText( $term );
231  if ( $title !== null ) {
232  $this->getHookRunner()->onSpecialSearchNogomatch( $title );
233  }
234  }
235 
236  $this->setupPage( $term );
237 
238  if ( $this->getConfig()->get( MainConfigNames::DisableTextSearch ) ) {
239  $searchForwardUrl = $this->getConfig()->get( MainConfigNames::SearchForwardUrl );
240  if ( $searchForwardUrl ) {
241  $url = str_replace( '$1', urlencode( $term ), $searchForwardUrl );
242  $out->redirect( $url );
243  } else {
244  $out->addHTML( $this->showGoogleSearch( $term ) );
245  }
246 
247  return;
248  }
249 
250  $this->showResults( $term );
251  }
252 
261  private function showGoogleSearch( $term ) {
262  return "<fieldset>" .
263  "<legend>" .
264  $this->msg( 'search-external' )->escaped() .
265  "</legend>" .
266  "<p class='mw-searchdisabled'>" .
267  $this->msg( 'searchdisabled' )->escaped() .
268  "</p>" .
269  // googlesearch is part of $wgRawHtmlMessages and safe to use as is here
270  $this->msg( 'googlesearch' )->rawParams(
271  htmlspecialchars( $term ),
272  'UTF-8',
273  $this->msg( 'searchbutton' )->escaped()
274  )->text() .
275  "</fieldset>";
276  }
277 
283  public function load() {
284  $this->loadStatus = new Status();
285 
286  $request = $this->getRequest();
287  $this->searchEngineType = $request->getVal( 'srbackend' );
288 
289  [ $this->limit, $this->offset ] = $request->getLimitOffsetForUser(
290  $this->getUser(),
291  20,
292  'searchlimit'
293  );
294  $this->mPrefix = $request->getVal( 'prefix', '' );
295  if ( $this->mPrefix !== '' ) {
296  $this->setExtraParam( 'prefix', $this->mPrefix );
297  }
298 
299  $sort = $request->getVal( 'sort', SearchEngine::DEFAULT_SORT );
300  $validSorts = $this->getSearchEngine()->getValidSorts();
301  if ( !in_array( $sort, $validSorts ) ) {
302  $this->loadStatus->warning( 'search-invalid-sort-order', $sort,
303  implode( ', ', $validSorts ) );
304  } elseif ( $sort !== $this->sort ) {
305  $this->sort = $sort;
306  $this->setExtraParam( 'sort', $this->sort );
307  }
308 
309  $user = $this->getUser();
310 
311  # Extract manually requested namespaces
312  $nslist = $this->powerSearch( $request );
313  if ( $nslist === [] ) {
314  # Fallback to user preference
315  $nslist = $this->searchConfig->userNamespaces( $user );
316  }
317 
318  $profile = null;
319  if ( $nslist === [] ) {
320  $profile = 'default';
321  }
322 
323  $profile = $request->getVal( 'profile', $profile );
324  $profiles = $this->getSearchProfiles();
325  if ( $profile === null ) {
326  // BC with old request format
327  $profile = 'advanced';
328  foreach ( $profiles as $key => $data ) {
329  if ( $nslist === $data['namespaces'] && $key !== 'advanced' ) {
330  $profile = $key;
331  }
332  }
333  $this->namespaces = $nslist;
334  } elseif ( $profile === 'advanced' ) {
335  $this->namespaces = $nslist;
336  } elseif ( isset( $profiles[$profile]['namespaces'] ) ) {
337  $this->namespaces = $profiles[$profile]['namespaces'];
338  } else {
339  // Unknown profile requested
340  $this->loadStatus->warning( 'search-unknown-profile', $profile );
341  $profile = 'default';
342  $this->namespaces = $profiles['default']['namespaces'];
343  }
344 
345  $this->fulltext = $request->getVal( 'fulltext' );
346  $this->runSuggestion = (bool)$request->getVal( 'runsuggestion', '1' );
347  $this->profile = $profile;
348  }
349 
356  public function goResult( $term ) {
357  # If the string cannot be used to create a title
358  if ( Title::newFromText( $term ) === null ) {
359  return null;
360  }
361  # If there's an exact or very near match, jump right there.
362  $title = $this->titleMatcher->getNearMatch( $term );
363  if ( $title === null ) {
364  return null;
365  }
366  $url = null;
367  if ( !$this->getHookRunner()->onSpecialSearchGoResult( $term, $title, $url ) ) {
368  return null;
369  }
370 
371  if (
372  // If there is a preference set to NOT redirect on exact page match
373  // then return null (which prevents direction)
374  !$this->redirectOnExactMatch()
375  // BUT ...
376  // ... ignore no-redirect preference if the exact page match is an interwiki link
377  && !$title->isExternal()
378  // ... ignore no-redirect preference if the exact page match is NOT in the main
379  // namespace AND there's a namespace in the search string
380  && !( $title->getNamespace() !== NS_MAIN && strpos( $term, ':' ) > 0 )
381  ) {
382  return null;
383  }
384 
385  return $url ?? $title->getFullUrlForRedirect();
386  }
387 
388  private function redirectOnExactMatch() {
389  if ( !$this->getConfig()->get( MainConfigNames::SearchMatchRedirectPreference ) ) {
390  // If the preference for whether to redirect is disabled, use the default setting
391  $defaultOptions = $this->userOptionsManager->getDefaultOptions();
392  return $defaultOptions['search-match-redirect'];
393  } else {
394  // Otherwise use the user's preference
395  return $this->userOptionsManager->getOption( $this->getUser(), 'search-match-redirect' );
396  }
397  }
398 
402  public function showResults( $term ) {
403  if ( $this->searchEngineType !== null ) {
404  $this->setExtraParam( 'srbackend', $this->searchEngineType );
405  }
406 
407  $out = $this->getOutput();
408  $widgetOptions = $this->getConfig()->get( MainConfigNames::SpecialSearchFormOptions );
410  $this,
411  $this->searchConfig,
412  $this->getHookContainer(),
413  $this->languageConverterFactory->getLanguageConverter( $this->getLanguage() ),
414  $this->nsInfo,
415  $this->getSearchProfiles()
416  );
417  $filePrefix = $this->getContentLanguage()->getFormattedNsText( NS_FILE ) . ':';
418  if ( trim( $term ) === '' || $filePrefix === trim( $term ) ) {
419  // Empty query -- straight view of search form
420  if ( !$this->getHookRunner()->onSpecialSearchResultsPrepend( $this, $out, $term ) ) {
421  # Hook requested termination
422  return;
423  }
424  $out->enableOOUI();
425  // The form also contains the 'Showing results 0 - 20 of 1234' so we can
426  // only do the form render here for the empty $term case. Rendering
427  // the form when a search is provided is repeated below.
428  $out->addHTML( $formWidget->render(
429  $this->profile, $term, 0, 0, $this->offset, $this->isPowerSearch(), $widgetOptions
430  ) );
431  return;
432  }
433 
434  $engine = $this->getSearchEngine();
435  $engine->setFeatureData( 'rewrite', $this->runSuggestion );
436  $engine->setLimitOffset( $this->limit, $this->offset );
437  $engine->setNamespaces( $this->namespaces );
438  $engine->setSort( $this->sort );
439  $engine->prefix = $this->mPrefix;
440 
441  $this->getHookRunner()->onSpecialSearchSetupEngine( $this, $this->profile, $engine );
442  if ( !$this->getHookRunner()->onSpecialSearchResultsPrepend( $this, $out, $term ) ) {
443  # Hook requested termination
444  return;
445  }
446 
447  $title = Title::newFromText( $term );
448  $languageConverter = $this->languageConverterFactory->getLanguageConverter( $this->getContentLanguage() );
449  if ( $languageConverter->hasVariants() ) {
450  // findVariantLink will replace the link arg as well but we want to keep our original
451  // search string, use a copy in the $variantTerm var so that $term remains intact.
452  $variantTerm = $term;
453  $languageConverter->findVariantLink( $variantTerm, $title );
454  }
455 
456  $showSuggestion = $title === null || !$title->isKnown();
457  $engine->setShowSuggestion( $showSuggestion );
458 
459  $rewritten = $engine->replacePrefixes( $term );
460  if ( $rewritten !== $term ) {
461  wfDeprecatedMsg( 'SearchEngine::replacePrefixes() was overridden by ' .
462  get_class( $engine ) . ', this is deprecated since MediaWiki 1.32',
463  '1.32', false, false );
464  }
465 
466  // fetch search results
467  $titleMatches = $engine->searchTitle( $rewritten );
468  $textMatches = $engine->searchText( $rewritten );
469 
470  $textStatus = null;
471  if ( $textMatches instanceof Status ) {
472  $textStatus = $textMatches;
473  $textMatches = $textStatus->getValue();
474  }
475 
476  // Get number of results
477  $titleMatchesNum = $textMatchesNum = $numTitleMatches = $numTextMatches = 0;
478  if ( $titleMatches ) {
479  $titleMatchesNum = $titleMatches->numRows();
480  $numTitleMatches = $titleMatches->getTotalHits();
481  }
482  if ( $textMatches ) {
483  $textMatchesNum = $textMatches->numRows();
484  $numTextMatches = $textMatches->getTotalHits();
485  if ( $textMatchesNum > 0 ) {
486  $engine->augmentSearchResults( $textMatches );
487  }
488  }
489  $num = $titleMatchesNum + $textMatchesNum;
490  $totalRes = $numTitleMatches + $numTextMatches;
491 
492  // start rendering the page
493  $out->enableOOUI();
494  $out->addHTML( $formWidget->render(
495  $this->profile, $term, $num, $totalRes, $this->offset, $this->isPowerSearch(), $widgetOptions
496  ) );
497 
498  // did you mean... suggestions
499  if ( $textMatches ) {
500  $dymWidget = new MediaWiki\Search\SearchWidgets\DidYouMeanWidget( $this );
501  $out->addHTML( $dymWidget->render( $term, $textMatches ) );
502  }
503 
504  $hasSearchErrors = $textStatus && $textStatus->getErrors() !== [];
505  $hasInlineIwResults = $textMatches &&
507  $hasSecondaryIwResults = $textMatches &&
509 
510  $classNames = [ 'searchresults' ];
511  if ( $hasSecondaryIwResults ) {
512  $classNames[] = 'mw-searchresults-has-iw';
513  }
514  if ( $this->offset > 0 ) {
515  $classNames[] = 'mw-searchresults-has-offset';
516  }
517  $out->addHTML( '<div class="' . implode( ' ', $classNames ) . '">' );
518 
519  $out->addHTML( '<div class="mw-search-results-info">' );
520 
521  if ( $hasSearchErrors || $this->loadStatus->getErrors() ) {
522  if ( $textStatus === null ) {
523  $textStatus = $this->loadStatus;
524  } else {
525  $textStatus->merge( $this->loadStatus );
526  }
527  [ $error, $warning ] = $textStatus->splitByErrorType();
528  if ( $error->getErrors() ) {
529  $out->addHTML( Html::errorBox(
530  $error->getHTML( 'search-error' )
531  ) );
532  }
533  if ( $warning->getErrors() ) {
534  $out->addHTML( Html::warningBox(
535  $warning->getHTML( 'search-warning' )
536  ) );
537  }
538  }
539 
540  // If we have no results and have not already displayed an error message
541  if ( $num === 0 && !$hasSearchErrors ) {
542  $out->wrapWikiMsg( "<p class=\"mw-search-nonefound\">\n$1</p>", [
543  $hasInlineIwResults ? 'search-nonefound-thiswiki' : 'search-nonefound',
544  wfEscapeWikiText( $term ),
545  $term
546  ] );
547  }
548 
549  // Show the create link ahead
550  $this->showCreateLink( $title, $num, $titleMatches, $textMatches );
551 
552  $this->getHookRunner()->onSpecialSearchResults( $term, $titleMatches, $textMatches );
553 
554  // Close <div class='mw-search-results-info'>
555  $out->addHTML( '</div>' );
556 
557  // Although $num might be 0 there can still be secondary or inline
558  // results to display.
559  $linkRenderer = $this->getLinkRenderer();
560  $mainResultWidget = new FullSearchResultWidget(
561  $this,
562  $linkRenderer,
563  $this->getHookContainer(),
564  $this->repoGroup,
565  $this->thumbnailProvider,
566  $this->userOptionsManager
567  );
568 
569  // Default (null) on. Can be explicitly disabled.
570  if ( $engine->getFeatureData( 'enable-new-crossproject-page' ) !== false ) {
571  $sidebarResultWidget = new InterwikiSearchResultWidget( $this, $linkRenderer );
572  $sidebarResultsWidget = new InterwikiSearchResultSetWidget(
573  $this,
574  $sidebarResultWidget,
575  $linkRenderer,
576  $this->interwikiLookup,
577  $engine->getFeatureData( 'show-multimedia-search-results' )
578  );
579  } else {
580  $sidebarResultWidget = new SimpleSearchResultWidget( $this, $linkRenderer );
581  $sidebarResultsWidget = new SimpleSearchResultSetWidget(
582  $this,
583  $sidebarResultWidget,
584  $linkRenderer,
585  $this->interwikiLookup
586  );
587  }
588 
589  $widget = new BasicSearchResultSetWidget( $this, $mainResultWidget, $sidebarResultsWidget );
590 
591  $out->addHTML( $widget->render(
592  $term, $this->offset, $titleMatches, $textMatches
593  ) );
594 
595  $out->addHTML( '<div class="mw-search-visualclear"></div>' );
596  $this->prevNextLinks( $totalRes, $textMatches, $term, $out );
597 
598  // Close <div class='searchresults'>
599  $out->addHTML( "</div>" );
600 
601  $this->getHookRunner()->onSpecialSearchResultsAppend( $this, $out, $term );
602  }
603 
610  protected function showCreateLink( $title, $num, $titleMatches, $textMatches ) {
611  // show direct page/create link if applicable
612 
613  // Check DBkey !== '' in case of fragment link only.
614  if ( $title === null || $title->getDBkey() === ''
615  || ( $titleMatches !== null && $titleMatches->searchContainedSyntax() )
616  || ( $textMatches !== null && $textMatches->searchContainedSyntax() )
617  ) {
618  // invalid title
619  // preserve the paragraph for margins etc...
620  $this->getOutput()->addHTML( '<p></p>' );
621 
622  return;
623  }
624 
625  $messageName = 'searchmenu-new-nocreate';
626  $linkClass = 'mw-search-createlink';
627 
628  if ( !$title->isExternal() ) {
629  if ( $title->isKnown() ) {
630  $messageName = 'searchmenu-exists';
631  $linkClass = 'mw-search-exists';
632  } elseif (
633  $this->contentHandlerFactory->getContentHandler( $title->getContentModel() )
634  ->supportsDirectEditing()
635  && $this->getAuthority()->probablyCan( 'edit', $title )
636  ) {
637  $messageName = 'searchmenu-new';
638  }
639  }
640 
641  $params = [
642  $messageName,
643  wfEscapeWikiText( $title->getPrefixedText() ),
644  Message::numParam( $num )
645  ];
646  $this->getHookRunner()->onSpecialSearchCreateLink( $title, $params );
647 
648  // Extensions using the hook might still return an empty $messageName
649  // @phan-suppress-next-line PhanRedundantCondition Set by hook
650  if ( $messageName ) {
651  $this->getOutput()->wrapWikiMsg( "<p class=\"$linkClass\">\n$1</p>", $params );
652  } else {
653  // preserve the paragraph for margins etc...
654  $this->getOutput()->addHTML( '<p></p>' );
655  }
656  }
657 
664  protected function setupPage( $term ) {
665  $out = $this->getOutput();
666 
667  $this->setHeaders();
668  $this->outputHeader();
669  // TODO: Is this true? The namespace remember uses a user token
670  // on save.
671  $out->setPreventClickjacking( false );
672  $this->addHelpLink( 'Help:Searching' );
673 
674  if ( strval( $term ) !== '' ) {
675  $out->setPageTitle( $this->msg( 'searchresults' ) );
676  $out->setHTMLTitle( $this->msg( 'pagetitle' )
677  ->plaintextParams( $this->msg( 'searchresults-title' )->plaintextParams( $term )->text() )
678  ->inContentLanguage()->text()
679  );
680  }
681 
682  if ( $this->mPrefix !== '' ) {
683  $subtitle = $this->msg( 'search-filter-title-prefix' )->plaintextParams( $this->mPrefix );
684  $params = $this->powerSearchOptions();
685  unset( $params['prefix'] );
686  $params += [
687  'search' => $term,
688  'fulltext' => 1,
689  ];
690 
691  $subtitle .= ' (';
692  $subtitle .= Xml::element(
693  'a',
694  [
695  'href' => $this->getPageTitle()->getLocalURL( $params ),
696  'title' => $this->msg( 'search-filter-title-prefix-reset' )->text(),
697  ],
698  $this->msg( 'search-filter-title-prefix-reset' )->text()
699  );
700  $subtitle .= ')';
701  $out->setSubtitle( $subtitle );
702  }
703 
704  $out->addJsConfigVars( [ 'searchTerm' => $term ] );
705  $out->addModules( 'mediawiki.special.search' );
706  $out->addModuleStyles( [
707  'mediawiki.special', 'mediawiki.special.search.styles', 'mediawiki.ui', 'mediawiki.ui.button',
708  'mediawiki.ui.input', 'mediawiki.widgets.SearchInputWidget.styles',
709  ] );
710  }
711 
717  protected function isPowerSearch() {
718  return $this->profile === 'advanced';
719  }
720 
728  protected function powerSearch( &$request ) {
729  $arr = [];
730  foreach ( $this->searchConfig->searchableNamespaces() as $ns => $name ) {
731  if ( $request->getCheck( 'ns' . $ns ) ) {
732  $arr[] = $ns;
733  }
734  }
735 
736  return $arr;
737  }
738 
746  public function powerSearchOptions() {
747  $opt = [];
748  if ( $this->isPowerSearch() ) {
749  foreach ( $this->namespaces as $n ) {
750  $opt['ns' . $n] = 1;
751  }
752  } else {
753  $opt['profile'] = $this->profile;
754  }
755 
756  return $opt + $this->extraParams;
757  }
758 
764  protected function saveNamespaces() {
765  $user = $this->getUser();
766  $request = $this->getRequest();
767 
768  if ( $user->isRegistered() &&
769  $user->matchEditToken(
770  $request->getVal( 'nsRemember' ),
771  'searchnamespace',
772  $request
773  ) && !$this->readOnlyMode->isReadOnly()
774  ) {
775  // Reset namespace preferences: namespaces are not searched
776  // when they're not mentioned in the URL parameters.
777  foreach ( $this->nsInfo->getValidNamespaces() as $n ) {
778  $this->userOptionsManager->setOption( $user, 'searchNs' . $n, false );
779  }
780  // The request parameters include all the namespaces to be searched.
781  // Even if they're the same as an existing profile, they're not eaten.
782  foreach ( $this->namespaces as $n ) {
783  $this->userOptionsManager->setOption( $user, 'searchNs' . $n, true );
784  }
785 
786  DeferredUpdates::addCallableUpdate( static function () use ( $user ) {
787  $user->saveSettings();
788  } );
789 
790  return true;
791  }
792 
793  return false;
794  }
795 
800  protected function getSearchProfiles() {
801  // Builds list of Search Types (profiles)
802  $nsAllSet = array_keys( $this->searchConfig->searchableNamespaces() );
803  $defaultNs = $this->searchConfig->defaultNamespaces();
804  $profiles = [
805  'default' => [
806  'message' => 'searchprofile-articles',
807  'tooltip' => 'searchprofile-articles-tooltip',
808  'namespaces' => $defaultNs,
809  'namespace-messages' => $this->searchConfig->namespacesAsText(
810  $defaultNs
811  ),
812  ],
813  'images' => [
814  'message' => 'searchprofile-images',
815  'tooltip' => 'searchprofile-images-tooltip',
816  'namespaces' => [ NS_FILE ],
817  ],
818  'all' => [
819  'message' => 'searchprofile-everything',
820  'tooltip' => 'searchprofile-everything-tooltip',
821  'namespaces' => $nsAllSet,
822  ],
823  'advanced' => [
824  'message' => 'searchprofile-advanced',
825  'tooltip' => 'searchprofile-advanced-tooltip',
826  'namespaces' => self::NAMESPACES_CURRENT,
827  ]
828  ];
829 
830  $this->getHookRunner()->onSpecialSearchProfiles( $profiles );
831 
832  foreach ( $profiles as &$data ) {
833  if ( !is_array( $data['namespaces'] ) ) {
834  continue;
835  }
836  sort( $data['namespaces'] );
837  }
838 
839  return $profiles;
840  }
841 
847  public function getSearchEngine() {
848  if ( $this->searchEngine === null ) {
849  $this->searchEngine = $this->searchEngineFactory->create( $this->searchEngineType );
850  }
851 
852  return $this->searchEngine;
853  }
854 
859  public function getProfile() {
860  return $this->profile;
861  }
862 
867  public function getNamespaces() {
868  return $this->namespaces;
869  }
870 
880  public function setExtraParam( $key, $value ) {
881  $this->extraParams[$key] = $value;
882  }
883 
892  public function getPrefix() {
893  return $this->mPrefix;
894  }
895 
902  private function prevNextLinks( ?int $totalRes, ?ISearchResultSet $textMatches, string $term, OutputPage $out ) {
903  if ( $totalRes > $this->limit || $this->offset ) {
904  // Allow matches to define the correct offset, as interleaved
905  // AB testing may require a different next page offset.
906  if ( $textMatches && $textMatches->getOffset() !== null ) {
907  $offset = $textMatches->getOffset();
908  } else {
910  }
911 
912  // use the rewritten search term for subsequent page searches
913  $newSearchTerm = $term;
914  if ( $textMatches && $textMatches->hasRewrittenQuery() ) {
915  $newSearchTerm = $textMatches->getQueryAfterRewrite();
916  }
917 
918  $prevNext =
919  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable offset is not null
920  $this->buildPrevNextNavigation( $offset, $this->limit,
921  $this->powerSearchOptions() + [ 'search' => $newSearchTerm ],
922  $this->limit + $this->offset >= $totalRes );
923  $out->addHTML( "<div class='mw-search-pager-bottom'>{$prevNext}</div>\n" );
924  }
925  }
926 
927  protected function getGroupName() {
928  return 'pages';
929  }
930 }
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.
This class is a collection of static functions that serve two purposes:
Definition: Html.php:55
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.
Service implementation of near match title search.
Represents a title within MediaWiki.
Definition: Title.php:82
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:60
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:430
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:653
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:627
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:30
Configuration handling class for SearchEngine.
Factory class for SearchEngine.
const DEFAULT_SORT
Parent class for all special pages.
Definition: SpecialPage.php:45
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
__construct(SearchEngineConfig $searchConfig, SearchEngineFactory $searchEngineFactory, NamespaceInfo $nsInfo, IContentHandlerFactory $contentHandlerFactory, InterwikiLookup $interwikiLookup, ReadOnlyMode $readOnlyMode, UserOptionsManager $userOptionsManager, LanguageConverterFactory $languageConverterFactory, RepoGroup $repoGroup, SearchResultThumbnailProvider $thumbnailProvider, TitleMatcher $titleMatcher)
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.
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:46
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:44
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.