MediaWiki  REL1_23
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         ) );
00087 
00088         // Strip underscores from title parameter; most of the time we'll want
00089         // text form here. But don't strip underscores from actual text params!
00090         $titleParam = str_replace( '_', ' ', $par );
00091 
00092         $request = $this->getRequest();
00093 
00094         // Fetch the search term
00095         $search = str_replace( "\n", " ", $request->getText( 'search', $titleParam ) );
00096 
00097         $this->load();
00098 
00099         $this->searchEngineType = $request->getVal( 'srbackend' );
00100 
00101         if ( $request->getVal( 'fulltext' )
00102             || !is_null( $request->getVal( 'offset' ) )
00103         ) {
00104             $this->showResults( $search );
00105         } else {
00106             $this->goResult( $search );
00107         }
00108     }
00109 
00115     public function load() {
00116         $request = $this->getRequest();
00117         list( $this->limit, $this->offset ) = $request->getLimitOffset( 20 );
00118         $this->mPrefix = $request->getVal( 'prefix', '' );
00119 
00120         $user = $this->getUser();
00121 
00122         # Extract manually requested namespaces
00123         $nslist = $this->powerSearch( $request );
00124         if ( !count( $nslist ) ) {
00125             # Fallback to user preference
00126             $nslist = SearchEngine::userNamespaces( $user );
00127         }
00128 
00129         $profile = null;
00130         if ( !count( $nslist ) ) {
00131             $profile = 'default';
00132         }
00133 
00134         $profile = $request->getVal( 'profile', $profile );
00135         $profiles = $this->getSearchProfiles();
00136         if ( $profile === null ) {
00137             // BC with old request format
00138             $profile = 'advanced';
00139             foreach ( $profiles as $key => $data ) {
00140                 if ( $nslist === $data['namespaces'] && $key !== 'advanced' ) {
00141                     $profile = $key;
00142                 }
00143             }
00144             $this->namespaces = $nslist;
00145         } elseif ( $profile === 'advanced' ) {
00146             $this->namespaces = $nslist;
00147         } else {
00148             if ( isset( $profiles[$profile]['namespaces'] ) ) {
00149                 $this->namespaces = $profiles[$profile]['namespaces'];
00150             } else {
00151                 // Unknown profile requested
00152                 $profile = 'default';
00153                 $this->namespaces = $profiles['default']['namespaces'];
00154             }
00155         }
00156 
00157         $this->didYouMeanHtml = ''; # html of did you mean... link
00158         $this->fulltext = $request->getVal( 'fulltext' );
00159         $this->profile = $profile;
00160     }
00161 
00167     public function goResult( $term ) {
00168         $this->setupPage( $term );
00169         # Try to go to page as entered.
00170         $title = Title::newFromText( $term );
00171         # If the string cannot be used to create a title
00172         if ( is_null( $title ) ) {
00173             $this->showResults( $term );
00174 
00175             return;
00176         }
00177         # If there's an exact or very near match, jump right there.
00178         $title = SearchEngine::getNearMatch( $term );
00179 
00180         if ( !wfRunHooks( 'SpecialSearchGo', array( &$title, &$term ) ) ) {
00181             # Hook requested termination
00182             return;
00183         }
00184 
00185         if ( !is_null( $title ) ) {
00186             $this->getOutput()->redirect( $title->getFullURL() );
00187 
00188             return;
00189         }
00190         # No match, generate an edit URL
00191         $title = Title::newFromText( $term );
00192         if ( !is_null( $title ) ) {
00193             global $wgGoToEdit;
00194             wfRunHooks( 'SpecialSearchNogomatch', array( &$title ) );
00195             wfDebugLog( 'nogomatch', $title->getFullText(), 'private' );
00196 
00197             # If the feature is enabled, go straight to the edit page
00198             if ( $wgGoToEdit ) {
00199                 $this->getOutput()->redirect( $title->getFullURL( array( 'action' => 'edit' ) ) );
00200 
00201                 return;
00202             }
00203         }
00204         $this->showResults( $term );
00205     }
00206 
00210     public function showResults( $term ) {
00211         global $wgDisableTextSearch, $wgSearchForwardUrl, $wgContLang, $wgScript;
00212 
00213         $profile = new ProfileSection( __METHOD__ );
00214         $search = $this->getSearchEngine();
00215         $search->setLimitOffset( $this->limit, $this->offset );
00216         $search->setNamespaces( $this->namespaces );
00217         $search->prefix = $this->mPrefix;
00218         $term = $search->transformSearchTerm( $term );
00219 
00220         wfRunHooks( 'SpecialSearchSetupEngine', array( $this, $this->profile, $search ) );
00221 
00222         $this->setupPage( $term );
00223 
00224         $out = $this->getOutput();
00225 
00226         if ( $wgDisableTextSearch ) {
00227             if ( $wgSearchForwardUrl ) {
00228                 $url = str_replace( '$1', urlencode( $term ), $wgSearchForwardUrl );
00229                 $out->redirect( $url );
00230             } else {
00231                 $out->addHTML(
00232                     Xml::openElement( 'fieldset' ) .
00233                     Xml::element( 'legend', null, $this->msg( 'search-external' )->text() ) .
00234                     Xml::element( 'p', array( 'class' => 'mw-searchdisabled' ), $this->msg( 'searchdisabled' )->text() ) .
00235                     $this->msg( 'googlesearch' )->rawParams(
00236                         htmlspecialchars( $term ),
00237                         'UTF-8',
00238                         $this->msg( 'searchbutton' )->escaped()
00239                     )->text() .
00240                     Xml::closeElement( 'fieldset' )
00241                 );
00242             }
00243 
00244             return;
00245         }
00246 
00247         $title = Title::newFromText( $term );
00248         $showSuggestion = $title === null || !$title->isKnown();
00249         $search->setShowSuggestion( $showSuggestion );
00250 
00251         // fetch search results
00252         $rewritten = $search->replacePrefixes( $term );
00253 
00254         $titleMatches = $search->searchTitle( $rewritten );
00255         if ( !( $titleMatches instanceof SearchResultTooMany ) ) {
00256             $textMatches = $search->searchText( $rewritten );
00257         }
00258 
00259         $textStatus = null;
00260         if ( $textMatches instanceof Status ) {
00261             $textStatus = $textMatches;
00262             $textMatches = null;
00263         }
00264 
00265         // did you mean... suggestions
00266         if ( $showSuggestion && $textMatches && !$textStatus && $textMatches->hasSuggestion() ) {
00267             $st = SpecialPage::getTitleFor( 'Search' );
00268 
00269             # mirror Go/Search behavior of original request ..
00270             $didYouMeanParams = array( 'search' => $textMatches->getSuggestionQuery() );
00271 
00272             if ( $this->fulltext != null ) {
00273                 $didYouMeanParams['fulltext'] = $this->fulltext;
00274             }
00275 
00276             $stParams = array_merge(
00277                 $didYouMeanParams,
00278                 $this->powerSearchOptions()
00279             );
00280 
00281             $suggestionSnippet = $textMatches->getSuggestionSnippet();
00282 
00283             if ( $suggestionSnippet == '' ) {
00284                 $suggestionSnippet = null;
00285             }
00286 
00287             $suggestLink = Linker::linkKnown(
00288                 $st,
00289                 $suggestionSnippet,
00290                 array(),
00291                 $stParams
00292             );
00293 
00294             $this->didYouMeanHtml = '<div class="searchdidyoumean">' . $this->msg( 'search-suggest' )->rawParams( $suggestLink )->text() . '</div>';
00295         }
00296 
00297         if ( !wfRunHooks( 'SpecialSearchResultsPrepend', array( $this, $out, $term ) ) ) {
00298             # Hook requested termination
00299             return;
00300         }
00301 
00302         // start rendering the page
00303         $out->addHtml(
00304             Xml::openElement(
00305                 'form',
00306                 array(
00307                     'id' => ( $this->profile === 'advanced' ? 'powersearch' : 'search' ),
00308                     'method' => 'get',
00309                     'action' => $wgScript
00310                 )
00311             )
00312         );
00313         $out->addHtml(
00314             # This is an awful awful ID name. It's not a table, but we
00315             # named it poorly from when this was a table so now we're
00316             # stuck with it
00317             Xml::openElement( 'div', array( 'id' => 'mw-search-top-table' ) ) .
00318             $this->shortDialog( $term ) .
00319             Xml::closeElement( 'div' )
00320         );
00321 
00322         // Sometimes the search engine knows there are too many hits
00323         if ( $titleMatches instanceof SearchResultTooMany ) {
00324             $out->wrapWikiMsg( "==$1==\n", 'toomanymatches' );
00325 
00326             return;
00327         }
00328 
00329         $filePrefix = $wgContLang->getFormattedNsText( NS_FILE ) . ':';
00330         if ( trim( $term ) === '' || $filePrefix === trim( $term ) ) {
00331             $out->addHTML( $this->formHeader( $term, 0, 0 ) );
00332             $out->addHtml( $this->getProfileForm( $this->profile, $term ) );
00333             $out->addHTML( '</form>' );
00334 
00335             // Empty query -- straight view of search form
00336             return;
00337         }
00338 
00339         // Get number of results
00340         $titleMatchesNum = $titleMatches ? $titleMatches->numRows() : 0;
00341         $textMatchesNum = $textMatches ? $textMatches->numRows() : 0;
00342         // Total initial query matches (possible false positives)
00343         $num = $titleMatchesNum + $textMatchesNum;
00344 
00345         // Get total actual results (after second filtering, if any)
00346         $numTitleMatches = $titleMatches && !is_null( $titleMatches->getTotalHits() ) ?
00347             $titleMatches->getTotalHits() : $titleMatchesNum;
00348         $numTextMatches = $textMatches && !is_null( $textMatches->getTotalHits() ) ?
00349             $textMatches->getTotalHits() : $textMatchesNum;
00350 
00351         // get total number of results if backend can calculate it
00352         $totalRes = 0;
00353         if ( $titleMatches && !is_null( $titleMatches->getTotalHits() ) ) {
00354             $totalRes += $titleMatches->getTotalHits();
00355         }
00356         if ( $textMatches && !is_null( $textMatches->getTotalHits() ) ) {
00357             $totalRes += $textMatches->getTotalHits();
00358         }
00359 
00360         // show number of results and current offset
00361         $out->addHTML( $this->formHeader( $term, $num, $totalRes ) );
00362         $out->addHtml( $this->getProfileForm( $this->profile, $term ) );
00363 
00364         $out->addHtml( Xml::closeElement( 'form' ) );
00365         $out->addHtml( "<div class='searchresults'>" );
00366 
00367         // prev/next links
00368         if ( $num || $this->offset ) {
00369             // Show the create link ahead
00370             $this->showCreateLink( $title, $num, $titleMatches, $textMatches );
00371             $prevnext = $this->getLanguage()->viewPrevNext( $this->getPageTitle(), $this->offset, $this->limit,
00372                 $this->powerSearchOptions() + array( 'search' => $term ),
00373                 max( $titleMatchesNum, $textMatchesNum ) < $this->limit
00374             );
00375             //$out->addHTML( "<p class='mw-search-pager-top'>{$prevnext}</p>\n" );
00376             wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) );
00377         } else {
00378             wfRunHooks( 'SpecialSearchNoResults', array( $term ) );
00379         }
00380 
00381         $out->parserOptions()->setEditSection( false );
00382         if ( $titleMatches ) {
00383             if ( $numTitleMatches > 0 ) {
00384                 $out->wrapWikiMsg( "==$1==\n", 'titlematches' );
00385                 $out->addHTML( $this->showMatches( $titleMatches ) );
00386             }
00387             $titleMatches->free();
00388         }
00389         if ( $textMatches && !$textStatus ) {
00390             // output appropriate heading
00391             if ( $numTextMatches > 0 && $numTitleMatches > 0 ) {
00392                 // if no title matches the heading is redundant
00393                 $out->wrapWikiMsg( "==$1==\n", 'textmatches' );
00394             } elseif ( $totalRes == 0 ) {
00395                 # Don't show the 'no text matches' if we received title matches
00396                 # $out->wrapWikiMsg( "==$1==\n", 'notextmatches' );
00397             }
00398             // show interwiki results if any
00399             if ( $textMatches->hasInterwikiResults() ) {
00400                 $out->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ) );
00401             }
00402             // show results
00403             if ( $numTextMatches > 0 ) {
00404                 $out->addHTML( $this->showMatches( $textMatches ) );
00405             }
00406 
00407             $textMatches->free();
00408         }
00409         if ( $num === 0 ) {
00410             if ( $textStatus ) {
00411                 $out->addHTML( '<div class="error">' .
00412                     htmlspecialchars( $textStatus->getWikiText( 'search-error' ) ) . '</div>' );
00413             } else {
00414                 $out->wrapWikiMsg( "<p class=\"mw-search-nonefound\">\n$1</p>",
00415                     array( 'search-nonefound', wfEscapeWikiText( $term ) ) );
00416                 $this->showCreateLink( $title, $num, $titleMatches, $textMatches );
00417             }
00418         }
00419         $out->addHtml( "</div>" );
00420 
00421         if ( $num || $this->offset ) {
00422             $out->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" );
00423         }
00424         wfRunHooks( 'SpecialSearchResultsAppend', array( $this, $out, $term ) );
00425     }
00426 
00433     protected function showCreateLink( $title, $num, $titleMatches, $textMatches ) {
00434         // show direct page/create link if applicable
00435 
00436         // Check DBkey !== '' in case of fragment link only.
00437         if ( is_null( $title ) || $title->getDBkey() === ''
00438             || ( $titleMatches !== null && $titleMatches->searchContainedSyntax() )
00439             || ( $textMatches !== null && $textMatches->searchContainedSyntax() )
00440         ) {
00441             // invalid title
00442             // preserve the paragraph for margins etc...
00443             $this->getOutput()->addHtml( '<p></p>' );
00444 
00445             return;
00446         }
00447 
00448         if ( $title->isKnown() ) {
00449             $messageName = 'searchmenu-exists';
00450         } elseif ( $title->userCan( 'create', $this->getUser() ) ) {
00451             $messageName = 'searchmenu-new';
00452         } else {
00453             $messageName = 'searchmenu-new-nocreate';
00454         }
00455         $params = array( $messageName, wfEscapeWikiText( $title->getPrefixedText() ), Message::numParam( $num ) );
00456         wfRunHooks( 'SpecialSearchCreateLink', array( $title, &$params ) );
00457 
00458         // Extensions using the hook might still return an empty $messageName
00459         if ( $messageName ) {
00460             $this->getOutput()->wrapWikiMsg( "<p class=\"mw-search-createlink\">\n$1</p>", $params );
00461         } else {
00462             // preserve the paragraph for margins etc...
00463             $this->getOutput()->addHtml( '<p></p>' );
00464         }
00465     }
00466 
00470     protected function setupPage( $term ) {
00471         # Should advanced UI be used?
00472         $this->searchAdvanced = ( $this->profile === 'advanced' );
00473         $out = $this->getOutput();
00474         if ( strval( $term ) !== '' ) {
00475             $out->setPageTitle( $this->msg( 'searchresults' ) );
00476             $out->setHTMLTitle( $this->msg( 'pagetitle' )
00477                 ->rawParams( $this->msg( 'searchresults-title' )->rawParams( $term )->text() )
00478                 ->inContentLanguage()->text()
00479             );
00480         }
00481         // add javascript specific to special:search
00482         $out->addModules( 'mediawiki.special.search' );
00483     }
00484 
00492     protected function powerSearch( &$request ) {
00493         $arr = array();
00494         foreach ( SearchEngine::searchableNamespaces() as $ns => $name ) {
00495             if ( $request->getCheck( 'ns' . $ns ) ) {
00496                 $arr[] = $ns;
00497             }
00498         }
00499 
00500         return $arr;
00501     }
00502 
00508     protected function powerSearchOptions() {
00509         $opt = array();
00510         if ( $this->profile !== 'advanced' ) {
00511             $opt['profile'] = $this->profile;
00512         } else {
00513             foreach ( $this->namespaces as $n ) {
00514                 $opt['ns' . $n] = 1;
00515             }
00516         }
00517 
00518         return $opt + $this->extraParams;
00519     }
00520 
00528     protected function showMatches( &$matches ) {
00529         global $wgContLang;
00530 
00531         $profile = new ProfileSection( __METHOD__ );
00532         $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
00533 
00534         $out = "<ul class='mw-search-results'>\n";
00535         $result = $matches->next();
00536         while ( $result ) {
00537             $out .= $this->showHit( $result, $terms );
00538             $result = $matches->next();
00539         }
00540         $out .= "</ul>\n";
00541 
00542         // convert the whole thing to desired language variant
00543         $out = $wgContLang->convert( $out );
00544 
00545         return $out;
00546     }
00547 
00556     protected function showHit( $result, $terms ) {
00557         $profile = new ProfileSection( __METHOD__ );
00558 
00559         if ( $result->isBrokenTitle() ) {
00560             return "<!-- Broken link in search result -->\n";
00561         }
00562 
00563         $title = $result->getTitle();
00564 
00565         $titleSnippet = $result->getTitleSnippet( $terms );
00566 
00567         if ( $titleSnippet == '' ) {
00568             $titleSnippet = null;
00569         }
00570 
00571         $link_t = clone $title;
00572 
00573         wfRunHooks( 'ShowSearchHitTitle',
00574             array( &$link_t, &$titleSnippet, $result, $terms, $this ) );
00575 
00576         $link = Linker::linkKnown(
00577             $link_t,
00578             $titleSnippet
00579         );
00580 
00581         //If page content is not readable, just return the title.
00582         //This is not quite safe, but better than showing excerpts from non-readable pages
00583         //Note that hiding the entry entirely would screw up paging.
00584         if ( !$title->userCan( 'read', $this->getUser() ) ) {
00585             return "<li>{$link}</li>\n";
00586         }
00587 
00588         // If the page doesn't *exist*... our search index is out of date.
00589         // The least confusing at this point is to drop the result.
00590         // You may get less results, but... oh well. :P
00591         if ( $result->isMissingRevision() ) {
00592             return "<!-- missing page " . htmlspecialchars( $title->getPrefixedText() ) . "-->\n";
00593         }
00594 
00595         // format redirects / relevant sections
00596         $redirectTitle = $result->getRedirectTitle();
00597         $redirectText = $result->getRedirectSnippet( $terms );
00598         $sectionTitle = $result->getSectionTitle();
00599         $sectionText = $result->getSectionSnippet( $terms );
00600         $redirect = '';
00601 
00602         if ( !is_null( $redirectTitle ) ) {
00603             if ( $redirectText == '' ) {
00604                 $redirectText = null;
00605             }
00606 
00607             $redirect = "<span class='searchalttitle'>" .
00608                 $this->msg( 'search-redirect' )->rawParams(
00609                     Linker::linkKnown( $redirectTitle, $redirectText ) )->text() .
00610                 "</span>";
00611         }
00612 
00613         $section = '';
00614 
00615         if ( !is_null( $sectionTitle ) ) {
00616             if ( $sectionText == '' ) {
00617                 $sectionText = null;
00618             }
00619 
00620             $section = "<span class='searchalttitle'>" .
00621                 $this->msg( 'search-section' )->rawParams(
00622                     Linker::linkKnown( $sectionTitle, $sectionText ) )->text() .
00623                 "</span>";
00624         }
00625 
00626         // format text extract
00627         $extract = "<div class='searchresult'>" . $result->getTextSnippet( $terms ) . "</div>";
00628 
00629         $lang = $this->getLanguage();
00630 
00631         // format score
00632         if ( is_null( $result->getScore() ) ) {
00633             // Search engine doesn't report scoring info
00634             $score = '';
00635         } else {
00636             $percent = sprintf( '%2.1f', $result->getScore() * 100 );
00637             $score = $this->msg( 'search-result-score' )->numParams( $percent )->text()
00638                 . ' - ';
00639         }
00640 
00641         // format description
00642         $byteSize = $result->getByteSize();
00643         $wordCount = $result->getWordCount();
00644         $timestamp = $result->getTimestamp();
00645         $size = $this->msg( 'search-result-size', $lang->formatSize( $byteSize ) )
00646             ->numParams( $wordCount )->escaped();
00647 
00648         if ( $title->getNamespace() == NS_CATEGORY ) {
00649             $cat = Category::newFromTitle( $title );
00650             $size = $this->msg( 'search-result-category-size' )
00651                 ->numParams( $cat->getPageCount(), $cat->getSubcatCount(), $cat->getFileCount() )
00652                 ->escaped();
00653         }
00654 
00655         $date = $lang->userTimeAndDate( $timestamp, $this->getUser() );
00656 
00657         // link to related articles if supported
00658         $related = '';
00659         if ( $result->hasRelated() ) {
00660             $st = SpecialPage::getTitleFor( 'Search' );
00661             $stParams = array_merge(
00662                 $this->powerSearchOptions(),
00663                 array(
00664                     'search' => $this->msg( 'searchrelated' )->inContentLanguage()->text() .
00665                         ':' . $title->getPrefixedText(),
00666                     'fulltext' => $this->msg( 'search' )->text()
00667                 )
00668             );
00669 
00670             $related = ' -- ' . Linker::linkKnown(
00671                 $st,
00672                 $this->msg( 'search-relatedarticle' )->text(),
00673                 array(),
00674                 $stParams
00675             );
00676         }
00677 
00678         $fileMatch = '';
00679         // Include a thumbnail for media files...
00680         if ( $title->getNamespace() == NS_FILE ) {
00681             $img = $result->getFile();
00682             $img = $img ?: wfFindFile( $title );
00683             if ( $result->isFileMatch() ) {
00684                 $fileMatch = "<span class='searchalttitle'>" .
00685                     $this->msg( 'search-file-match' )->escaped() . "</span>";
00686             }
00687             if ( $img ) {
00688                 $thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) );
00689                 if ( $thumb ) {
00690                     $desc = $this->msg( 'parentheses' )->rawParams( $img->getShortDesc() )->escaped();
00691                     // Float doesn't seem to interact well with the bullets.
00692                     // Table messes up vertical alignment of the bullets.
00693                     // Bullets are therefore disabled (didn't look great anyway).
00694                     return "<li>" .
00695                         '<table class="searchResultImage">' .
00696                         '<tr>' .
00697                         '<td style="width: 120px; text-align: center; vertical-align: top;">' .
00698                         $thumb->toHtml( array( 'desc-link' => true ) ) .
00699                         '</td>' .
00700                         '<td style="vertical-align: top;">' .
00701                         "{$link} {$fileMatch}" .
00702                         $extract .
00703                         "<div class='mw-search-result-data'>{$score}{$desc} - {$date}{$related}</div>" .
00704                         '</td>' .
00705                         '</tr>' .
00706                         '</table>' .
00707                         "</li>\n";
00708                 }
00709             }
00710         }
00711 
00712         $html = null;
00713 
00714         if ( wfRunHooks( 'ShowSearchHit', array(
00715             $this, $result, $terms,
00716             &$link, &$redirect, &$section, &$extract,
00717             &$score, &$size, &$date, &$related,
00718             &$html
00719         ) ) ) {
00720             $html = "<li><div class='mw-search-result-heading'>{$link} {$redirect} {$section} {$fileMatch}</div> {$extract}\n" .
00721                 "<div class='mw-search-result-data'>{$score}{$size} - {$date}{$related}</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         $customLines = explode( "\n", $this->msg( 'search-interwiki-custom' )->text() ); // format per line <iwprefix>:<caption>
00747         foreach ( $customLines as $line ) {
00748             $parts = explode( ":", $line, 2 );
00749             if ( count( $parts ) == 2 ) { // validate line
00750                 $customCaptions[$parts[0]] = $parts[1];
00751             }
00752         }
00753 
00754         if ( !is_array( $matches ) ) {
00755             $matches = array( $matches );
00756         }
00757 
00758         foreach ( $matches as $set ) {
00759             $prev = null;
00760             $result = $set->next();
00761             while ( $result ) {
00762                 $out .= $this->showInterwikiHit( $result, $prev, $query, $customCaptions );
00763                 $prev = $result->getInterwikiPrefix();
00764                 $result = $set->next();
00765             }
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 "<!-- Broken link in search result -->\n";
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 
00860     protected function getProfileForm( $profile, $term ) {
00861         // Hidden stuff
00862         $opts = array();
00863         $opts['profile'] = $this->profile;
00864 
00865         if ( $profile === 'advanced' ) {
00866             return $this->powerSearchBox( $term, $opts );
00867         } else {
00868             $form = '';
00869             wfRunHooks( 'SpecialSearchProfileForm', array( $this, &$form, $profile, $term, $opts ) );
00870 
00871             return $form;
00872         }
00873     }
00874 
00882     protected function powerSearchBox( $term, $opts ) {
00883         global $wgContLang;
00884 
00885         // Groups namespaces into rows according to subject
00886         $rows = array();
00887         foreach ( SearchEngine::searchableNamespaces() as $namespace => $name ) {
00888             $subject = MWNamespace::getSubject( $namespace );
00889             if ( !array_key_exists( $subject, $rows ) ) {
00890                 $rows[$subject] = "";
00891             }
00892 
00893             $name = $wgContLang->getConverter()->convertNamespace( $namespace );
00894             if ( $name == '' ) {
00895                 $name = $this->msg( 'blanknamespace' )->text();
00896             }
00897 
00898             $rows[$subject] .=
00899                 Xml::openElement(
00900                     'td', array( 'style' => 'white-space: nowrap' )
00901                 ) .
00902                 Xml::checkLabel(
00903                     $name,
00904                     "ns{$namespace}",
00905                     "mw-search-ns{$namespace}",
00906                     in_array( $namespace, $this->namespaces )
00907                 ) .
00908                 Xml::closeElement( 'td' );
00909         }
00910 
00911         $rows = array_values( $rows );
00912         $numRows = count( $rows );
00913 
00914         // Lays out namespaces in multiple floating two-column tables so they'll
00915         // be arranged nicely while still accommodating different screen widths
00916         $namespaceTables = '';
00917         for ( $i = 0; $i < $numRows; $i += 4 ) {
00918             $namespaceTables .= Xml::openElement(
00919                 'table',
00920                 array( 'cellpadding' => 0, 'cellspacing' => 0 )
00921             );
00922 
00923             for ( $j = $i; $j < $i + 4 && $j < $numRows; $j++ ) {
00924                 $namespaceTables .= Xml::tags( 'tr', null, $rows[$j] );
00925             }
00926 
00927             $namespaceTables .= Xml::closeElement( 'table' );
00928         }
00929 
00930         $showSections = array( 'namespaceTables' => $namespaceTables );
00931 
00932         wfRunHooks( 'SpecialSearchPowerBox', array( &$showSections, $term, $opts ) );
00933 
00934         $hidden = '';
00935         foreach ( $opts as $key => $value ) {
00936             $hidden .= Html::hidden( $key, $value );
00937         }
00938 
00939         // Return final output
00940         return Xml::openElement(
00941             'fieldset',
00942             array( 'id' => 'mw-searchoptions', 'style' => 'margin:0em;' )
00943         ) .
00944             Xml::element( 'legend', null, $this->msg( 'powersearch-legend' )->text() ) .
00945             Xml::tags( 'h4', null, $this->msg( 'powersearch-ns' )->parse() ) .
00946             Html::element( 'div', array( 'id' => 'mw-search-togglebox' ) ) .
00947             Xml::element( 'div', array( 'class' => 'divider' ), '', false ) .
00948             implode( Xml::element( 'div', array( 'class' => 'divider' ), '', false ), $showSections ) .
00949             $hidden .
00950             Xml::closeElement( 'fieldset' );
00951     }
00952 
00956     protected function getSearchProfiles() {
00957         // Builds list of Search Types (profiles)
00958         $nsAllSet = array_keys( SearchEngine::searchableNamespaces() );
00959 
00960         $profiles = array(
00961             'default' => array(
00962                 'message' => 'searchprofile-articles',
00963                 'tooltip' => 'searchprofile-articles-tooltip',
00964                 'namespaces' => SearchEngine::defaultNamespaces(),
00965                 'namespace-messages' => SearchEngine::namespacesAsText(
00966                     SearchEngine::defaultNamespaces()
00967                 ),
00968             ),
00969             'images' => array(
00970                 'message' => 'searchprofile-images',
00971                 'tooltip' => 'searchprofile-images-tooltip',
00972                 'namespaces' => array( NS_FILE ),
00973             ),
00974             'help' => array(
00975                 'message' => 'searchprofile-project',
00976                 'tooltip' => 'searchprofile-project-tooltip',
00977                 'namespaces' => SearchEngine::helpNamespaces(),
00978                 'namespace-messages' => SearchEngine::namespacesAsText(
00979                     SearchEngine::helpNamespaces()
00980                 ),
00981             ),
00982             'all' => array(
00983                 'message' => 'searchprofile-everything',
00984                 'tooltip' => 'searchprofile-everything-tooltip',
00985                 'namespaces' => $nsAllSet,
00986             ),
00987             'advanced' => array(
00988                 'message' => 'searchprofile-advanced',
00989                 'tooltip' => 'searchprofile-advanced-tooltip',
00990                 'namespaces' => self::NAMESPACES_CURRENT,
00991             )
00992         );
00993 
00994         wfRunHooks( 'SpecialSearchProfiles', array( &$profiles ) );
00995 
00996         foreach ( $profiles as &$data ) {
00997             if ( !is_array( $data['namespaces'] ) ) {
00998                 continue;
00999             }
01000             sort( $data['namespaces'] );
01001         }
01002 
01003         return $profiles;
01004     }
01005 
01012     protected function formHeader( $term, $resultsShown, $totalNum ) {
01013         $out = Xml::openElement( 'div', array( 'class' => 'mw-search-formheader' ) );
01014 
01015         $bareterm = $term;
01016         if ( $this->startsWithImage( $term ) ) {
01017             // Deletes prefixes
01018             $bareterm = substr( $term, strpos( $term, ':' ) + 1 );
01019         }
01020 
01021         $profiles = $this->getSearchProfiles();
01022         $lang = $this->getLanguage();
01023 
01024         // Outputs XML for Search Types
01025         $out .= Xml::openElement( 'div', array( 'class' => 'search-types' ) );
01026         $out .= Xml::openElement( 'ul' );
01027         foreach ( $profiles as $id => $profile ) {
01028             if ( !isset( $profile['parameters'] ) ) {
01029                 $profile['parameters'] = array();
01030             }
01031             $profile['parameters']['profile'] = $id;
01032 
01033             $tooltipParam = isset( $profile['namespace-messages'] ) ?
01034                 $lang->commaList( $profile['namespace-messages'] ) : null;
01035             $out .= Xml::tags(
01036                 'li',
01037                 array(
01038                     'class' => $this->profile === $id ? 'current' : 'normal'
01039                 ),
01040                 $this->makeSearchLink(
01041                     $bareterm,
01042                     array(),
01043                     $this->msg( $profile['message'] )->text(),
01044                     $this->msg( $profile['tooltip'], $tooltipParam )->text(),
01045                     $profile['parameters']
01046                 )
01047             );
01048         }
01049         $out .= Xml::closeElement( 'ul' );
01050         $out .= Xml::closeElement( 'div' );
01051 
01052         // Results-info
01053         if ( $resultsShown > 0 ) {
01054             if ( $totalNum > 0 ) {
01055                 $top = $this->msg( 'showingresultsheader' )
01056                     ->numParams( $this->offset + 1, $this->offset + $resultsShown, $totalNum )
01057                     ->params( wfEscapeWikiText( $term ) )
01058                     ->numParams( $resultsShown )
01059                     ->parse();
01060             } elseif ( $resultsShown >= $this->limit ) {
01061                 $top = $this->msg( 'showingresults' )
01062                     ->numParams( $this->limit, $this->offset + 1 )
01063                     ->parse();
01064             } else {
01065                 $top = $this->msg( 'showingresultsnum' )
01066                     ->numParams( $this->limit, $this->offset + 1, $resultsShown )
01067                     ->parse();
01068             }
01069             $out .= Xml::tags( 'div', array( 'class' => 'results-info' ),
01070                 Xml::tags( 'ul', null, Xml::tags( 'li', null, $top ) )
01071             );
01072         }
01073 
01074         $out .= Xml::element( 'div', array( 'style' => 'clear:both' ), '', false );
01075         $out .= Xml::closeElement( 'div' );
01076 
01077         return $out;
01078     }
01079 
01084     protected function shortDialog( $term ) {
01085         $out = Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() );
01086         $out .= Html::hidden( 'profile', $this->profile ) . "\n";
01087         // Term box
01088         $out .= Html::input( 'search', $term, 'search', array(
01089             'id' => $this->profile === 'advanced' ? 'powerSearchText' : 'searchText',
01090             'size' => '50',
01091             'autofocus',
01092             'class' => 'mw-ui-input',
01093         ) ) . "\n";
01094         $out .= Html::hidden( 'fulltext', 'Search' ) . "\n";
01095         $out .= Xml::submitButton(
01096             $this->msg( 'searchbutton' )->text(),
01097             array( 'class' => array( 'mw-ui-button', 'mw-ui-progressive' ) )
01098         ) . "\n";
01099 
01100         return $out . $this->didYouMeanHtml;
01101     }
01102 
01113     protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params = array() ) {
01114         $opt = $params;
01115         foreach ( $namespaces as $n ) {
01116             $opt['ns' . $n] = 1;
01117         }
01118 
01119         $stParams = array_merge(
01120             array(
01121                 'search' => $term,
01122                 'fulltext' => $this->msg( 'search' )->text()
01123             ),
01124             $opt
01125         );
01126 
01127         return Xml::element(
01128             'a',
01129             array(
01130                 'href' => $this->getPageTitle()->getLocalURL( $stParams ),
01131                 'title' => $tooltip
01132             ),
01133             $label
01134         );
01135     }
01136 
01143     protected function startsWithImage( $term ) {
01144         global $wgContLang;
01145 
01146         $parts = explode( ':', $term );
01147         if ( count( $parts ) > 1 ) {
01148             return $wgContLang->getNsIndex( $parts[0] ) == NS_FILE;
01149         }
01150 
01151         return false;
01152     }
01153 
01160     protected function startsWithAll( $term ) {
01161 
01162         $allkeyword = $this->msg( 'searchall' )->inContentLanguage()->text();
01163 
01164         $parts = explode( ':', $term );
01165         if ( count( $parts ) > 1 ) {
01166             return $parts[0] == $allkeyword;
01167         }
01168 
01169         return false;
01170     }
01171 
01177     public function getSearchEngine() {
01178         if ( $this->searchEngine === null ) {
01179             $this->searchEngine = $this->searchEngineType ?
01180                 SearchEngine::create( $this->searchEngineType ) : SearchEngine::create();
01181         }
01182 
01183         return $this->searchEngine;
01184     }
01185 
01190     function getProfile() {
01191         return $this->profile;
01192     }
01193 
01198     function getNamespaces() {
01199         return $this->namespaces;
01200     }
01201 
01211     public function setExtraParam( $key, $value ) {
01212         $this->extraParams[$key] = $value;
01213     }
01214 
01215     protected function getGroupName() {
01216         return 'pages';
01217     }
01218 }