MediaWiki  REL1_22
SpecialSearch.php
Go to the documentation of this file.
00001 <?php
00030 class SpecialSearch extends SpecialPage {
00039     protected $profile;
00040     function getProfile() { return $this->profile; }
00041 
00043     protected $searchEngine;
00044 
00046     protected $searchEngineType;
00047 
00049     protected $extraParams = array();
00050 
00052     protected $mPrefix;
00053 
00057     protected $limit, $offset;
00058 
00062     protected $namespaces;
00063     function getNamespaces() { return $this->namespaces; }
00064 
00068     protected $searchRedirects;
00069 
00073     protected $didYouMeanHtml, $fulltext;
00074 
00075     const NAMESPACES_CURRENT = 'sense';
00076 
00077     public function __construct() {
00078         parent::__construct( 'Search' );
00079     }
00080 
00086     public function execute( $par ) {
00087         $this->setHeaders();
00088         $this->outputHeader();
00089         $out = $this->getOutput();
00090         $out->allowClickjacking();
00091         $out->addModuleStyles( 'mediawiki.special' );
00092 
00093         // Strip underscores from title parameter; most of the time we'll want
00094         // text form here. But don't strip underscores from actual text params!
00095         $titleParam = str_replace( '_', ' ', $par );
00096 
00097         $request = $this->getRequest();
00098 
00099         // Fetch the search term
00100         $search = str_replace( "\n", " ", $request->getText( 'search', $titleParam ) );
00101 
00102         $this->load();
00103 
00104         $this->searchEngineType = $request->getVal( 'srbackend' );
00105 
00106         if ( $request->getVal( 'fulltext' )
00107             || !is_null( $request->getVal( 'offset' ) )
00108             || !is_null( $request->getVal( 'searchx' ) ) )
00109         {
00110             $this->showResults( $search );
00111         } else {
00112             $this->goResult( $search );
00113         }
00114     }
00115 
00121     public function load() {
00122         $request = $this->getRequest();
00123         list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' );
00124         $this->mPrefix = $request->getVal( 'prefix', '' );
00125 
00126         $user = $this->getUser();
00127 
00128         # Extract manually requested namespaces
00129         $nslist = $this->powerSearch( $request );
00130         if ( !count( $nslist ) ) {
00131             # Fallback to user preference
00132             $nslist = SearchEngine::userNamespaces( $user );
00133         }
00134 
00135         $profile = null;
00136         if ( !count( $nslist ) ) {
00137             $profile = 'default';
00138         }
00139 
00140         $profile = $request->getVal( 'profile', $profile );
00141         $profiles = $this->getSearchProfiles();
00142         if ( $profile === null ) {
00143             // BC with old request format
00144             $profile = 'advanced';
00145             foreach ( $profiles as $key => $data ) {
00146                 if ( $nslist === $data['namespaces'] && $key !== 'advanced' ) {
00147                     $profile = $key;
00148                 }
00149             }
00150             $this->namespaces = $nslist;
00151         } elseif ( $profile === 'advanced' ) {
00152             $this->namespaces = $nslist;
00153         } else {
00154             if ( isset( $profiles[$profile]['namespaces'] ) ) {
00155                 $this->namespaces = $profiles[$profile]['namespaces'];
00156             } else {
00157                 // Unknown profile requested
00158                 $profile = 'default';
00159                 $this->namespaces = $profiles['default']['namespaces'];
00160             }
00161         }
00162 
00163         // Redirects defaults to true, but we don't know whether it was ticked of or just missing
00164         $default = $request->getBool( 'profile' ) ? 0 : 1;
00165         $this->searchRedirects = $request->getBool( 'redirs', $default ) ? 1 : 0;
00166         $this->didYouMeanHtml = ''; # html of did you mean... link
00167         $this->fulltext = $request->getVal( 'fulltext' );
00168         $this->profile = $profile;
00169     }
00170 
00176     public function goResult( $term ) {
00177         $this->setupPage( $term );
00178         # Try to go to page as entered.
00179         $t = Title::newFromText( $term );
00180         # If the string cannot be used to create a title
00181         if ( is_null( $t ) ) {
00182             $this->showResults( $term );
00183             return;
00184         }
00185         # If there's an exact or very near match, jump right there.
00186         $t = SearchEngine::getNearMatch( $term );
00187 
00188         if ( !wfRunHooks( 'SpecialSearchGo', array( &$t, &$term ) ) ) {
00189             # Hook requested termination
00190             return;
00191         }
00192 
00193         if ( !is_null( $t ) ) {
00194             $this->getOutput()->redirect( $t->getFullURL() );
00195             return;
00196         }
00197         # No match, generate an edit URL
00198         $t = Title::newFromText( $term );
00199         if ( !is_null( $t ) ) {
00200             global $wgGoToEdit;
00201             wfRunHooks( 'SpecialSearchNogomatch', array( &$t ) );
00202             wfDebugLog( 'nogomatch', $t->getText(), false );
00203 
00204             # If the feature is enabled, go straight to the edit page
00205             if ( $wgGoToEdit ) {
00206                 $this->getOutput()->redirect( $t->getFullURL( array( 'action' => 'edit' ) ) );
00207                 return;
00208             }
00209         }
00210         $this->showResults( $term );
00211     }
00212 
00216     public function showResults( $term ) {
00217         global $wgDisableTextSearch, $wgSearchForwardUrl, $wgContLang, $wgScript;
00218         wfProfileIn( __METHOD__ );
00219 
00220         $search = $this->getSearchEngine();
00221         $search->setLimitOffset( $this->limit, $this->offset );
00222         $search->setNamespaces( $this->namespaces );
00223         $search->showRedirects = $this->searchRedirects; // BC
00224         $search->setFeatureData( 'list-redirects', $this->searchRedirects );
00225         $search->prefix = $this->mPrefix;
00226         $term = $search->transformSearchTerm( $term );
00227 
00228         wfRunHooks( 'SpecialSearchSetupEngine', array( $this, $this->profile, $search ) );
00229 
00230         $this->setupPage( $term );
00231 
00232         $out = $this->getOutput();
00233 
00234         if ( $wgDisableTextSearch ) {
00235             if ( $wgSearchForwardUrl ) {
00236                 $url = str_replace( '$1', urlencode( $term ), $wgSearchForwardUrl );
00237                 $out->redirect( $url );
00238             } else {
00239                 $out->addHTML(
00240                     Xml::openElement( 'fieldset' ) .
00241                     Xml::element( 'legend', null, $this->msg( 'search-external' )->text() ) .
00242                     Xml::element( 'p', array( 'class' => 'mw-searchdisabled' ), $this->msg( 'searchdisabled' )->text() ) .
00243                     $this->msg( 'googlesearch' )->rawParams(
00244                         htmlspecialchars( $term ),
00245                         'UTF-8',
00246                         $this->msg( 'searchbutton' )->escaped()
00247                     )->text() .
00248                     Xml::closeElement( 'fieldset' )
00249                 );
00250             }
00251             wfProfileOut( __METHOD__ );
00252             return;
00253         }
00254 
00255         $t = Title::newFromText( $term );
00256 
00257         // fetch search results
00258         $rewritten = $search->replacePrefixes( $term );
00259 
00260         $titleMatches = $search->searchTitle( $rewritten );
00261         if ( !( $titleMatches instanceof SearchResultTooMany ) ) {
00262             $textMatches = $search->searchText( $rewritten );
00263         }
00264 
00265         $textStatus = null;
00266         if ( $textMatches instanceof Status ) {
00267             $textStatus = $textMatches;
00268             $textMatches = null;
00269         }
00270 
00271         // did you mean... suggestions
00272         if ( $textMatches && !$textStatus && $textMatches->hasSuggestion() ) {
00273             $st = SpecialPage::getTitleFor( 'Search' );
00274 
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                 $st,
00295                 $suggestionSnippet,
00296                 array(),
00297                 $stParams
00298             );
00299 
00300             $this->didYouMeanHtml = '<div class="searchdidyoumean">' . $this->msg( 'search-suggest' )->rawParams( $suggestLink )->text() . '</div>';
00301         }
00302 
00303         if ( !wfRunHooks( 'SpecialSearchResultsPrepend', array( $this, $out, $term ) ) ) {
00304             # Hook requested termination
00305             wfProfileOut( __METHOD__ );
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' => $wgScript
00317                 )
00318             )
00319         );
00320         $out->addHtml(
00321             Xml::openElement( 'table', array( 'id' => 'mw-search-top-table', 'cellpadding' => 0, 'cellspacing' => 0 ) ) .
00322             Xml::openElement( 'tr' ) .
00323             Xml::openElement( 'td' ) . "\n" .
00324             $this->shortDialog( $term ) .
00325             Xml::closeElement( 'td' ) .
00326             Xml::closeElement( 'tr' ) .
00327             Xml::closeElement( 'table' )
00328         );
00329 
00330         // Sometimes the search engine knows there are too many hits
00331         if ( $titleMatches instanceof SearchResultTooMany ) {
00332             $out->wrapWikiMsg( "==$1==\n", 'toomanymatches' );
00333             wfProfileOut( __METHOD__ );
00334             return;
00335         }
00336 
00337         $filePrefix = $wgContLang->getFormattedNsText( NS_FILE ) . ':';
00338         if ( trim( $term ) === '' || $filePrefix === trim( $term ) ) {
00339             $out->addHTML( $this->formHeader( $term, 0, 0 ) );
00340             $out->addHtml( $this->getProfileForm( $this->profile, $term ) );
00341             $out->addHTML( '</form>' );
00342             // Empty query -- straight view of search form
00343             wfProfileOut( __METHOD__ );
00344             return;
00345         }
00346 
00347         // Get number of results
00348         $titleMatchesNum = $titleMatches ? $titleMatches->numRows() : 0;
00349         $textMatchesNum = $textMatches ? $textMatches->numRows() : 0;
00350         // Total initial query matches (possible false positives)
00351         $num = $titleMatchesNum + $textMatchesNum;
00352 
00353         // Get total actual results (after second filtering, if any)
00354         $numTitleMatches = $titleMatches && !is_null( $titleMatches->getTotalHits() ) ?
00355             $titleMatches->getTotalHits() : $titleMatchesNum;
00356         $numTextMatches = $textMatches && !is_null( $textMatches->getTotalHits() ) ?
00357             $textMatches->getTotalHits() : $textMatchesNum;
00358 
00359         // get total number of results if backend can calculate it
00360         $totalRes = 0;
00361         if ( $titleMatches && !is_null( $titleMatches->getTotalHits() ) ) {
00362             $totalRes += $titleMatches->getTotalHits();
00363         }
00364         if ( $textMatches && !is_null( $textMatches->getTotalHits() ) ) {
00365             $totalRes += $textMatches->getTotalHits();
00366         }
00367 
00368         // show number of results and current offset
00369         $out->addHTML( $this->formHeader( $term, $num, $totalRes ) );
00370         $out->addHtml( $this->getProfileForm( $this->profile, $term ) );
00371 
00372         $out->addHtml( Xml::closeElement( 'form' ) );
00373         $out->addHtml( "<div class='searchresults'>" );
00374 
00375         // prev/next links
00376         if ( $num || $this->offset ) {
00377             // Show the create link ahead
00378             $this->showCreateLink( $t );
00379             $prevnext = $this->getLanguage()->viewPrevNext( $this->getTitle(), $this->offset, $this->limit,
00380                 $this->powerSearchOptions() + array( 'search' => $term ),
00381                 max( $titleMatchesNum, $textMatchesNum ) < $this->limit
00382             );
00383             //$out->addHTML( "<p class='mw-search-pager-top'>{$prevnext}</p>\n" );
00384             wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) );
00385         } else {
00386             wfRunHooks( 'SpecialSearchNoResults', array( $term ) );
00387         }
00388 
00389         $out->parserOptions()->setEditSection( false );
00390         if ( $titleMatches ) {
00391             if ( $numTitleMatches > 0 ) {
00392                 $out->wrapWikiMsg( "==$1==\n", 'titlematches' );
00393                 $out->addHTML( $this->showMatches( $titleMatches ) );
00394             }
00395             $titleMatches->free();
00396         }
00397         if ( $textMatches && !$textStatus ) {
00398             // output appropriate heading
00399             if ( $numTextMatches > 0 && $numTitleMatches > 0 ) {
00400                 // if no title matches the heading is redundant
00401                 $out->wrapWikiMsg( "==$1==\n", 'textmatches' );
00402             } elseif ( $totalRes == 0 ) {
00403                 # Don't show the 'no text matches' if we received title matches
00404                 # $out->wrapWikiMsg( "==$1==\n", 'notextmatches' );
00405             }
00406             // show interwiki results if any
00407             if ( $textMatches->hasInterwikiResults() ) {
00408                 $out->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ) );
00409             }
00410             // show results
00411             if ( $numTextMatches > 0 ) {
00412                 $out->addHTML( $this->showMatches( $textMatches ) );
00413             }
00414 
00415             $textMatches->free();
00416         }
00417         if ( $num === 0 ) {
00418             if ( $textStatus ) {
00419                 $out->addHTML( '<div class="error">' .
00420                     htmlspecialchars( $textStatus->getWikiText( 'search-error' ) ) . '</div>' );
00421             } else {
00422                 $out->wrapWikiMsg( "<p class=\"mw-search-nonefound\">\n$1</p>",
00423                     array( 'search-nonefound', wfEscapeWikiText( $term ) ) );
00424                 $this->showCreateLink( $t );
00425             }
00426         }
00427         $out->addHtml( "</div>" );
00428 
00429         if ( $num || $this->offset ) {
00430             $out->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" );
00431         }
00432         wfRunHooks( 'SpecialSearchResultsAppend', array( $this, $out, $term ) );
00433         wfProfileOut( __METHOD__ );
00434     }
00435 
00439     protected function showCreateLink( $t ) {
00440         // show direct page/create link if applicable
00441 
00442         // Check DBkey !== '' in case of fragment link only.
00443         if ( is_null( $t ) || $t->getDBkey() === '' ) {
00444             // invalid title
00445             // preserve the paragraph for margins etc...
00446             $this->getOutput()->addHtml( '<p></p>' );
00447             return;
00448         }
00449 
00450         if ( $t->isKnown() ) {
00451             $messageName = 'searchmenu-exists';
00452         } elseif ( $t->userCan( 'create', $this->getUser() ) ) {
00453             $messageName = 'searchmenu-new';
00454         } else {
00455             $messageName = 'searchmenu-new-nocreate';
00456         }
00457         $params = array( $messageName, wfEscapeWikiText( $t->getPrefixedText() ) );
00458         wfRunHooks( 'SpecialSearchCreateLink', array( $t, &$params ) );
00459 
00460         // Extensions using the hook might still return an empty $messageName
00461         if ( $messageName ) {
00462             $this->getOutput()->wrapWikiMsg( "<p class=\"mw-search-createlink\">\n$1</p>", $params );
00463         } else {
00464             // preserve the paragraph for margins etc...
00465             $this->getOutput()->addHtml( '<p></p>' );
00466         }
00467     }
00468 
00472     protected function setupPage( $term ) {
00473         # Should advanced UI be used?
00474         $this->searchAdvanced = ( $this->profile === 'advanced' );
00475         $out = $this->getOutput();
00476         if ( strval( $term ) !== '' ) {
00477             $out->setPageTitle( $this->msg( 'searchresults' ) );
00478             $out->setHTMLTitle( $this->msg( 'pagetitle' )->rawParams(
00479                 $this->msg( 'searchresults-title' )->rawParams( $term )->text()
00480             ) );
00481         }
00482         // add javascript specific to special:search
00483         $out->addModules( 'mediawiki.special.search' );
00484     }
00485 
00493     protected function powerSearch( &$request ) {
00494         $arr = array();
00495         foreach ( SearchEngine::searchableNamespaces() as $ns => $name ) {
00496             if ( $request->getCheck( 'ns' . $ns ) ) {
00497                 $arr[] = $ns;
00498             }
00499         }
00500 
00501         return $arr;
00502     }
00503 
00509     protected function powerSearchOptions() {
00510         $opt = array();
00511         $opt['redirs'] = $this->searchRedirects ? 1 : 0;
00512         if ( $this->profile !== 'advanced' ) {
00513             $opt['profile'] = $this->profile;
00514         } else {
00515             foreach ( $this->namespaces as $n ) {
00516                 $opt['ns' . $n] = 1;
00517             }
00518         }
00519         return $opt + $this->extraParams;
00520     }
00521 
00529     protected function showMatches( &$matches ) {
00530         global $wgContLang;
00531         wfProfileIn( __METHOD__ );
00532 
00533         $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
00534 
00535         $out = "";
00536         $infoLine = $matches->getInfo();
00537         if ( !is_null( $infoLine ) ) {
00538             $out .= "\n<!-- {$infoLine} -->\n";
00539         }
00540         $out .= "<ul class='mw-search-results'>\n";
00541         $result = $matches->next();
00542         while ( $result ) {
00543             $out .= $this->showHit( $result, $terms );
00544             $result = $matches->next();
00545         }
00546         $out .= "</ul>\n";
00547 
00548         // convert the whole thing to desired language variant
00549         $out = $wgContLang->convert( $out );
00550         wfProfileOut( __METHOD__ );
00551         return $out;
00552     }
00553 
00562     protected function showHit( $result, $terms ) {
00563         wfProfileIn( __METHOD__ );
00564 
00565         if ( $result->isBrokenTitle() ) {
00566             wfProfileOut( __METHOD__ );
00567             return "<!-- Broken link in search result -->\n";
00568         }
00569 
00570         $t = $result->getTitle();
00571 
00572         $titleSnippet = $result->getTitleSnippet( $terms );
00573 
00574         if ( $titleSnippet == '' ) {
00575             $titleSnippet = null;
00576         }
00577 
00578         $link_t = clone $t;
00579 
00580         wfRunHooks( 'ShowSearchHitTitle',
00581                     array( &$link_t, &$titleSnippet, $result, $terms, $this ) );
00582 
00583         $link = Linker::linkKnown(
00584             $link_t,
00585             $titleSnippet
00586         );
00587 
00588         //If page content is not readable, just return the title.
00589         //This is not quite safe, but better than showing excerpts from non-readable pages
00590         //Note that hiding the entry entirely would screw up paging.
00591         if ( !$t->userCan( 'read', $this->getUser() ) ) {
00592             wfProfileOut( __METHOD__ );
00593             return "<li>{$link}</li>\n";
00594         }
00595 
00596         // If the page doesn't *exist*... our search index is out of date.
00597         // The least confusing at this point is to drop the result.
00598         // You may get less results, but... oh well. :P
00599         if ( $result->isMissingRevision() ) {
00600             wfProfileOut( __METHOD__ );
00601             return "<!-- missing page " . htmlspecialchars( $t->getPrefixedText() ) . "-->\n";
00602         }
00603 
00604         // format redirects / relevant sections
00605         $redirectTitle = $result->getRedirectTitle();
00606         $redirectText = $result->getRedirectSnippet( $terms );
00607         $sectionTitle = $result->getSectionTitle();
00608         $sectionText = $result->getSectionSnippet( $terms );
00609         $redirect = '';
00610 
00611         if ( !is_null( $redirectTitle ) ) {
00612             if ( $redirectText == '' ) {
00613                 $redirectText = null;
00614             }
00615 
00616             $redirect = "<span class='searchalttitle'>" .
00617                 $this->msg( 'search-redirect' )->rawParams(
00618                     Linker::linkKnown( $redirectTitle, $redirectText ) )->text() .
00619                 "</span>";
00620         }
00621 
00622         $section = '';
00623 
00624         if ( !is_null( $sectionTitle ) ) {
00625             if ( $sectionText == '' ) {
00626                 $sectionText = null;
00627             }
00628 
00629             $section = "<span class='searchalttitle'>" .
00630                 $this->msg( 'search-section' )->rawParams(
00631                     Linker::linkKnown( $sectionTitle, $sectionText ) )->text() .
00632                 "</span>";
00633         }
00634 
00635         // format text extract
00636         $extract = "<div class='searchresult'>" . $result->getTextSnippet( $terms ) . "</div>";
00637 
00638         $lang = $this->getLanguage();
00639 
00640         // format score
00641         if ( is_null( $result->getScore() ) ) {
00642             // Search engine doesn't report scoring info
00643             $score = '';
00644         } else {
00645             $percent = sprintf( '%2.1f', $result->getScore() * 100 );
00646             $score = $this->msg( 'search-result-score' )->numParams( $percent )->text()
00647                 . ' - ';
00648         }
00649 
00650         // format description
00651         $byteSize = $result->getByteSize();
00652         $wordCount = $result->getWordCount();
00653         $timestamp = $result->getTimestamp();
00654         $size = $this->msg( 'search-result-size', $lang->formatSize( $byteSize ) )
00655             ->numParams( $wordCount )->escaped();
00656 
00657         if ( $t->getNamespace() == NS_CATEGORY ) {
00658             $cat = Category::newFromTitle( $t );
00659             $size = $this->msg( 'search-result-category-size' )
00660                 ->numParams( $cat->getPageCount(), $cat->getSubcatCount(), $cat->getFileCount() )
00661                 ->escaped();
00662         }
00663 
00664         $date = $lang->userTimeAndDate( $timestamp, $this->getUser() );
00665 
00666         // link to related articles if supported
00667         $related = '';
00668         if ( $result->hasRelated() ) {
00669             $st = SpecialPage::getTitleFor( 'Search' );
00670             $stParams = array_merge(
00671                 $this->powerSearchOptions(),
00672                 array(
00673                     'search' => $this->msg( 'searchrelated' )->inContentLanguage()->text() .
00674                         ':' . $t->getPrefixedText(),
00675                     'fulltext' => $this->msg( 'search' )->text()
00676                 )
00677             );
00678 
00679             $related = ' -- ' . Linker::linkKnown(
00680                 $st,
00681                 $this->msg( 'search-relatedarticle' )->text(),
00682                 array(),
00683                 $stParams
00684             );
00685         }
00686 
00687         // Include a thumbnail for media files...
00688         if ( $t->getNamespace() == NS_FILE ) {
00689             $img = wfFindFile( $t );
00690             if ( $img ) {
00691                 $thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) );
00692                 if ( $thumb ) {
00693                     $desc = $this->msg( 'parentheses' )->rawParams( $img->getShortDesc() )->escaped();
00694                     wfProfileOut( __METHOD__ );
00695                     // Float doesn't seem to interact well with the bullets.
00696                     // Table messes up vertical alignment of the bullets.
00697                     // Bullets are therefore disabled (didn't look great anyway).
00698                     return "<li>" .
00699                         '<table class="searchResultImage">' .
00700                         '<tr>' .
00701                         '<td style="width: 120px; text-align: center; vertical-align: top;">' .
00702                         $thumb->toHtml( array( 'desc-link' => true ) ) .
00703                         '</td>' .
00704                         '<td style="vertical-align: top;">' .
00705                         $link .
00706                         $extract .
00707                         "<div class='mw-search-result-data'>{$score}{$desc} - {$date}{$related}</div>" .
00708                         '</td>' .
00709                         '</tr>' .
00710                         '</table>' .
00711                         "</li>\n";
00712                 }
00713             }
00714         }
00715 
00716         $html = null;
00717 
00718         if ( wfRunHooks( 'ShowSearchHit', array(
00719             $this, $result, $terms,
00720             &$link, &$redirect, &$section, &$extract,
00721             &$score, &$size, &$date, &$related,
00722             &$html
00723         ) ) ) {
00724             $html = "<li><div class='mw-search-result-heading'>{$link} {$redirect} {$section}</div> {$extract}\n" .
00725                 "<div class='mw-search-result-data'>{$score}{$size} - {$date}{$related}</div>" .
00726                 "</li>\n";
00727         }
00728 
00729         wfProfileOut( __METHOD__ );
00730         return $html;
00731     }
00732 
00741     protected function showInterwiki( &$matches, $query ) {
00742         global $wgContLang;
00743         wfProfileIn( __METHOD__ );
00744         $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
00745 
00746         $out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>" .
00747             $this->msg( 'search-interwiki-caption' )->text() . "</div>\n";
00748         $out .= "<ul class='mw-search-iwresults'>\n";
00749 
00750         // work out custom project captions
00751         $customCaptions = array();
00752         $customLines = explode( "\n", $this->msg( 'search-interwiki-custom' )->text() ); // format per line <iwprefix>:<caption>
00753         foreach ( $customLines as $line ) {
00754             $parts = explode( ":", $line, 2 );
00755             if ( count( $parts ) == 2 ) { // validate line
00756                 $customCaptions[$parts[0]] = $parts[1];
00757             }
00758         }
00759 
00760         $prev = null;
00761         $result = $matches->next();
00762         while ( $result ) {
00763             $out .= $this->showInterwikiHit( $result, $prev, $terms, $query, $customCaptions );
00764             $prev = $result->getInterwikiPrefix();
00765             $result = $matches->next();
00766         }
00767         // TODO: should support paging in a non-confusing way (not sure how though, maybe via ajax)..
00768         $out .= "</ul></div>\n";
00769 
00770         // convert the whole thing to desired language variant
00771         $out = $wgContLang->convert( $out );
00772         wfProfileOut( __METHOD__ );
00773         return $out;
00774     }
00775 
00787     protected function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions ) {
00788         wfProfileIn( __METHOD__ );
00789 
00790         if ( $result->isBrokenTitle() ) {
00791             wfProfileOut( __METHOD__ );
00792             return "<!-- Broken link in search result -->\n";
00793         }
00794 
00795         $t = $result->getTitle();
00796 
00797         $titleSnippet = $result->getTitleSnippet( $terms );
00798 
00799         if ( $titleSnippet == '' ) {
00800             $titleSnippet = null;
00801         }
00802 
00803         $link = Linker::linkKnown(
00804             $t,
00805             $titleSnippet
00806         );
00807 
00808         // format redirect if any
00809         $redirectTitle = $result->getRedirectTitle();
00810         $redirectText = $result->getRedirectSnippet( $terms );
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 != $t->getInterwiki() ) {
00826             if ( array_key_exists( $t->getInterwiki(), $customCaptions ) ) {
00827                 // captions from 'search-interwiki-custom'
00828                 $caption = $customCaptions[$t->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( $t->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( $t->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         wfProfileOut( __METHOD__ );
00852         return $out;
00853     }
00854 
00860     protected function getProfileForm( $profile, $term ) {
00861         // Hidden stuff
00862         $opts = array();
00863         $opts['redirs'] = $this->searchRedirects;
00864         $opts['profile'] = $this->profile;
00865 
00866         if ( $profile === 'advanced' ) {
00867             return $this->powerSearchBox( $term, $opts );
00868         } else {
00869             $form = '';
00870             wfRunHooks( 'SpecialSearchProfileForm', array( $this, &$form, $profile, $term, $opts ) );
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         // Show redirects check only if backend supports it
00933         if ( $this->getSearchEngine()->supports( 'list-redirects' ) ) {
00934             $showSections['redirects'] =
00935                 Xml::checkLabel( $this->msg( 'powersearch-redir' )->text(), 'redirs', 'redirs', $this->searchRedirects );
00936         }
00937 
00938         wfRunHooks( 'SpecialSearchPowerBox', array( &$showSections, $term, $opts ) );
00939 
00940         $hidden = '';
00941         unset( $opts['redirs'] );
00942         foreach ( $opts as $key => $value ) {
00943             $hidden .= Html::hidden( $key, $value );
00944         }
00945         // Return final output
00946         return Xml::openElement(
00947                 'fieldset',
00948                 array( 'id' => 'mw-searchoptions', 'style' => 'margin:0em;' )
00949             ) .
00950             Xml::element( 'legend', null, $this->msg( 'powersearch-legend' )->text() ) .
00951             Xml::tags( 'h4', null, $this->msg( 'powersearch-ns' )->parse() ) .
00952             Html::element( 'div', array( 'id' => 'mw-search-togglebox' ) ) .
00953             Xml::element( 'div', array( 'class' => 'divider' ), '', false ) .
00954             implode( Xml::element( 'div', array( 'class' => 'divider' ), '', false ), $showSections ) .
00955             $hidden .
00956             Xml::closeElement( 'fieldset' );
00957     }
00958 
00962     protected function getSearchProfiles() {
00963         // Builds list of Search Types (profiles)
00964         $nsAllSet = array_keys( SearchEngine::searchableNamespaces() );
00965 
00966         $profiles = array(
00967             'default' => array(
00968                 'message' => 'searchprofile-articles',
00969                 'tooltip' => 'searchprofile-articles-tooltip',
00970                 'namespaces' => SearchEngine::defaultNamespaces(),
00971                 'namespace-messages' => SearchEngine::namespacesAsText(
00972                     SearchEngine::defaultNamespaces()
00973                 ),
00974             ),
00975             'images' => array(
00976                 'message' => 'searchprofile-images',
00977                 'tooltip' => 'searchprofile-images-tooltip',
00978                 'namespaces' => array( NS_FILE ),
00979             ),
00980             'help' => array(
00981                 'message' => 'searchprofile-project',
00982                 'tooltip' => 'searchprofile-project-tooltip',
00983                 'namespaces' => SearchEngine::helpNamespaces(),
00984                 'namespace-messages' => SearchEngine::namespacesAsText(
00985                     SearchEngine::helpNamespaces()
00986                 ),
00987             ),
00988             'all' => array(
00989                 'message' => 'searchprofile-everything',
00990                 'tooltip' => 'searchprofile-everything-tooltip',
00991                 'namespaces' => $nsAllSet,
00992             ),
00993             'advanced' => array(
00994                 'message' => 'searchprofile-advanced',
00995                 'tooltip' => 'searchprofile-advanced-tooltip',
00996                 'namespaces' => self::NAMESPACES_CURRENT,
00997             )
00998         );
00999 
01000         wfRunHooks( 'SpecialSearchProfiles', array( &$profiles ) );
01001 
01002         foreach ( $profiles as &$data ) {
01003             if ( !is_array( $data['namespaces'] ) ) {
01004                 continue;
01005             }
01006             sort( $data['namespaces'] );
01007         }
01008 
01009         return $profiles;
01010     }
01011 
01018     protected function formHeader( $term, $resultsShown, $totalNum ) {
01019         $out = Xml::openElement( 'div', array( 'class' => 'mw-search-formheader' ) );
01020 
01021         $bareterm = $term;
01022         if ( $this->startsWithImage( $term ) ) {
01023             // Deletes prefixes
01024             $bareterm = substr( $term, strpos( $term, ':' ) + 1 );
01025         }
01026 
01027         $profiles = $this->getSearchProfiles();
01028         $lang = $this->getLanguage();
01029 
01030         // Outputs XML for Search Types
01031         $out .= Xml::openElement( 'div', array( 'class' => 'search-types' ) );
01032         $out .= Xml::openElement( 'ul' );
01033         foreach ( $profiles as $id => $profile ) {
01034             if ( !isset( $profile['parameters'] ) ) {
01035                 $profile['parameters'] = array();
01036             }
01037             $profile['parameters']['profile'] = $id;
01038 
01039             $tooltipParam = isset( $profile['namespace-messages'] ) ?
01040                 $lang->commaList( $profile['namespace-messages'] ) : null;
01041             $out .= Xml::tags(
01042                 'li',
01043                 array(
01044                     'class' => $this->profile === $id ? 'current' : 'normal'
01045                 ),
01046                 $this->makeSearchLink(
01047                     $bareterm,
01048                     array(),
01049                     $this->msg( $profile['message'] )->text(),
01050                     $this->msg( $profile['tooltip'], $tooltipParam )->text(),
01051                     $profile['parameters']
01052                 )
01053             );
01054         }
01055         $out .= Xml::closeElement( 'ul' );
01056         $out .= Xml::closeElement( 'div' );
01057 
01058         // Results-info
01059         if ( $resultsShown > 0 ) {
01060             if ( $totalNum > 0 ) {
01061                 $top = $this->msg( 'showingresultsheader' )
01062                     ->numParams( $this->offset + 1, $this->offset + $resultsShown, $totalNum )
01063                     ->params( wfEscapeWikiText( $term ) )
01064                     ->numParams( $resultsShown )
01065                     ->parse();
01066             } elseif ( $resultsShown >= $this->limit ) {
01067                 $top = $this->msg( 'showingresults' )
01068                     ->numParams( $this->limit, $this->offset + 1 )
01069                     ->parse();
01070             } else {
01071                 $top = $this->msg( 'showingresultsnum' )
01072                     ->numParams( $this->limit, $this->offset + 1, $resultsShown )
01073                     ->parse();
01074             }
01075             $out .= Xml::tags( 'div', array( 'class' => 'results-info' ),
01076                 Xml::tags( 'ul', null, Xml::tags( 'li', null, $top ) )
01077             );
01078         }
01079 
01080         $out .= Xml::element( 'div', array( 'style' => 'clear:both' ), '', false );
01081         $out .= Xml::closeElement( 'div' );
01082 
01083         return $out;
01084     }
01085 
01090     protected function shortDialog( $term ) {
01091         $out = Html::hidden( 'title', $this->getTitle()->getPrefixedText() );
01092         $out .= Html::hidden( 'profile', $this->profile ) . "\n";
01093         // Term box
01094         $out .= Html::input( 'search', $term, 'search', array(
01095             'id' => $this->profile === 'advanced' ? 'powerSearchText' : 'searchText',
01096             'size' => '50',
01097             'autofocus'
01098         ) ) . "\n";
01099         $out .= Html::hidden( 'fulltext', 'Search' ) . "\n";
01100         $out .= Xml::submitButton( $this->msg( 'searchbutton' )->text() ) . "\n";
01101         return $out . $this->didYouMeanHtml;
01102     }
01103 
01114     protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params = array() ) {
01115         $opt = $params;
01116         foreach ( $namespaces as $n ) {
01117             $opt['ns' . $n] = 1;
01118         }
01119         $opt['redirs'] = $this->searchRedirects;
01120 
01121         $stParams = array_merge(
01122             array(
01123                 'search' => $term,
01124                 'fulltext' => $this->msg( 'search' )->text()
01125             ),
01126             $opt
01127         );
01128 
01129         return Xml::element(
01130             'a',
01131             array(
01132                 'href' => $this->getTitle()->getLocalURL( $stParams ),
01133                 'title' => $tooltip
01134             ),
01135             $label
01136         );
01137     }
01138 
01145     protected function startsWithImage( $term ) {
01146         global $wgContLang;
01147 
01148         $p = explode( ':', $term );
01149         if ( count( $p ) > 1 ) {
01150             return $wgContLang->getNsIndex( $p[0] ) == NS_FILE;
01151         }
01152         return false;
01153     }
01154 
01161     protected function startsWithAll( $term ) {
01162 
01163         $allkeyword = $this->msg( 'searchall' )->inContentLanguage()->text();
01164 
01165         $p = explode( ':', $term );
01166         if ( count( $p ) > 1 ) {
01167             return $p[0] == $allkeyword;
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         return $this->searchEngine;
01183     }
01184 
01194     public function setExtraParam( $key, $value ) {
01195         $this->extraParams[$key] = $value;
01196     }
01197 
01198     protected function getGroupName() {
01199         return 'pages';
01200     }
01201 }