MediaWiki  REL1_24
SpecialSearch.php
Go to the documentation of this file.
00001 <?php
00030 class SpecialSearch extends SpecialPage {
00039     protected $profile;
00040 
00042     protected $searchEngine;
00043 
00045     protected $searchEngineType;
00046 
00048     protected $extraParams = array();
00049 
00051     protected $mPrefix;
00052 
00056     protected $limit, $offset;
00057 
00061     protected $namespaces;
00062 
00066     protected $didYouMeanHtml, $fulltext;
00067 
00068     const NAMESPACES_CURRENT = 'sense';
00069 
00070     public function __construct() {
00071         parent::__construct( 'Search' );
00072     }
00073 
00079     public function execute( $par ) {
00080         $this->setHeaders();
00081         $this->outputHeader();
00082         $out = $this->getOutput();
00083         $out->allowClickjacking();
00084         $out->addModuleStyles( array(
00085             'mediawiki.special', 'mediawiki.special.search', 'mediawiki.ui', 'mediawiki.ui.button',
00086             'mediawiki.ui.input',
00087         ) );
00088 
00089         // Strip underscores from title parameter; most of the time we'll want
00090         // text form here. But don't strip underscores from actual text params!
00091         $titleParam = str_replace( '_', ' ', $par );
00092 
00093         $request = $this->getRequest();
00094 
00095         // Fetch the search term
00096         $search = str_replace( "\n", " ", $request->getText( 'search', $titleParam ) );
00097 
00098         $this->load();
00099         if ( !is_null( $request->getVal( 'nsRemember' ) ) ) {
00100             $this->saveNamespaces();
00101             // Remove the token from the URL to prevent the user from inadvertently
00102             // exposing it (e.g. by pasting it into a public wiki page) or undoing
00103             // later settings changes (e.g. by reloading the page).
00104             $query = $request->getValues();
00105             unset( $query['title'], $query['nsRemember'] );
00106             $out->redirect( $this->getPageTitle()->getFullURL( $query ) );
00107             return;
00108         }
00109 
00110         $this->searchEngineType = $request->getVal( 'srbackend' );
00111 
00112         if ( $request->getVal( 'fulltext' )
00113             || !is_null( $request->getVal( 'offset' ) )
00114         ) {
00115             $this->showResults( $search );
00116         } else {
00117             $this->goResult( $search );
00118         }
00119     }
00120 
00126     public function load() {
00127         $request = $this->getRequest();
00128         list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, '' );
00129         $this->mPrefix = $request->getVal( 'prefix', '' );
00130 
00131         $user = $this->getUser();
00132 
00133         # Extract manually requested namespaces
00134         $nslist = $this->powerSearch( $request );
00135         if ( !count( $nslist ) ) {
00136             # Fallback to user preference
00137             $nslist = SearchEngine::userNamespaces( $user );
00138         }
00139 
00140         $profile = null;
00141         if ( !count( $nslist ) ) {
00142             $profile = 'default';
00143         }
00144 
00145         $profile = $request->getVal( 'profile', $profile );
00146         $profiles = $this->getSearchProfiles();
00147         if ( $profile === null ) {
00148             // BC with old request format
00149             $profile = 'advanced';
00150             foreach ( $profiles as $key => $data ) {
00151                 if ( $nslist === $data['namespaces'] && $key !== 'advanced' ) {
00152                     $profile = $key;
00153                 }
00154             }
00155             $this->namespaces = $nslist;
00156         } elseif ( $profile === 'advanced' ) {
00157             $this->namespaces = $nslist;
00158         } else {
00159             if ( isset( $profiles[$profile]['namespaces'] ) ) {
00160                 $this->namespaces = $profiles[$profile]['namespaces'];
00161             } else {
00162                 // Unknown profile requested
00163                 $profile = 'default';
00164                 $this->namespaces = $profiles['default']['namespaces'];
00165             }
00166         }
00167 
00168         $this->didYouMeanHtml = ''; # html of did you mean... link
00169         $this->fulltext = $request->getVal( 'fulltext' );
00170         $this->profile = $profile;
00171     }
00172 
00178     public function goResult( $term ) {
00179         $this->setupPage( $term );
00180         # Try to go to page as entered.
00181         $title = Title::newFromText( $term );
00182         # If the string cannot be used to create a title
00183         if ( is_null( $title ) ) {
00184             $this->showResults( $term );
00185 
00186             return;
00187         }
00188         # If there's an exact or very near match, jump right there.
00189         $title = SearchEngine::getNearMatch( $term );
00190 
00191         if ( !is_null( $title ) ) {
00192             $this->getOutput()->redirect( $title->getFullURL() );
00193 
00194             return;
00195         }
00196         # No match, generate an edit URL
00197         $title = Title::newFromText( $term );
00198         if ( !is_null( $title ) ) {
00199             wfRunHooks( 'SpecialSearchNogomatch', array( &$title ) );
00200             wfDebugLog( 'nogomatch', $title->getFullText(), 'private' );
00201 
00202             # If the feature is enabled, go straight to the edit page
00203             if ( $this->getConfig()->get( 'GoToEdit' ) ) {
00204                 $this->getOutput()->redirect( $title->getFullURL( array( 'action' => 'edit' ) ) );
00205 
00206                 return;
00207             }
00208         }
00209         $this->showResults( $term );
00210     }
00211 
00215     public function showResults( $term ) {
00216         global $wgContLang;
00217 
00218         $profile = new ProfileSection( __METHOD__ );
00219         $search = $this->getSearchEngine();
00220         $search->setLimitOffset( $this->limit, $this->offset );
00221         $search->setNamespaces( $this->namespaces );
00222         $search->prefix = $this->mPrefix;
00223         $term = $search->transformSearchTerm( $term );
00224 
00225         wfRunHooks( 'SpecialSearchSetupEngine', array( $this, $this->profile, $search ) );
00226 
00227         $this->setupPage( $term );
00228 
00229         $out = $this->getOutput();
00230 
00231         if ( $this->getConfig()->get( 'DisableTextSearch' ) ) {
00232             $searchFowardUrl = $this->getConfig()->get( 'SearchForwardUrl' );
00233             if ( $searchFowardUrl ) {
00234                 $url = str_replace( '$1', urlencode( $term ), $searchFowardUrl );
00235                 $out->redirect( $url );
00236             } else {
00237                 $out->addHTML(
00238                     Xml::openElement( 'fieldset' ) .
00239                     Xml::element( 'legend', null, $this->msg( 'search-external' )->text() ) .
00240                     Xml::element(
00241                         'p',
00242                         array( 'class' => 'mw-searchdisabled' ),
00243                         $this->msg( 'searchdisabled' )->text()
00244                     ) .
00245                     $this->msg( 'googlesearch' )->rawParams(
00246                         htmlspecialchars( $term ),
00247                         'UTF-8',
00248                         $this->msg( 'searchbutton' )->escaped()
00249                     )->text() .
00250                     Xml::closeElement( 'fieldset' )
00251                 );
00252             }
00253 
00254             return;
00255         }
00256 
00257         $title = Title::newFromText( $term );
00258         $showSuggestion = $title === null || !$title->isKnown();
00259         $search->setShowSuggestion( $showSuggestion );
00260 
00261         // fetch search results
00262         $rewritten = $search->replacePrefixes( $term );
00263 
00264         $titleMatches = $search->searchTitle( $rewritten );
00265         $textMatches = $search->searchText( $rewritten );
00266 
00267         $textStatus = null;
00268         if ( $textMatches instanceof Status ) {
00269             $textStatus = $textMatches;
00270             $textMatches = null;
00271         }
00272 
00273         // did you mean... suggestions
00274         if ( $showSuggestion && $textMatches && !$textStatus && $textMatches->hasSuggestion() ) {
00275             # mirror Go/Search behavior of original request ..
00276             $didYouMeanParams = array( 'search' => $textMatches->getSuggestionQuery() );
00277 
00278             if ( $this->fulltext != null ) {
00279                 $didYouMeanParams['fulltext'] = $this->fulltext;
00280             }
00281 
00282             $stParams = array_merge(
00283                 $didYouMeanParams,
00284                 $this->powerSearchOptions()
00285             );
00286 
00287             $suggestionSnippet = $textMatches->getSuggestionSnippet();
00288 
00289             if ( $suggestionSnippet == '' ) {
00290                 $suggestionSnippet = null;
00291             }
00292 
00293             $suggestLink = Linker::linkKnown(
00294                 $this->getPageTitle(),
00295                 $suggestionSnippet,
00296                 array(),
00297                 $stParams
00298             );
00299 
00300             $this->didYouMeanHtml = '<div class="searchdidyoumean">'
00301                 . $this->msg( 'search-suggest' )->rawParams( $suggestLink )->text() . '</div>';
00302         }
00303 
00304         if ( !wfRunHooks( 'SpecialSearchResultsPrepend', array( $this, $out, $term ) ) ) {
00305             # Hook requested termination
00306             return;
00307         }
00308 
00309         // start rendering the page
00310         $out->addHtml(
00311             Xml::openElement(
00312                 'form',
00313                 array(
00314                     'id' => ( $this->profile === 'advanced' ? 'powersearch' : 'search' ),
00315                     'method' => 'get',
00316                     'action' => wfScript(),
00317                 )
00318             )
00319         );
00320 
00321         // Get number of results
00322         $titleMatchesNum = $textMatchesNum = $numTitleMatches = $numTextMatches = 0;
00323         if ( $titleMatches ) {
00324             $titleMatchesNum = $titleMatches->numRows();
00325             $numTitleMatches = $titleMatches->getTotalHits();
00326         }
00327         if ( $textMatches ) {
00328             $textMatchesNum = $textMatches->numRows();
00329             $numTextMatches = $textMatches->getTotalHits();
00330         }
00331         $num = $titleMatchesNum + $textMatchesNum;
00332         $totalRes = $numTitleMatches + $numTextMatches;
00333 
00334         $out->addHtml(
00335             # This is an awful awful ID name. It's not a table, but we
00336             # named it poorly from when this was a table so now we're
00337             # stuck with it
00338             Xml::openElement( 'div', array( 'id' => 'mw-search-top-table' ) ) .
00339             $this->shortDialog( $term, $num, $totalRes ) .
00340             Xml::closeElement( 'div' ) .
00341             $this->formHeader( $term ) .
00342             Xml::closeElement( 'form' )
00343         );
00344 
00345         $filePrefix = $wgContLang->getFormattedNsText( NS_FILE ) . ':';
00346         if ( trim( $term ) === '' || $filePrefix === trim( $term ) ) {
00347             // Empty query -- straight view of search form
00348             return;
00349         }
00350 
00351         $out->addHtml( "<div class='searchresults'>" );
00352 
00353         // prev/next links
00354         $prevnext = null;
00355         if ( $num || $this->offset ) {
00356             // Show the create link ahead
00357             $this->showCreateLink( $title, $num, $titleMatches, $textMatches );
00358             if ( $totalRes > $this->limit || $this->offset ) {
00359                 if ( $this->searchEngineType !== null ) {
00360                     $this->setExtraParam( 'srbackend', $this->searchEngineType );
00361                 }
00362                 $prevnext = $this->getLanguage()->viewPrevNext(
00363                     $this->getPageTitle(),
00364                     $this->offset,
00365                     $this->limit,
00366                     $this->powerSearchOptions() + array( 'search' => $term ),
00367                     $this->limit + $this->offset >= $totalRes
00368                 );
00369             }
00370         }
00371         wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) );
00372 
00373         $out->parserOptions()->setEditSection( false );
00374         if ( $titleMatches ) {
00375             if ( $numTitleMatches > 0 ) {
00376                 $out->wrapWikiMsg( "==$1==\n", 'titlematches' );
00377                 $out->addHTML( $this->showMatches( $titleMatches ) );
00378             }
00379             $titleMatches->free();
00380         }
00381         if ( $textMatches && !$textStatus ) {
00382             // output appropriate heading
00383             if ( $numTextMatches > 0 && $numTitleMatches > 0 ) {
00384                 // if no title matches the heading is redundant
00385                 $out->wrapWikiMsg( "==$1==\n", 'textmatches' );
00386             }
00387 
00388             // show interwiki results if any
00389             if ( $textMatches->hasInterwikiResults() ) {
00390                 $out->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ) );
00391             }
00392             // show results
00393             if ( $numTextMatches > 0 ) {
00394                 $out->addHTML( $this->showMatches( $textMatches ) );
00395             }
00396 
00397             $textMatches->free();
00398         }
00399         if ( $num === 0 ) {
00400             if ( $textStatus ) {
00401                 $out->addHTML( '<div class="error">' .
00402                     $textStatus->getMessage( 'search-error' ) . '</div>' );
00403             } else {
00404                 $out->wrapWikiMsg( "<p class=\"mw-search-nonefound\">\n$1</p>",
00405                     array( 'search-nonefound', wfEscapeWikiText( $term ) ) );
00406                 $this->showCreateLink( $title, $num, $titleMatches, $textMatches );
00407             }
00408         }
00409         $out->addHtml( "</div>" );
00410 
00411         if ( $prevnext ) {
00412             $out->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" );
00413         }
00414     }
00415 
00422     protected function showCreateLink( $title, $num, $titleMatches, $textMatches ) {
00423         // show direct page/create link if applicable
00424 
00425         // Check DBkey !== '' in case of fragment link only.
00426         if ( is_null( $title ) || $title->getDBkey() === ''
00427             || ( $titleMatches !== null && $titleMatches->searchContainedSyntax() )
00428             || ( $textMatches !== null && $textMatches->searchContainedSyntax() )
00429         ) {
00430             // invalid title
00431             // preserve the paragraph for margins etc...
00432             $this->getOutput()->addHtml( '<p></p>' );
00433 
00434             return;
00435         }
00436 
00437         $linkClass = 'mw-search-createlink';
00438         if ( $title->isKnown() ) {
00439             $messageName = 'searchmenu-exists';
00440             $linkClass = 'mw-search-exists';
00441         } elseif ( $title->quickUserCan( 'create', $this->getUser() ) ) {
00442             $messageName = 'searchmenu-new';
00443         } else {
00444             $messageName = 'searchmenu-new-nocreate';
00445         }
00446         $params = array(
00447             $messageName,
00448             wfEscapeWikiText( $title->getPrefixedText() ),
00449             Message::numParam( $num )
00450         );
00451         wfRunHooks( 'SpecialSearchCreateLink', array( $title, &$params ) );
00452 
00453         // Extensions using the hook might still return an empty $messageName
00454         if ( $messageName ) {
00455             $this->getOutput()->wrapWikiMsg( "<p class=\"$linkClass\">\n$1</p>", $params );
00456         } else {
00457             // preserve the paragraph for margins etc...
00458             $this->getOutput()->addHtml( '<p></p>' );
00459         }
00460     }
00461 
00465     protected function setupPage( $term ) {
00466         # Should advanced UI be used?
00467         $this->searchAdvanced = ( $this->profile === 'advanced' );
00468         $out = $this->getOutput();
00469         if ( strval( $term ) !== '' ) {
00470             $out->setPageTitle( $this->msg( 'searchresults' ) );
00471             $out->setHTMLTitle( $this->msg( 'pagetitle' )
00472                 ->rawParams( $this->msg( 'searchresults-title' )->rawParams( $term )->text() )
00473                 ->inContentLanguage()->text()
00474             );
00475         }
00476         // add javascript specific to special:search
00477         $out->addModules( 'mediawiki.special.search' );
00478     }
00479 
00487     protected function powerSearch( &$request ) {
00488         $arr = array();
00489         foreach ( SearchEngine::searchableNamespaces() as $ns => $name ) {
00490             if ( $request->getCheck( 'ns' . $ns ) ) {
00491                 $arr[] = $ns;
00492             }
00493         }
00494 
00495         return $arr;
00496     }
00497 
00503     protected function powerSearchOptions() {
00504         $opt = array();
00505         if ( $this->profile !== 'advanced' ) {
00506             $opt['profile'] = $this->profile;
00507         } else {
00508             foreach ( $this->namespaces as $n ) {
00509                 $opt['ns' . $n] = 1;
00510             }
00511         }
00512 
00513         return $opt + $this->extraParams;
00514     }
00515 
00521     protected function saveNamespaces() {
00522         $user = $this->getUser();
00523         $request = $this->getRequest();
00524 
00525         if ( $user->isLoggedIn() &&
00526             $user->matchEditToken(
00527                 $request->getVal( 'nsRemember' ),
00528                 'searchnamespace',
00529                 $request
00530             )
00531         ) {
00532             // Reset namespace preferences: namespaces are not searched
00533             // when they're not mentioned in the URL parameters.
00534             foreach ( MWNamespace::getValidNamespaces() as $n ) {
00535                 $user->setOption( 'searchNs' . $n, false );
00536             }
00537             // The request parameters include all the namespaces to be searched.
00538             // Even if they're the same as an existing profile, they're not eaten.
00539             foreach ( $this->namespaces as $n ) {
00540                 $user->setOption( 'searchNs' . $n, true );
00541             }
00542 
00543             $user->saveSettings();
00544             return true;
00545         }
00546 
00547         return false;
00548     }
00549 
00557     protected function showMatches( &$matches ) {
00558         global $wgContLang;
00559 
00560         $profile = new ProfileSection( __METHOD__ );
00561         $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
00562 
00563         $out = "<ul class='mw-search-results'>\n";
00564         $result = $matches->next();
00565         while ( $result ) {
00566             $out .= $this->showHit( $result, $terms );
00567             $result = $matches->next();
00568         }
00569         $out .= "</ul>\n";
00570 
00571         // convert the whole thing to desired language variant
00572         $out = $wgContLang->convert( $out );
00573 
00574         return $out;
00575     }
00576 
00585     protected function showHit( $result, $terms ) {
00586         $profile = new ProfileSection( __METHOD__ );
00587 
00588         if ( $result->isBrokenTitle() ) {
00589             return '';
00590         }
00591 
00592         $title = $result->getTitle();
00593 
00594         $titleSnippet = $result->getTitleSnippet( $terms );
00595 
00596         if ( $titleSnippet == '' ) {
00597             $titleSnippet = null;
00598         }
00599 
00600         $link_t = clone $title;
00601 
00602         wfRunHooks( 'ShowSearchHitTitle',
00603             array( &$link_t, &$titleSnippet, $result, $terms, $this ) );
00604 
00605         $link = Linker::linkKnown(
00606             $link_t,
00607             $titleSnippet
00608         );
00609 
00610         //If page content is not readable, just return the title.
00611         //This is not quite safe, but better than showing excerpts from non-readable pages
00612         //Note that hiding the entry entirely would screw up paging.
00613         if ( !$title->userCan( 'read', $this->getUser() ) ) {
00614             return "<li>{$link}</li>\n";
00615         }
00616 
00617         // If the page doesn't *exist*... our search index is out of date.
00618         // The least confusing at this point is to drop the result.
00619         // You may get less results, but... oh well. :P
00620         if ( $result->isMissingRevision() ) {
00621             return '';
00622         }
00623 
00624         // format redirects / relevant sections
00625         $redirectTitle = $result->getRedirectTitle();
00626         $redirectText = $result->getRedirectSnippet( $terms );
00627         $sectionTitle = $result->getSectionTitle();
00628         $sectionText = $result->getSectionSnippet( $terms );
00629         $redirect = '';
00630 
00631         if ( !is_null( $redirectTitle ) ) {
00632             if ( $redirectText == '' ) {
00633                 $redirectText = null;
00634             }
00635 
00636             $redirect = "<span class='searchalttitle'>" .
00637                 $this->msg( 'search-redirect' )->rawParams(
00638                     Linker::linkKnown( $redirectTitle, $redirectText ) )->text() .
00639                 "</span>";
00640         }
00641 
00642         $section = '';
00643 
00644         if ( !is_null( $sectionTitle ) ) {
00645             if ( $sectionText == '' ) {
00646                 $sectionText = null;
00647             }
00648 
00649             $section = "<span class='searchalttitle'>" .
00650                 $this->msg( 'search-section' )->rawParams(
00651                     Linker::linkKnown( $sectionTitle, $sectionText ) )->text() .
00652                 "</span>";
00653         }
00654 
00655         // format text extract
00656         $extract = "<div class='searchresult'>" . $result->getTextSnippet( $terms ) . "</div>";
00657 
00658         $lang = $this->getLanguage();
00659 
00660         // format description
00661         $byteSize = $result->getByteSize();
00662         $wordCount = $result->getWordCount();
00663         $timestamp = $result->getTimestamp();
00664         $size = $this->msg( 'search-result-size', $lang->formatSize( $byteSize ) )
00665             ->numParams( $wordCount )->escaped();
00666 
00667         if ( $title->getNamespace() == NS_CATEGORY ) {
00668             $cat = Category::newFromTitle( $title );
00669             $size = $this->msg( 'search-result-category-size' )
00670                 ->numParams( $cat->getPageCount(), $cat->getSubcatCount(), $cat->getFileCount() )
00671                 ->escaped();
00672         }
00673 
00674         $date = $lang->userTimeAndDate( $timestamp, $this->getUser() );
00675 
00676         $fileMatch = '';
00677         // Include a thumbnail for media files...
00678         if ( $title->getNamespace() == NS_FILE ) {
00679             $img = $result->getFile();
00680             $img = $img ?: wfFindFile( $title );
00681             if ( $result->isFileMatch() ) {
00682                 $fileMatch = "<span class='searchalttitle'>" .
00683                     $this->msg( 'search-file-match' )->escaped() . "</span>";
00684             }
00685             if ( $img ) {
00686                 $thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) );
00687                 if ( $thumb ) {
00688                     $desc = $this->msg( 'parentheses' )->rawParams( $img->getShortDesc() )->escaped();
00689                     // Float doesn't seem to interact well with the bullets.
00690                     // Table messes up vertical alignment of the bullets.
00691                     // Bullets are therefore disabled (didn't look great anyway).
00692                     return "<li>" .
00693                         '<table class="searchResultImage">' .
00694                         '<tr>' .
00695                         '<td style="width: 120px; text-align: center; vertical-align: top;">' .
00696                         $thumb->toHtml( array( 'desc-link' => true ) ) .
00697                         '</td>' .
00698                         '<td style="vertical-align: top;">' .
00699                         "{$link} {$redirect} {$section} {$fileMatch}" .
00700                         $extract .
00701                         "<div class='mw-search-result-data'>{$desc} - {$date}</div>" .
00702                         '</td>' .
00703                         '</tr>' .
00704                         '</table>' .
00705                         "</li>\n";
00706                 }
00707             }
00708         }
00709 
00710         $html = null;
00711 
00712         $score = '';
00713         if ( wfRunHooks( 'ShowSearchHit', array(
00714             $this, $result, $terms,
00715             &$link, &$redirect, &$section, &$extract,
00716             &$score, &$size, &$date, &$related,
00717             &$html
00718         ) ) ) {
00719             $html = "<li><div class='mw-search-result-heading'>" .
00720                 "{$link} {$redirect} {$section} {$fileMatch}</div> {$extract}\n" .
00721                 "<div class='mw-search-result-data'>{$size} - {$date}</div>" .
00722                 "</li>\n";
00723         }
00724 
00725         return $html;
00726     }
00727 
00736     protected function showInterwiki( $matches, $query ) {
00737         global $wgContLang;
00738         $profile = new ProfileSection( __METHOD__ );
00739 
00740         $out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>" .
00741             $this->msg( 'search-interwiki-caption' )->text() . "</div>\n";
00742         $out .= "<ul class='mw-search-iwresults'>\n";
00743 
00744         // work out custom project captions
00745         $customCaptions = array();
00746         // format per line <iwprefix>:<caption>
00747         $customLines = explode( "\n", $this->msg( 'search-interwiki-custom' )->text() );
00748         foreach ( $customLines as $line ) {
00749             $parts = explode( ":", $line, 2 );
00750             if ( count( $parts ) == 2 ) { // validate line
00751                 $customCaptions[$parts[0]] = $parts[1];
00752             }
00753         }
00754 
00755         if ( !is_array( $matches ) ) {
00756             $matches = array( $matches );
00757         }
00758 
00759         foreach ( $matches as $set ) {
00760             $prev = null;
00761             $result = $set->next();
00762             while ( $result ) {
00763                 $out .= $this->showInterwikiHit( $result, $prev, $query, $customCaptions );
00764                 $prev = $result->getInterwikiPrefix();
00765                 $result = $set->next();
00766             }
00767         }
00768 
00769         // @todo Should support paging in a non-confusing way (not sure how though, maybe via ajax)..
00770         $out .= "</ul></div>\n";
00771 
00772         // convert the whole thing to desired language variant
00773         $out = $wgContLang->convert( $out );
00774 
00775         return $out;
00776     }
00777 
00788     protected function showInterwikiHit( $result, $lastInterwiki, $query, $customCaptions ) {
00789         $profile = new ProfileSection( __METHOD__ );
00790 
00791         if ( $result->isBrokenTitle() ) {
00792             return '';
00793         }
00794 
00795         $title = $result->getTitle();
00796 
00797         $titleSnippet = $result->getTitleSnippet();
00798 
00799         if ( $titleSnippet == '' ) {
00800             $titleSnippet = null;
00801         }
00802 
00803         $link = Linker::linkKnown(
00804             $title,
00805             $titleSnippet
00806         );
00807 
00808         // format redirect if any
00809         $redirectTitle = $result->getRedirectTitle();
00810         $redirectText = $result->getRedirectSnippet();
00811         $redirect = '';
00812         if ( !is_null( $redirectTitle ) ) {
00813             if ( $redirectText == '' ) {
00814                 $redirectText = null;
00815             }
00816 
00817             $redirect = "<span class='searchalttitle'>" .
00818                 $this->msg( 'search-redirect' )->rawParams(
00819                     Linker::linkKnown( $redirectTitle, $redirectText ) )->text() .
00820                 "</span>";
00821         }
00822 
00823         $out = "";
00824         // display project name
00825         if ( is_null( $lastInterwiki ) || $lastInterwiki != $title->getInterwiki() ) {
00826             if ( array_key_exists( $title->getInterwiki(), $customCaptions ) ) {
00827                 // captions from 'search-interwiki-custom'
00828                 $caption = $customCaptions[$title->getInterwiki()];
00829             } else {
00830                 // default is to show the hostname of the other wiki which might suck
00831                 // if there are many wikis on one hostname
00832                 $parsed = wfParseUrl( $title->getFullURL() );
00833                 $caption = $this->msg( 'search-interwiki-default', $parsed['host'] )->text();
00834             }
00835             // "more results" link (special page stuff could be localized, but we might not know target lang)
00836             $searchTitle = Title::newFromText( $title->getInterwiki() . ":Special:Search" );
00837             $searchLink = Linker::linkKnown(
00838                 $searchTitle,
00839                 $this->msg( 'search-interwiki-more' )->text(),
00840                 array(),
00841                 array(
00842                     'search' => $query,
00843                     'fulltext' => 'Search'
00844                 )
00845             );
00846             $out .= "</ul><div class='mw-search-interwiki-project'><span class='mw-search-interwiki-more'>
00847                 {$searchLink}</span>{$caption}</div>\n<ul>";
00848         }
00849 
00850         $out .= "<li>{$link} {$redirect}</li>\n";
00851 
00852         return $out;
00853     }
00854 
00862     protected function powerSearchBox( $term, $opts ) {
00863         global $wgContLang;
00864 
00865         // Groups namespaces into rows according to subject
00866         $rows = array();
00867         foreach ( SearchEngine::searchableNamespaces() as $namespace => $name ) {
00868             $subject = MWNamespace::getSubject( $namespace );
00869             if ( !array_key_exists( $subject, $rows ) ) {
00870                 $rows[$subject] = "";
00871             }
00872 
00873             $name = $wgContLang->getConverter()->convertNamespace( $namespace );
00874             if ( $name == '' ) {
00875                 $name = $this->msg( 'blanknamespace' )->text();
00876             }
00877 
00878             $rows[$subject] .=
00879                 Xml::openElement( 'td' ) .
00880                 Xml::checkLabel(
00881                     $name,
00882                     "ns{$namespace}",
00883                     "mw-search-ns{$namespace}",
00884                     in_array( $namespace, $this->namespaces )
00885                 ) .
00886                 Xml::closeElement( 'td' );
00887         }
00888 
00889         $rows = array_values( $rows );
00890         $numRows = count( $rows );
00891 
00892         // Lays out namespaces in multiple floating two-column tables so they'll
00893         // be arranged nicely while still accommodating different screen widths
00894         $namespaceTables = '';
00895         for ( $i = 0; $i < $numRows; $i += 4 ) {
00896             $namespaceTables .= Xml::openElement(
00897                 'table',
00898                 array( 'cellpadding' => 0, 'cellspacing' => 0 )
00899             );
00900 
00901             for ( $j = $i; $j < $i + 4 && $j < $numRows; $j++ ) {
00902                 $namespaceTables .= Xml::tags( 'tr', null, $rows[$j] );
00903             }
00904 
00905             $namespaceTables .= Xml::closeElement( 'table' );
00906         }
00907 
00908         $showSections = array( 'namespaceTables' => $namespaceTables );
00909 
00910         wfRunHooks( 'SpecialSearchPowerBox', array( &$showSections, $term, $opts ) );
00911 
00912         $hidden = '';
00913         foreach ( $opts as $key => $value ) {
00914             $hidden .= Html::hidden( $key, $value );
00915         }
00916 
00917         # Stuff to feed saveNamespaces()
00918         $remember = '';
00919         $user = $this->getUser();
00920         if ( $user->isLoggedIn() ) {
00921             $remember .= Xml::checkLabel(
00922                 wfMessage( 'powersearch-remember' )->text(),
00923                 'nsRemember',
00924                 'mw-search-powersearch-remember',
00925                 false,
00926                 // The token goes here rather than in a hidden field so it
00927                 // is only sent when necessary (not every form submission).
00928                 array( 'value' => $user->getEditToken(
00929                     'searchnamespace',
00930                     $this->getRequest()
00931                 ) )
00932             );
00933         }
00934 
00935         // Return final output
00936         return Xml::openElement( 'fieldset', array( 'id' => 'mw-searchoptions' ) ) .
00937             Xml::element( 'legend', null, $this->msg( 'powersearch-legend' )->text() ) .
00938             Xml::tags( 'h4', null, $this->msg( 'powersearch-ns' )->parse() ) .
00939             Xml::element( 'div', array( 'id' => 'mw-search-togglebox' ), '', false ) .
00940             Xml::element( 'div', array( 'class' => 'divider' ), '', false ) .
00941             implode( Xml::element( 'div', array( 'class' => 'divider' ), '', false ), $showSections ) .
00942             $hidden .
00943             Xml::element( 'div', array( 'class' => 'divider' ), '', false ) .
00944             $remember .
00945             Xml::closeElement( 'fieldset' );
00946     }
00947 
00951     protected function getSearchProfiles() {
00952         // Builds list of Search Types (profiles)
00953         $nsAllSet = array_keys( SearchEngine::searchableNamespaces() );
00954 
00955         $profiles = array(
00956             'default' => array(
00957                 'message' => 'searchprofile-articles',
00958                 'tooltip' => 'searchprofile-articles-tooltip',
00959                 'namespaces' => SearchEngine::defaultNamespaces(),
00960                 'namespace-messages' => SearchEngine::namespacesAsText(
00961                     SearchEngine::defaultNamespaces()
00962                 ),
00963             ),
00964             'images' => array(
00965                 'message' => 'searchprofile-images',
00966                 'tooltip' => 'searchprofile-images-tooltip',
00967                 'namespaces' => array( NS_FILE ),
00968             ),
00969             'all' => array(
00970                 'message' => 'searchprofile-everything',
00971                 'tooltip' => 'searchprofile-everything-tooltip',
00972                 'namespaces' => $nsAllSet,
00973             ),
00974             'advanced' => array(
00975                 'message' => 'searchprofile-advanced',
00976                 'tooltip' => 'searchprofile-advanced-tooltip',
00977                 'namespaces' => self::NAMESPACES_CURRENT,
00978             )
00979         );
00980 
00981         wfRunHooks( 'SpecialSearchProfiles', array( &$profiles ) );
00982 
00983         foreach ( $profiles as &$data ) {
00984             if ( !is_array( $data['namespaces'] ) ) {
00985                 continue;
00986             }
00987             sort( $data['namespaces'] );
00988         }
00989 
00990         return $profiles;
00991     }
00992 
00997     protected function formHeader( $term ) {
00998         $out = Xml::openElement( 'div', array( 'class' => 'mw-search-formheader' ) );
00999 
01000         $bareterm = $term;
01001         if ( $this->startsWithImage( $term ) ) {
01002             // Deletes prefixes
01003             $bareterm = substr( $term, strpos( $term, ':' ) + 1 );
01004         }
01005 
01006         $profiles = $this->getSearchProfiles();
01007         $lang = $this->getLanguage();
01008 
01009         // Outputs XML for Search Types
01010         $out .= Xml::openElement( 'div', array( 'class' => 'search-types' ) );
01011         $out .= Xml::openElement( 'ul' );
01012         foreach ( $profiles as $id => $profile ) {
01013             if ( !isset( $profile['parameters'] ) ) {
01014                 $profile['parameters'] = array();
01015             }
01016             $profile['parameters']['profile'] = $id;
01017 
01018             $tooltipParam = isset( $profile['namespace-messages'] ) ?
01019                 $lang->commaList( $profile['namespace-messages'] ) : null;
01020             $out .= Xml::tags(
01021                 'li',
01022                 array(
01023                     'class' => $this->profile === $id ? 'current' : 'normal'
01024                 ),
01025                 $this->makeSearchLink(
01026                     $bareterm,
01027                     array(),
01028                     $this->msg( $profile['message'] )->text(),
01029                     $this->msg( $profile['tooltip'], $tooltipParam )->text(),
01030                     $profile['parameters']
01031                 )
01032             );
01033         }
01034         $out .= Xml::closeElement( 'ul' );
01035         $out .= Xml::closeElement( 'div' );
01036         $out .= Xml::element( 'div', array( 'style' => 'clear:both' ), '', false );
01037         $out .= Xml::closeElement( 'div' );
01038 
01039         // Hidden stuff
01040         $opts = array();
01041         $opts['profile'] = $this->profile;
01042 
01043         if ( $this->profile === 'advanced' ) {
01044             $out .= $this->powerSearchBox( $term, $opts );
01045         } else {
01046             $form = '';
01047             wfRunHooks( 'SpecialSearchProfileForm', array( $this, &$form, $this->profile, $term, $opts ) );
01048             $out .= $form;
01049         }
01050 
01051         return $out;
01052     }
01053 
01060     protected function shortDialog( $term, $resultsShown, $totalNum ) {
01061         $out = Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() );
01062         $out .= Html::hidden( 'profile', $this->profile ) . "\n";
01063         // Term box
01064         $out .= Html::input( 'search', $term, 'search', array(
01065             'id' => $this->profile === 'advanced' ? 'powerSearchText' : 'searchText',
01066             'size' => '50',
01067             'autofocus',
01068             'class' => 'mw-ui-input mw-ui-input-inline',
01069         ) ) . "\n";
01070         $out .= Html::hidden( 'fulltext', 'Search' ) . "\n";
01071         $out .= Xml::submitButton(
01072             $this->msg( 'searchbutton' )->text(),
01073             array( 'class' => array( 'mw-ui-button', 'mw-ui-progressive' ) )
01074         ) . "\n";
01075 
01076         // Results-info
01077         if ( $totalNum > 0 && $this->offset < $totalNum ) {
01078             $top = $this->msg( 'search-showingresults' )
01079                 ->numParams( $this->offset + 1, $this->offset + $resultsShown, $totalNum )
01080                 ->numParams( $resultsShown )
01081                 ->parse();
01082             $out .= Xml::tags( 'div', array( 'class' => 'results-info' ), $top ) .
01083                 Xml::element( 'div', array( 'style' => 'clear:both' ), '', false );
01084         }
01085 
01086         return $out . $this->didYouMeanHtml;
01087     }
01088 
01099     protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params = array() ) {
01100         $opt = $params;
01101         foreach ( $namespaces as $n ) {
01102             $opt['ns' . $n] = 1;
01103         }
01104 
01105         $stParams = array_merge(
01106             array(
01107                 'search' => $term,
01108                 'fulltext' => $this->msg( 'search' )->text()
01109             ),
01110             $opt
01111         );
01112 
01113         return Xml::element(
01114             'a',
01115             array(
01116                 'href' => $this->getPageTitle()->getLocalURL( $stParams ),
01117                 'title' => $tooltip
01118             ),
01119             $label
01120         );
01121     }
01122 
01129     protected function startsWithImage( $term ) {
01130         global $wgContLang;
01131 
01132         $parts = explode( ':', $term );
01133         if ( count( $parts ) > 1 ) {
01134             return $wgContLang->getNsIndex( $parts[0] ) == NS_FILE;
01135         }
01136 
01137         return false;
01138     }
01139 
01146     protected function startsWithAll( $term ) {
01147 
01148         $allkeyword = $this->msg( 'searchall' )->inContentLanguage()->text();
01149 
01150         $parts = explode( ':', $term );
01151         if ( count( $parts ) > 1 ) {
01152             return $parts[0] == $allkeyword;
01153         }
01154 
01155         return false;
01156     }
01157 
01163     public function getSearchEngine() {
01164         if ( $this->searchEngine === null ) {
01165             $this->searchEngine = $this->searchEngineType ?
01166                 SearchEngine::create( $this->searchEngineType ) : SearchEngine::create();
01167         }
01168 
01169         return $this->searchEngine;
01170     }
01171 
01176     function getProfile() {
01177         return $this->profile;
01178     }
01179 
01184     function getNamespaces() {
01185         return $this->namespaces;
01186     }
01187 
01197     public function setExtraParam( $key, $value ) {
01198         $this->extraParams[$key] = $value;
01199     }
01200 
01201     protected function getGroupName() {
01202         return 'pages';
01203     }
01204 }