MediaWiki master
SpecialSearch.php
Go to the documentation of this file.
1<?php
9namespace MediaWiki\Specials;
10
39use StatusValue;
41
57 protected $profile;
58
60 protected $searchEngine;
61
63 protected $searchEngineType = null;
64
66 protected $extraParams = [];
67
72 protected $mPrefix;
73
74 protected int $limit;
75 protected int $offset;
76
80 protected $namespaces;
81
85 protected $fulltext;
86
90 protected $sort = SearchEngine::DEFAULT_SORT;
91
95 protected $runSuggestion = true;
96
101 private $loadStatus;
102
103 private const NAMESPACES_CURRENT = 'sense';
104
105 public function __construct(
106 protected readonly SearchEngineConfig $searchConfig,
107 private readonly SearchEngineFactory $searchEngineFactory,
108 private readonly NamespaceInfo $nsInfo,
109 private readonly IContentHandlerFactory $contentHandlerFactory,
110 private readonly InterwikiLookup $interwikiLookup,
111 private readonly ReadOnlyMode $readOnlyMode,
112 private readonly UserOptionsManager $userOptionsManager,
113 private readonly LanguageConverterFactory $languageConverterFactory,
114 private readonly RepoGroup $repoGroup,
115 private readonly SearchResultThumbnailProvider $thumbnailProvider,
116 private readonly TitleMatcher $titleMatcher,
117 ) {
118 parent::__construct( 'Search' );
119 }
120
126 public function execute( $par ) {
127 $request = $this->getRequest();
128 $out = $this->getOutput();
129
130 // Fetch the search term
131 $term = str_replace( "\n", " ", $request->getText( 'search' ) );
132
133 // Historically search terms have been accepted not only in the search query
134 // parameter, but also as part of the primary url. This can have PII implications
135 // in releasing page view data. As such issue a 301 redirect to the correct
136 // URL.
137 if ( $par !== null && $par !== '' && $term === '' ) {
138 $query = $request->getQueryValues();
139 unset( $query['title'] );
140 // Strip underscores from title parameter; most of the time we'll want
141 // text form here. But don't strip underscores from actual text params!
142 $query['search'] = str_replace( '_', ' ', $par );
143 $out->redirect( $this->getPageTitle()->getFullURL( $query ), 301 );
144 return;
145 }
146
147 // Need to load selected namespaces before handling nsRemember
148 $this->load();
149 // TODO: This performs database actions on GET request, which is going to
150 // be a problem for our multi-datacenter work.
151 if ( $request->getCheck( 'nsRemember' ) ) {
152 $this->saveNamespaces();
153 // Remove the token from the URL to prevent the user from inadvertently
154 // exposing it (e.g. by pasting it into a public wiki page) or undoing
155 // later settings changes (e.g. by reloading the page).
156 $query = $request->getQueryValues();
157 unset( $query['title'], $query['nsRemember'] );
158 $out->redirect( $this->getPageTitle()->getFullURL( $query ) );
159 return;
160 }
161
162 if ( !$request->getVal( 'fulltext' ) && !$request->getCheck( 'offset' ) ) {
163 $url = $this->goResult( $term );
164 if ( $url !== null ) {
165 // successful 'go'
166 $out->redirect( $url );
167 return;
168 }
169 // No match. If it could plausibly be a title
170 // run the No go match hook.
171 $title = Title::newFromText( $term );
172 if ( $title !== null ) {
173 $this->getHookRunner()->onSpecialSearchNogomatch( $title );
174 }
175 }
176
177 $this->setupPage( $term );
178
179 if ( $this->getConfig()->get( MainConfigNames::DisableTextSearch ) ) {
180 $searchForwardUrl = $this->getConfig()->get( MainConfigNames::SearchForwardUrl );
181 if ( $searchForwardUrl ) {
182 $url = str_replace( '$1', urlencode( $term ), $searchForwardUrl );
183 $out->redirect( $url );
184 } else {
185 $out->addHTML( Html::errorBox( Html::rawElement(
186 'p',
187 [ 'class' => 'mw-searchdisabled' ],
188 $this->msg( 'searchdisabled', [ 'mw:Special:MyLanguage/Manual:$wgSearchForwardUrl' ] )->parse()
189 ) ) );
190 $titleNs = count( $this->namespaces ) === 1 ? reset( $this->namespaces ) : null;
191 $title = Title::newFromText( $term, $titleNs );
192 $this->showCreateLink( $title, 0, null, null );
193 }
194
195 return;
196 }
197
198 $this->showResults( $term );
199 }
200
206 public function load() {
207 $this->loadStatus = new StatusValue();
208
209 $request = $this->getRequest();
210 $this->searchEngineType = $request->getVal( 'srbackend' );
211
212 [ $this->limit, $this->offset ] = $request->getLimitOffsetForUser(
213 $this->getUser(),
214 20,
215 'searchlimit'
216 );
217 $this->mPrefix = $request->getVal( 'prefix', '' );
218 if ( $this->mPrefix !== '' ) {
219 $this->setExtraParam( 'prefix', $this->mPrefix );
220 }
221
222 $sort = $request->getVal( 'sort', SearchEngine::DEFAULT_SORT );
223 $validSorts = $this->getSearchEngine()->getValidSorts();
224 if ( !in_array( $sort, $validSorts ) ) {
225 $this->loadStatus->warning( 'search-invalid-sort-order', $sort,
226 implode( ', ', $validSorts ) );
227 } elseif ( $sort !== $this->sort ) {
228 $this->sort = $sort;
229 $this->setExtraParam( 'sort', $this->sort );
230 }
231
232 $user = $this->getUser();
233
234 # Extract manually requested namespaces
235 $nslist = $this->powerSearch( $request );
236 if ( $nslist === [] ) {
237 # Fallback to user preference
238 $nslist = $this->searchConfig->userNamespaces( $user );
239 }
240
241 $profile = null;
242 if ( $nslist === [] ) {
243 $profile = 'default';
244 }
245
246 $profile = $request->getVal( 'profile', $profile );
247 $profiles = $this->getSearchProfiles();
248 if ( $profile === null ) {
249 // BC with old request format
250 $profile = 'advanced';
251 foreach ( $profiles as $key => $data ) {
252 if ( $nslist === $data['namespaces'] && $key !== 'advanced' ) {
253 $profile = $key;
254 }
255 }
256 $this->namespaces = $nslist;
257 } elseif ( $profile === 'advanced' ) {
258 $this->namespaces = $nslist;
259 } elseif ( isset( $profiles[$profile]['namespaces'] ) ) {
260 $this->namespaces = $profiles[$profile]['namespaces'];
261 } else {
262 // Unknown profile requested
263 $this->loadStatus->warning( 'search-unknown-profile', $profile );
264 $profile = 'default';
265 $this->namespaces = $profiles['default']['namespaces'];
266 }
267
268 $this->fulltext = $request->getVal( 'fulltext' );
269 $this->runSuggestion = (bool)$request->getVal( 'runsuggestion', '1' );
270 $this->profile = $profile;
271 }
272
279 public function goResult( $term ) {
280 # If the string cannot be used to create a title
281 if ( Title::newFromText( $term ) === null ) {
282 return null;
283 }
284 # If there's an exact or very near match, jump right there.
285 $title = $this->titleMatcher->getNearMatch( $term );
286 if ( $title === null ) {
287 return null;
288 }
289 $url = null;
290 if ( !$this->getHookRunner()->onSpecialSearchGoResult( $term, $title, $url ) ) {
291 return null;
292 }
293
294 if (
295 // If there is a preference set to NOT redirect on exact page match
296 // then return null (which prevents direction)
297 !$this->redirectOnExactMatch()
298 // BUT ...
299 // ... ignore no-redirect preference if the exact page match is an interwiki link
300 && !$title->isExternal()
301 // ... ignore no-redirect preference if the exact page match is NOT in the main
302 // namespace AND there's a namespace in the search string
303 && !( $title->getNamespace() !== NS_MAIN && strpos( $term, ':' ) > 0 )
304 ) {
305 return null;
306 }
307
308 return $url ?? $title->getFullUrlForRedirect();
309 }
310
311 private function redirectOnExactMatch(): bool {
312 if ( !$this->getConfig()->get( MainConfigNames::SearchMatchRedirectPreference ) ) {
313 // If the preference for whether to redirect is disabled, use the default setting
314 return (bool)$this->userOptionsManager->getDefaultOption(
315 'search-match-redirect',
316 $this->getUser()
317 );
318 } else {
319 // Otherwise use the user's preference
320 return $this->userOptionsManager->getBoolOption( $this->getUser(), 'search-match-redirect' );
321 }
322 }
323
327 public function showResults( $term ) {
328 if ( $this->searchEngineType !== null ) {
329 $this->setExtraParam( 'srbackend', $this->searchEngineType );
330 }
331
332 $out = $this->getOutput();
333 $widgetOptions = $this->getConfig()->get( MainConfigNames::SpecialSearchFormOptions );
334 $formWidget = new SearchFormWidget(
335 $this,
336 $this->searchConfig,
337 $this->getHookContainer(),
338 $this->languageConverterFactory->getLanguageConverter( $this->getLanguage() ),
339 $this->nsInfo,
340 $this->getSearchProfiles()
341 );
342 $filePrefix = $this->getContentLanguage()->getFormattedNsText( NS_FILE ) . ':';
343 if ( trim( $term ) === '' || $filePrefix === trim( $term ) ) {
344 // Empty query -- straight view of search form
345 if ( !$this->getHookRunner()->onSpecialSearchResultsPrepend( $this, $out, $term ) ) {
346 # Hook requested termination
347 return;
348 }
349 $out->enableOOUI();
350 // The form also contains the 'Showing results 0 - 20 of 1234' so we can
351 // only do the form render here for the empty $term case. Rendering
352 // the form when a search is provided is repeated below.
353 $out->addHTML( $formWidget->render(
354 $this->profile, $term, 0, 0, false, $this->offset, $this->isPowerSearch(), $widgetOptions
355 ) );
356 return;
357 }
358
359 $engine = $this->getSearchEngine();
360 $engine->setFeatureData( 'rewrite', $this->runSuggestion );
361 $engine->setLimitOffset( $this->limit, $this->offset );
362 $engine->setNamespaces( $this->namespaces );
363 $engine->setSort( $this->sort );
364 $engine->prefix = $this->mPrefix;
365
366 $this->getHookRunner()->onSpecialSearchSetupEngine( $this, $this->profile, $engine );
367 if ( !$this->getHookRunner()->onSpecialSearchResultsPrepend( $this, $out, $term ) ) {
368 # Hook requested termination
369 return;
370 }
371
372 $titleNs = count( $this->namespaces ) === 1 ? reset( $this->namespaces ) : null;
373 $title = Title::newFromText( $term, $titleNs );
374 $languageConverter = $this->languageConverterFactory->getLanguageConverter( $this->getContentLanguage() );
375 if ( $languageConverter->hasVariants() ) {
376 // findVariantLink will replace the link arg as well but we want to keep our original
377 // search string, use a copy in the $variantTerm var so that $term remains intact.
378 $variantTerm = $term;
379 $languageConverter->findVariantLink( $variantTerm, $title );
380 }
381
382 $showSuggestion = $title === null || !$title->isKnown();
383 $engine->setShowSuggestion( $showSuggestion );
384
385 // fetch search results
386 $titleMatches = $engine->searchTitle( $term );
387 $textMatches = $engine->searchText( $term );
388
389 $textStatus = null;
390 if ( $textMatches instanceof StatusValue ) {
391 $textStatus = $textMatches;
392 $textMatches = $textStatus->getValue();
393 }
394
395 if ( $textMatches && $textMatches->numRows() ) {
396 $engine->augmentSearchResults( $textMatches );
397 }
398
399 $this->getHookRunner()->onSpecialSearchResults( $term, $titleMatches, $textMatches );
400
401 // Get number of results
402 $titleMatchesNum = $textMatchesNum = $numTitleMatches = $numTextMatches = 0;
403 $approxTotalRes = false;
404 if ( $titleMatches ) {
405 $titleMatchesNum = $titleMatches->numRows();
406 $numTitleMatches = $titleMatches->getTotalHits();
407 $approxTotalRes = $titleMatches->isApproximateTotalHits();
408 }
409 if ( $textMatches ) {
410 $textMatchesNum = $textMatches->numRows();
411 $numTextMatches = $textMatches->getTotalHits();
412 $approxTotalRes = $approxTotalRes || $textMatches->isApproximateTotalHits();
413 }
414 $num = $titleMatchesNum + $textMatchesNum;
415 $totalRes = $numTitleMatches + $numTextMatches;
416
417 // start rendering the page
418 $out->enableOOUI();
419 $out->addHTML( $formWidget->render(
420 $this->profile, $term, $num, $totalRes, $approxTotalRes, $this->offset, $this->isPowerSearch(),
421 $widgetOptions
422 ) );
423
424 // did you mean... suggestions
425 if ( $textMatches ) {
426 $dymWidget = new DidYouMeanWidget( $this );
427 $out->addHTML( $dymWidget->render( $term, $textMatches ) );
428 }
429
430 $hasSearchErrors = $textStatus && $textStatus->getMessages() !== [];
431 $hasInlineIwResults = $textMatches &&
432 $textMatches->hasInterwikiResults( ISearchResultSet::INLINE_RESULTS );
433 $hasSecondaryIwResults = $textMatches &&
434 $textMatches->hasInterwikiResults( ISearchResultSet::SECONDARY_RESULTS );
435
436 $classNames = [ 'searchresults' ];
437 if ( $hasSecondaryIwResults ) {
438 $classNames[] = 'mw-searchresults-has-iw';
439 }
440 if ( $this->offset > 0 ) {
441 $classNames[] = 'mw-searchresults-has-offset';
442 }
443 $out->addHTML( Html::openElement( 'div', [ 'class' => $classNames ] ) );
444
445 $out->addHTML( '<div class="mw-search-results-info">' );
446
447 if ( $hasSearchErrors || $this->loadStatus->getMessages() ) {
448 if ( $textStatus === null ) {
449 $textStatus = $this->loadStatus;
450 } else {
451 $textStatus->merge( $this->loadStatus );
452 }
453 [ $error, $warning ] = $textStatus->splitByErrorType();
454 if ( $error->getMessages() ) {
455 $out->addHTML( Html::errorBox(
456 Status::wrap( $error )->getHTML( 'search-error' )
457 ) );
458 }
459 if ( $warning->getMessages() ) {
460 $out->addHTML( Html::warningBox(
461 Status::wrap( $warning )->getHTML( 'search-warning' )
462 ) );
463 }
464 }
465
466 // If we have no results and have not already displayed an error message
467 if ( $num === 0 && !$hasSearchErrors ) {
468 $out->wrapWikiMsg( "<p class=\"mw-search-nonefound\">\n$1</p>", [
469 $hasInlineIwResults ? 'search-nonefound-thiswiki' : 'search-nonefound',
470 wfEscapeWikiText( $term ),
471 $term
472 ] );
473 }
474
475 // Show the create link ahead
476 $this->showCreateLink( $title, $num, $titleMatches, $textMatches );
477
478 // Close <div class='mw-search-results-info'>
479 $out->addHTML( '</div>' );
480
481 // Although $num might be 0 there can still be secondary or inline
482 // results to display.
483 $linkRenderer = $this->getLinkRenderer();
484 $mainResultWidget = new FullSearchResultWidget(
485 $this,
486 $linkRenderer,
487 $this->getHookContainer(),
488 $this->repoGroup,
489 $this->thumbnailProvider,
490 $this->userOptionsManager
491 );
492
493 $sidebarResultWidget = new InterwikiSearchResultWidget( $this, $linkRenderer );
494 $sidebarResultsWidget = new InterwikiSearchResultSetWidget(
495 $this,
496 $sidebarResultWidget,
497 $linkRenderer,
498 $this->interwikiLookup,
499 $engine->getFeatureData( 'show-multimedia-search-results' ) ?? false
500 );
501
502 $widget = new BasicSearchResultSetWidget( $this, $mainResultWidget, $sidebarResultsWidget );
503
504 $out->addHTML( '<div class="mw-search-visualclear"></div>' );
505 $this->prevNextLinks( $totalRes, $textMatches, $term, 'mw-search-pager-top', $out );
506
507 $out->addHTML( $widget->render(
508 $term, $this->offset, $titleMatches, $textMatches
509 ) );
510
511 $out->addHTML( '<div class="mw-search-visualclear"></div>' );
512 $this->prevNextLinks( $totalRes, $textMatches, $term, 'mw-search-pager-bottom', $out );
513
514 // Close <div class='searchresults'>
515 $out->addHTML( "</div>" );
516
517 $this->getHookRunner()->onSpecialSearchResultsAppend( $this, $out, $term );
518 }
519
526 protected function showCreateLink( $title, $num, $titleMatches, $textMatches ) {
527 // show direct page/create link if applicable
528
529 // Check DBkey !== '' in case of fragment link only.
530 if ( $title === null || $title->getDBkey() === ''
531 || ( $titleMatches !== null && $titleMatches->searchContainedSyntax() )
532 || ( $textMatches !== null && $textMatches->searchContainedSyntax() )
533 ) {
534 // invalid title
535 // preserve the paragraph for margins etc...
536 $this->getOutput()->addHTML( '<p></p>' );
537
538 return;
539 }
540
541 $messageName = 'searchmenu-new-nocreate';
542 $linkClass = 'mw-search-createlink';
543
544 if ( !$title->isExternal() ) {
545 if ( $title->isKnown() ) {
546 $firstTitle = null;
547 if ( $titleMatches && $titleMatches->numRows() > 0 ) {
548 $firstTitle = $titleMatches->extractTitles()[0] ?? null;
549 } elseif ( $textMatches && $textMatches->numRows() > 0 ) {
550 $firstTitle = $textMatches->extractTitles()[0] ?? null;
551 }
552
553 if ( $firstTitle && $title->isSamePageAs( $firstTitle ) ) {
554 $messageName = '';
555 } else {
556 $messageName = 'searchmenu-exists';
557 $linkClass = 'mw-search-exists';
558 }
559 } elseif (
560 $this->contentHandlerFactory->getContentHandler( $title->getContentModel() )
561 ->supportsDirectEditing()
562 && $this->getAuthority()->probablyCan( 'edit', $title )
563 ) {
564 $messageName = 'searchmenu-new';
565 }
566 } else {
567 $messageName = 'searchmenu-new-external';
568 }
569
570 $params = [
571 $messageName,
572 wfEscapeWikiText( $title->getPrefixedText() ),
573 Message::numParam( $num )
574 ];
575 $this->getHookRunner()->onSpecialSearchCreateLink( $title, $params );
576
577 // Extensions using the hook might still return an empty $messageName
578 if ( $messageName ) {
579 $this->getOutput()->wrapWikiMsg( "<p class=\"$linkClass\">\n$1</p>", $params );
580 }
581 }
582
589 protected function setupPage( $term ) {
590 $out = $this->getOutput();
591
592 $this->setHeaders();
593 $this->outputHeader();
594 // TODO: Is this true? The namespace remember uses a user token
595 // on save.
596 $out->getMetadata()->setPreventClickjacking( false );
597 $this->addHelpLink( 'Help:Searching' );
598
599 if ( strval( $term ) !== '' ) {
600 $out->setPageTitleMsg( $this->msg( 'searchresults' ) );
601 $out->setHTMLTitle( $this->msg( 'pagetitle' )
602 ->plaintextParams( $this->msg( 'searchresults-title' )->plaintextParams( $term )->text() )
603 ->inContentLanguage()->text()
604 );
605 }
606
607 if ( $this->mPrefix !== '' ) {
608 $subtitle = $this->msg( 'search-filter-title-prefix' )->plaintextParams( $this->mPrefix );
609 $params = $this->powerSearchOptions();
610 unset( $params['prefix'] );
611 $params += [
612 'search' => $term,
613 'fulltext' => 1,
614 ];
615
616 $subtitle .= ' (';
617 $subtitle .= Html::element(
618 'a',
619 [
620 'href' => $this->getPageTitle()->getLocalURL( $params ),
621 'title' => $this->msg( 'search-filter-title-prefix-reset' )->text(),
622 ],
623 $this->msg( 'search-filter-title-prefix-reset' )->text()
624 );
625 $subtitle .= ')';
626 $out->setSubtitle( $subtitle );
627 }
628
629 $out->addJsConfigVars( [ 'searchTerm' => $term ] );
630 $out->addModules( 'mediawiki.special.search' );
631 $out->addModuleStyles( [
632 'mediawiki.special', 'mediawiki.special.search.styles',
633 'mediawiki.widgets.SearchInputWidget.styles',
634 // Special page makes use of Html::warningBox and Html::errorBox in multiple places.
635 'mediawiki.codex.messagebox.styles',
636 ] );
637 }
638
644 protected function isPowerSearch() {
645 return $this->profile === 'advanced';
646 }
647
655 protected function powerSearch( &$request ) {
656 $arr = [];
657 foreach ( $this->searchConfig->searchableNamespaces() as $ns => $_ ) {
658 if ( $request->getCheck( 'ns' . $ns ) ) {
659 $arr[] = $ns;
660 }
661 }
662
663 return $arr;
664 }
665
673 public function powerSearchOptions() {
674 $opt = [];
675 if ( $this->isPowerSearch() ) {
676 foreach ( $this->namespaces as $n ) {
677 $opt['ns' . $n] = 1;
678 }
679 } else {
680 $opt['profile'] = $this->profile;
681 }
682
683 return $opt + $this->extraParams;
684 }
685
691 protected function saveNamespaces() {
692 $user = $this->getUser();
693 $request = $this->getRequest();
694
695 if ( $user->isRegistered() &&
696 $user->matchEditToken(
697 $request->getVal( 'nsRemember' ),
698 'searchnamespace',
699 $request
700 ) && !$this->readOnlyMode->isReadOnly()
701 ) {
702 // Reset namespace preferences: namespaces are not searched
703 // when they're not mentioned in the URL parameters.
704 foreach ( $this->nsInfo->getValidNamespaces() as $n ) {
705 $this->userOptionsManager->setOption( $user, 'searchNs' . $n, false );
706 }
707 // The request parameters include all the namespaces to be searched.
708 // Even if they're the same as an existing profile, they're not eaten.
709 foreach ( $this->namespaces as $n ) {
710 $this->userOptionsManager->setOption( $user, 'searchNs' . $n, true );
711 }
712
713 DeferredUpdates::addCallableUpdate( static function () use ( $user ) {
714 $user->saveSettings();
715 } );
716
717 return true;
718 }
719
720 return false;
721 }
722
727 protected function getSearchProfiles() {
728 // Builds list of Search Types (profiles)
729 $nsAllSet = array_keys( $this->searchConfig->searchableNamespaces() );
730 $defaultNs = $this->searchConfig->defaultNamespaces();
731 $profiles = [
732 'default' => [
733 'message' => 'searchprofile-articles',
734 'tooltip' => 'searchprofile-articles-tooltip',
735 'namespaces' => $defaultNs,
736 'namespace-messages' => $this->searchConfig->namespacesAsText(
737 $defaultNs
738 ),
739 ],
740 'images' => [
741 'message' => 'searchprofile-images',
742 'tooltip' => 'searchprofile-images-tooltip',
743 'namespaces' => [ NS_FILE ],
744 ],
745 'all' => [
746 'message' => 'searchprofile-everything',
747 'tooltip' => 'searchprofile-everything-tooltip',
748 'namespaces' => $nsAllSet,
749 ],
750 'advanced' => [
751 'message' => 'searchprofile-advanced',
752 'tooltip' => 'searchprofile-advanced-tooltip',
753 'namespaces' => self::NAMESPACES_CURRENT,
754 ]
755 ];
756
757 $this->getHookRunner()->onSpecialSearchProfiles( $profiles );
758
759 foreach ( $profiles as &$data ) {
760 if ( !is_array( $data['namespaces'] ) ) {
761 continue;
762 }
763 sort( $data['namespaces'] );
764 }
765
766 return $profiles;
767 }
768
774 public function getSearchEngine() {
775 if ( $this->searchEngine === null ) {
776 $this->searchEngine = $this->searchEngineFactory->create( $this->searchEngineType );
777 }
778
779 return $this->searchEngine;
780 }
781
786 public function getProfile() {
787 return $this->profile;
788 }
789
794 public function getNamespaces() {
795 return $this->namespaces;
796 }
797
807 public function setExtraParam( $key, $value ) {
808 $this->extraParams[$key] = $value;
809 }
810
819 public function getPrefix() {
820 return $this->mPrefix;
821 }
822
830 private function prevNextLinks(
831 ?int $totalRes,
832 ?ISearchResultSet $textMatches,
833 string $term,
834 string $class,
835 OutputPage $out
836 ) {
837 if ( $totalRes > $this->limit || $this->offset ) {
838 // Allow matches to define the correct offset, as interleaved
839 // AB testing may require a different next page offset.
840 if ( $textMatches && $textMatches->getOffset() !== null ) {
841 $offset = $textMatches->getOffset();
842 } else {
843 $offset = $this->offset;
844 }
845
846 // use the rewritten search term for subsequent page searches
847 $newSearchTerm = $term;
848 if ( $textMatches && $textMatches->hasRewrittenQuery() ) {
849 $newSearchTerm = $textMatches->getQueryAfterRewrite();
850 }
851
852 $prevNext =
853 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable offset is not null
854 $this->buildPrevNextNavigation( $offset, $this->limit,
855 $this->powerSearchOptions() + [ 'search' => $newSearchTerm ],
856 $this->limit + $this->offset >= $totalRes );
857 $out->addHTML( "<div class='{$class}'>{$prevNext}</div>\n" );
858 }
859 }
860
871 protected function buildPrevNextNavigation(
872 $offset,
873 $limit,
874 array $query = [],
875 $atEnd = false,
876 $subpage = false
877 ) {
878 $navBuilder = new CodexPagerNavigationBuilder(
879 $this->getContext(), array_merge( $this->getRequest()->getQueryValues(), $query )
880 );
881 $navBuilder
882 ->setPage( $this->getPageTitle( $subpage ) )
883 ->setLinkQuery( [ 'limit' => $limit, 'offset' => $offset ] + $query )
884 ->setLimitLinkQueryParam( 'limit' )
885 ->setCurrentLimit( $limit )
886 ->setPrevTooltipMsg( 'prevn-title' )
887 ->setNextTooltipMsg( 'nextn-title' )
888 ->setLimitTooltipMsg( 'shown-title' )
889 ->setHideLast( true );
890
891 if ( $offset > 0 ) {
892 $navBuilder->setFirstLinkQuery( [ 'offset' => 0 ] );
893 $navBuilder->setPrevLinkQuery( [ 'offset' => (string)max( $offset - $limit, 0 ) ] );
894 }
895 if ( !$atEnd ) {
896 $navBuilder->setNextLinkQuery( [ 'offset' => (string)( $offset + $limit ) ] );
897 }
898
899 return $navBuilder->getHtml();
900 }
901
903 protected function getGroupName() {
904 return 'pages';
905 }
906}
907
912class_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:69
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
An interface for creating language converters.
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,...
Configuration handling class for SearchEngine.
Factory class for SearchEngine.
Contain a class for special pages.
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)
buildPrevNextNavigation( $offset, $limit, array $query=[], $atEnd=false, $subpage=false)
Generate navigation for pagination.
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.
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.
__construct(protected readonly SearchEngineConfig $searchConfig, private readonly SearchEngineFactory $searchEngineFactory, private readonly NamespaceInfo $nsInfo, private readonly IContentHandlerFactory $contentHandlerFactory, private readonly InterwikiLookup $interwikiLookup, private readonly ReadOnlyMode $readOnlyMode, private readonly UserOptionsManager $userOptionsManager, private readonly LanguageConverterFactory $languageConverterFactory, private readonly RepoGroup $repoGroup, private readonly SearchResultThumbnailProvider $thumbnailProvider, private readonly TitleMatcher $titleMatcher,)
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:69
A service class to control user options.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Determine whether a site is currently in read-only mode.
Service interface for looking up Interwiki records.
A set of SearchEngine results.
isApproximateTotalHits()
If getTotalHits() is supported determine whether this number is approximate or not.
hasInterwikiResults( $type=self::SECONDARY_RESULTS)
Check if there are results on other wikis.
getTotalHits()
Some search modes return a total hit count for the query in the entire article database.
searchContainedSyntax()
Did the search contain search syntax? If so, Special:Search won't offer the user a link to a create a...
extractTitles()
Extract all the titles in the result set.
hasRewrittenQuery()
Some search modes will run an alternative query that it thinks gives a better result than the provide...
element(SerializerNode $parent, SerializerNode $node, $contents)