MediaWiki
REL1_21
|
00001 <?php 00030 class SpecialSearch extends SpecialPage { 00039 protected $profile; 00040 function getProfile() { return $this->profile; } 00041 00043 protected $searchEngine; 00044 00046 protected $extraParams = array(); 00047 00049 protected $mPrefix; 00050 00054 protected $limit, $offset; 00055 00059 protected $namespaces; 00060 function getNamespaces() { return $this->namespaces; } 00061 00065 protected $searchRedirects; 00066 00070 protected $didYouMeanHtml, $fulltext; 00071 00072 const NAMESPACES_CURRENT = 'sense'; 00073 00074 public function __construct() { 00075 parent::__construct( 'Search' ); 00076 } 00077 00083 public function execute( $par ) { 00084 $this->setHeaders(); 00085 $this->outputHeader(); 00086 $out = $this->getOutput(); 00087 $out->allowClickjacking(); 00088 $out->addModuleStyles( 'mediawiki.special' ); 00089 00090 // Strip underscores from title parameter; most of the time we'll want 00091 // text form here. But don't strip underscores from actual text params! 00092 $titleParam = str_replace( '_', ' ', $par ); 00093 00094 $request = $this->getRequest(); 00095 00096 // Fetch the search term 00097 $search = str_replace( "\n", " ", $request->getText( 'search', $titleParam ) ); 00098 00099 $this->load(); 00100 00101 if ( $request->getVal( 'fulltext' ) 00102 || !is_null( $request->getVal( 'offset' ) ) 00103 || !is_null( $request->getVal( 'searchx' ) ) ) 00104 { 00105 $this->showResults( $search ); 00106 } else { 00107 $this->goResult( $search ); 00108 } 00109 } 00110 00116 public function load() { 00117 $request = $this->getRequest(); 00118 list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' ); 00119 $this->mPrefix = $request->getVal( 'prefix', '' ); 00120 00121 $user = $this->getUser(); 00122 00123 # Extract manually requested namespaces 00124 $nslist = $this->powerSearch( $request ); 00125 if ( !count( $nslist ) ) { 00126 # Fallback to user preference 00127 $nslist = SearchEngine::userNamespaces( $user ); 00128 } 00129 00130 $profile = null; 00131 if ( !count( $nslist ) ) { 00132 $profile = 'default'; 00133 } 00134 00135 $profile = $request->getVal( 'profile', $profile ); 00136 $profiles = $this->getSearchProfiles(); 00137 if ( $profile === null ) { 00138 // BC with old request format 00139 $profile = 'advanced'; 00140 foreach( $profiles as $key => $data ) { 00141 if ( $nslist === $data['namespaces'] && $key !== 'advanced' ) { 00142 $profile = $key; 00143 } 00144 } 00145 $this->namespaces = $nslist; 00146 } elseif ( $profile === 'advanced' ) { 00147 $this->namespaces = $nslist; 00148 } else { 00149 if ( isset( $profiles[$profile]['namespaces'] ) ) { 00150 $this->namespaces = $profiles[$profile]['namespaces']; 00151 } else { 00152 // Unknown profile requested 00153 $profile = 'default'; 00154 $this->namespaces = $profiles['default']['namespaces']; 00155 } 00156 } 00157 00158 // Redirects defaults to true, but we don't know whether it was ticked of or just missing 00159 $default = $request->getBool( 'profile' ) ? 0 : 1; 00160 $this->searchRedirects = $request->getBool( 'redirs', $default ) ? 1 : 0; 00161 $this->didYouMeanHtml = ''; # html of did you mean... link 00162 $this->fulltext = $request->getVal( 'fulltext' ); 00163 $this->profile = $profile; 00164 } 00165 00171 public function goResult( $term ) { 00172 $this->setupPage( $term ); 00173 # Try to go to page as entered. 00174 $t = Title::newFromText( $term ); 00175 # If the string cannot be used to create a title 00176 if( is_null( $t ) ) { 00177 $this->showResults( $term ); 00178 return; 00179 } 00180 # If there's an exact or very near match, jump right there. 00181 $t = SearchEngine::getNearMatch( $term ); 00182 00183 if ( !wfRunHooks( 'SpecialSearchGo', array( &$t, &$term ) ) ) { 00184 # Hook requested termination 00185 return; 00186 } 00187 00188 if( !is_null( $t ) ) { 00189 $this->getOutput()->redirect( $t->getFullURL() ); 00190 return; 00191 } 00192 # No match, generate an edit URL 00193 $t = Title::newFromText( $term ); 00194 if( !is_null( $t ) ) { 00195 global $wgGoToEdit; 00196 wfRunHooks( 'SpecialSearchNogomatch', array( &$t ) ); 00197 wfDebugLog( 'nogomatch', $t->getText(), false ); 00198 00199 # If the feature is enabled, go straight to the edit page 00200 if( $wgGoToEdit ) { 00201 $this->getOutput()->redirect( $t->getFullURL( array( 'action' => 'edit' ) ) ); 00202 return; 00203 } 00204 } 00205 $this->showResults( $term ); 00206 } 00207 00211 public function showResults( $term ) { 00212 global $wgDisableTextSearch, $wgSearchForwardUrl, $wgContLang, $wgScript; 00213 wfProfileIn( __METHOD__ ); 00214 00215 $search = $this->getSearchEngine(); 00216 $search->setLimitOffset( $this->limit, $this->offset ); 00217 $search->setNamespaces( $this->namespaces ); 00218 $search->showRedirects = $this->searchRedirects; // BC 00219 $search->setFeatureData( 'list-redirects', $this->searchRedirects ); 00220 $search->prefix = $this->mPrefix; 00221 $term = $search->transformSearchTerm( $term ); 00222 00223 wfRunHooks( 'SpecialSearchSetupEngine', array( $this, $this->profile, $search ) ); 00224 00225 $this->setupPage( $term ); 00226 00227 $out = $this->getOutput(); 00228 00229 if ( $wgDisableTextSearch ) { 00230 if ( $wgSearchForwardUrl ) { 00231 $url = str_replace( '$1', urlencode( $term ), $wgSearchForwardUrl ); 00232 $out->redirect( $url ); 00233 } else { 00234 $out->addHTML( 00235 Xml::openElement( 'fieldset' ) . 00236 Xml::element( 'legend', null, $this->msg( 'search-external' )->text() ) . 00237 Xml::element( 'p', array( 'class' => 'mw-searchdisabled' ), $this->msg( 'searchdisabled' )->text() ) . 00238 $this->msg( 'googlesearch' )->rawParams( 00239 htmlspecialchars( $term ), 00240 'UTF-8', 00241 $this->msg( 'searchbutton' )->escaped() 00242 )->text() . 00243 Xml::closeElement( 'fieldset' ) 00244 ); 00245 } 00246 wfProfileOut( __METHOD__ ); 00247 return; 00248 } 00249 00250 $t = Title::newFromText( $term ); 00251 00252 // fetch search results 00253 $rewritten = $search->replacePrefixes( $term ); 00254 00255 $titleMatches = $search->searchTitle( $rewritten ); 00256 if( !( $titleMatches instanceof SearchResultTooMany ) ) { 00257 $textMatches = $search->searchText( $rewritten ); 00258 } 00259 00260 // did you mean... suggestions 00261 if( $textMatches && $textMatches->hasSuggestion() ) { 00262 $st = SpecialPage::getTitleFor( 'Search' ); 00263 00264 # mirror Go/Search behavior of original request .. 00265 $didYouMeanParams = array( 'search' => $textMatches->getSuggestionQuery() ); 00266 00267 if( $this->fulltext != null ) { 00268 $didYouMeanParams['fulltext'] = $this->fulltext; 00269 } 00270 00271 $stParams = array_merge( 00272 $didYouMeanParams, 00273 $this->powerSearchOptions() 00274 ); 00275 00276 $suggestionSnippet = $textMatches->getSuggestionSnippet(); 00277 00278 if( $suggestionSnippet == '' ) { 00279 $suggestionSnippet = null; 00280 } 00281 00282 $suggestLink = Linker::linkKnown( 00283 $st, 00284 $suggestionSnippet, 00285 array(), 00286 $stParams 00287 ); 00288 00289 $this->didYouMeanHtml = '<div class="searchdidyoumean">' . $this->msg( 'search-suggest' )->rawParams( $suggestLink )->text() . '</div>'; 00290 } 00291 00292 if ( !wfRunHooks( 'SpecialSearchResultsPrepend', array( $this, $out, $term ) ) ) { 00293 # Hook requested termination 00294 wfProfileOut( __METHOD__ ); 00295 return; 00296 } 00297 00298 // start rendering the page 00299 $out->addHtml( 00300 Xml::openElement( 00301 'form', 00302 array( 00303 'id' => ( $this->profile === 'advanced' ? 'powersearch' : 'search' ), 00304 'method' => 'get', 00305 'action' => $wgScript 00306 ) 00307 ) 00308 ); 00309 $out->addHtml( 00310 Xml::openElement( 'table', array( 'id' => 'mw-search-top-table', 'cellpadding' => 0, 'cellspacing' => 0 ) ) . 00311 Xml::openElement( 'tr' ) . 00312 Xml::openElement( 'td' ) . "\n" . 00313 $this->shortDialog( $term ) . 00314 Xml::closeElement( 'td' ) . 00315 Xml::closeElement( 'tr' ) . 00316 Xml::closeElement( 'table' ) 00317 ); 00318 00319 // Sometimes the search engine knows there are too many hits 00320 if( $titleMatches instanceof SearchResultTooMany ) { 00321 $out->wrapWikiMsg( "==$1==\n", 'toomanymatches' ); 00322 wfProfileOut( __METHOD__ ); 00323 return; 00324 } 00325 00326 $filePrefix = $wgContLang->getFormattedNsText( NS_FILE ) . ':'; 00327 if( trim( $term ) === '' || $filePrefix === trim( $term ) ) { 00328 $out->addHTML( $this->formHeader( $term, 0, 0 ) ); 00329 $out->addHtml( $this->getProfileForm( $this->profile, $term ) ); 00330 $out->addHTML( '</form>' ); 00331 // Empty query -- straight view of search form 00332 wfProfileOut( __METHOD__ ); 00333 return; 00334 } 00335 00336 // Get number of results 00337 $titleMatchesNum = $titleMatches ? $titleMatches->numRows() : 0; 00338 $textMatchesNum = $textMatches ? $textMatches->numRows() : 0; 00339 // Total initial query matches (possible false positives) 00340 $num = $titleMatchesNum + $textMatchesNum; 00341 00342 // Get total actual results (after second filtering, if any) 00343 $numTitleMatches = $titleMatches && !is_null( $titleMatches->getTotalHits() ) ? 00344 $titleMatches->getTotalHits() : $titleMatchesNum; 00345 $numTextMatches = $textMatches && !is_null( $textMatches->getTotalHits() ) ? 00346 $textMatches->getTotalHits() : $textMatchesNum; 00347 00348 // get total number of results if backend can calculate it 00349 $totalRes = 0; 00350 if( $titleMatches && !is_null( $titleMatches->getTotalHits() ) ) 00351 $totalRes += $titleMatches->getTotalHits(); 00352 if( $textMatches && !is_null( $textMatches->getTotalHits() ) ) 00353 $totalRes += $textMatches->getTotalHits(); 00354 00355 // show number of results and current offset 00356 $out->addHTML( $this->formHeader( $term, $num, $totalRes ) ); 00357 $out->addHtml( $this->getProfileForm( $this->profile, $term ) ); 00358 00359 $out->addHtml( Xml::closeElement( 'form' ) ); 00360 $out->addHtml( "<div class='searchresults'>" ); 00361 00362 // prev/next links 00363 if( $num || $this->offset ) { 00364 // Show the create link ahead 00365 $this->showCreateLink( $t ); 00366 $prevnext = $this->getLanguage()->viewPrevNext( $this->getTitle(), $this->offset, $this->limit, 00367 $this->powerSearchOptions() + array( 'search' => $term ), 00368 max( $titleMatchesNum, $textMatchesNum ) < $this->limit 00369 ); 00370 //$out->addHTML( "<p class='mw-search-pager-top'>{$prevnext}</p>\n" ); 00371 wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) ); 00372 } else { 00373 wfRunHooks( 'SpecialSearchNoResults', array( $term ) ); 00374 } 00375 00376 $out->parserOptions()->setEditSection( false ); 00377 if( $titleMatches ) { 00378 if( $numTitleMatches > 0 ) { 00379 $out->wrapWikiMsg( "==$1==\n", 'titlematches' ); 00380 $out->addHTML( $this->showMatches( $titleMatches ) ); 00381 } 00382 $titleMatches->free(); 00383 } 00384 if( $textMatches ) { 00385 // output appropriate heading 00386 if( $numTextMatches > 0 && $numTitleMatches > 0 ) { 00387 // if no title matches the heading is redundant 00388 $out->wrapWikiMsg( "==$1==\n", 'textmatches' ); 00389 } elseif( $totalRes == 0 ) { 00390 # Don't show the 'no text matches' if we received title matches 00391 # $out->wrapWikiMsg( "==$1==\n", 'notextmatches' ); 00392 } 00393 // show interwiki results if any 00394 if( $textMatches->hasInterwikiResults() ) { 00395 $out->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ) ); 00396 } 00397 // show results 00398 if( $numTextMatches > 0 ) { 00399 $out->addHTML( $this->showMatches( $textMatches ) ); 00400 } 00401 00402 $textMatches->free(); 00403 } 00404 if( $num === 0 ) { 00405 $out->wrapWikiMsg( "<p class=\"mw-search-nonefound\">\n$1</p>", array( 'search-nonefound', wfEscapeWikiText( $term ) ) ); 00406 $this->showCreateLink( $t ); 00407 } 00408 $out->addHtml( "</div>" ); 00409 00410 if( $num || $this->offset ) { 00411 $out->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" ); 00412 } 00413 wfRunHooks( 'SpecialSearchResultsAppend', array( $this, $out, $term ) ); 00414 wfProfileOut( __METHOD__ ); 00415 } 00416 00420 protected function showCreateLink( $t ) { 00421 // show direct page/create link if applicable 00422 00423 // Check DBkey !== '' in case of fragment link only. 00424 if( is_null( $t ) || $t->getDBkey() === '' ) { 00425 // invalid title 00426 // preserve the paragraph for margins etc... 00427 $this->getOutput()->addHtml( '<p></p>' ); 00428 return; 00429 } 00430 00431 if( $t->isKnown() ) { 00432 $messageName = 'searchmenu-exists'; 00433 } elseif( $t->userCan( 'create', $this->getUser() ) ) { 00434 $messageName = 'searchmenu-new'; 00435 } else { 00436 $messageName = 'searchmenu-new-nocreate'; 00437 } 00438 $params = array( $messageName, wfEscapeWikiText( $t->getPrefixedText() ) ); 00439 wfRunHooks( 'SpecialSearchCreateLink', array( $t, &$params ) ); 00440 00441 // Extensions using the hook might still return an empty $messageName 00442 if( $messageName ) { 00443 $this->getOutput()->wrapWikiMsg( "<p class=\"mw-search-createlink\">\n$1</p>", $params ); 00444 } else { 00445 // preserve the paragraph for margins etc... 00446 $this->getOutput()->addHtml( '<p></p>' ); 00447 } 00448 } 00449 00453 protected function setupPage( $term ) { 00454 # Should advanced UI be used? 00455 $this->searchAdvanced = ($this->profile === 'advanced'); 00456 $out = $this->getOutput(); 00457 if( strval( $term ) !== '' ) { 00458 $out->setPageTitle( $this->msg( 'searchresults' ) ); 00459 $out->setHTMLTitle( $this->msg( 'pagetitle' )->rawParams( 00460 $this->msg( 'searchresults-title' )->rawParams( $term )->text() 00461 ) ); 00462 } 00463 // add javascript specific to special:search 00464 $out->addModules( 'mediawiki.special.search' ); 00465 } 00466 00474 protected function powerSearch( &$request ) { 00475 $arr = array(); 00476 foreach( SearchEngine::searchableNamespaces() as $ns => $name ) { 00477 if( $request->getCheck( 'ns' . $ns ) ) { 00478 $arr[] = $ns; 00479 } 00480 } 00481 00482 return $arr; 00483 } 00484 00490 protected function powerSearchOptions() { 00491 $opt = array(); 00492 $opt['redirs'] = $this->searchRedirects ? 1 : 0; 00493 if( $this->profile !== 'advanced' ) { 00494 $opt['profile'] = $this->profile; 00495 } else { 00496 foreach( $this->namespaces as $n ) { 00497 $opt['ns' . $n] = 1; 00498 } 00499 } 00500 return $opt + $this->extraParams; 00501 } 00502 00510 protected function showMatches( &$matches ) { 00511 global $wgContLang; 00512 wfProfileIn( __METHOD__ ); 00513 00514 $terms = $wgContLang->convertForSearchResult( $matches->termMatches() ); 00515 00516 $out = ""; 00517 $infoLine = $matches->getInfo(); 00518 if( !is_null( $infoLine ) ) { 00519 $out .= "\n<!-- {$infoLine} -->\n"; 00520 } 00521 $out .= "<ul class='mw-search-results'>\n"; 00522 $result = $matches->next(); 00523 while( $result ) { 00524 $out .= $this->showHit( $result, $terms ); 00525 $result = $matches->next(); 00526 } 00527 $out .= "</ul>\n"; 00528 00529 // convert the whole thing to desired language variant 00530 $out = $wgContLang->convert( $out ); 00531 wfProfileOut( __METHOD__ ); 00532 return $out; 00533 } 00534 00543 protected function showHit( $result, $terms ) { 00544 wfProfileIn( __METHOD__ ); 00545 00546 if( $result->isBrokenTitle() ) { 00547 wfProfileOut( __METHOD__ ); 00548 return "<!-- Broken link in search result -->\n"; 00549 } 00550 00551 $t = $result->getTitle(); 00552 00553 $titleSnippet = $result->getTitleSnippet( $terms ); 00554 00555 if( $titleSnippet == '' ) 00556 $titleSnippet = null; 00557 00558 $link_t = clone $t; 00559 00560 wfRunHooks( 'ShowSearchHitTitle', 00561 array( &$link_t, &$titleSnippet, $result, $terms, $this ) ); 00562 00563 $link = Linker::linkKnown( 00564 $link_t, 00565 $titleSnippet 00566 ); 00567 00568 //If page content is not readable, just return the title. 00569 //This is not quite safe, but better than showing excerpts from non-readable pages 00570 //Note that hiding the entry entirely would screw up paging. 00571 if( !$t->userCan( 'read', $this->getUser() ) ) { 00572 wfProfileOut( __METHOD__ ); 00573 return "<li>{$link}</li>\n"; 00574 } 00575 00576 // If the page doesn't *exist*... our search index is out of date. 00577 // The least confusing at this point is to drop the result. 00578 // You may get less results, but... oh well. :P 00579 if( $result->isMissingRevision() ) { 00580 wfProfileOut( __METHOD__ ); 00581 return "<!-- missing page " . htmlspecialchars( $t->getPrefixedText() ) . "-->\n"; 00582 } 00583 00584 // format redirects / relevant sections 00585 $redirectTitle = $result->getRedirectTitle(); 00586 $redirectText = $result->getRedirectSnippet( $terms ); 00587 $sectionTitle = $result->getSectionTitle(); 00588 $sectionText = $result->getSectionSnippet( $terms ); 00589 $redirect = ''; 00590 00591 if( !is_null( $redirectTitle ) ) { 00592 if( $redirectText == '' ) 00593 $redirectText = null; 00594 00595 $redirect = "<span class='searchalttitle'>" . 00596 $this->msg( 'search-redirect' )->rawParams( 00597 Linker::linkKnown( $redirectTitle, $redirectText ) )->text() . 00598 "</span>"; 00599 } 00600 00601 $section = ''; 00602 00603 if( !is_null( $sectionTitle ) ) { 00604 if( $sectionText == '' ) 00605 $sectionText = null; 00606 00607 $section = "<span class='searchalttitle'>" . 00608 $this->msg( 'search-section' )->rawParams( 00609 Linker::linkKnown( $sectionTitle, $sectionText ) )->text() . 00610 "</span>"; 00611 } 00612 00613 // format text extract 00614 $extract = "<div class='searchresult'>" . $result->getTextSnippet( $terms ) . "</div>"; 00615 00616 $lang = $this->getLanguage(); 00617 00618 // format score 00619 if( is_null( $result->getScore() ) ) { 00620 // Search engine doesn't report scoring info 00621 $score = ''; 00622 } else { 00623 $percent = sprintf( '%2.1f', $result->getScore() * 100 ); 00624 $score = $this->msg( 'search-result-score' )->numParams( $percent )->text() 00625 . ' - '; 00626 } 00627 00628 // format description 00629 $byteSize = $result->getByteSize(); 00630 $wordCount = $result->getWordCount(); 00631 $timestamp = $result->getTimestamp(); 00632 $size = $this->msg( 'search-result-size', $lang->formatSize( $byteSize ) ) 00633 ->numParams( $wordCount )->escaped(); 00634 00635 if( $t->getNamespace() == NS_CATEGORY ) { 00636 $cat = Category::newFromTitle( $t ); 00637 $size = $this->msg( 'search-result-category-size' ) 00638 ->numParams( $cat->getPageCount(), $cat->getSubcatCount(), $cat->getFileCount() ) 00639 ->escaped(); 00640 } 00641 00642 $date = $lang->userTimeAndDate( $timestamp, $this->getUser() ); 00643 00644 // link to related articles if supported 00645 $related = ''; 00646 if( $result->hasRelated() ) { 00647 $st = SpecialPage::getTitleFor( 'Search' ); 00648 $stParams = array_merge( 00649 $this->powerSearchOptions(), 00650 array( 00651 'search' => $this->msg( 'searchrelated' )->inContentLanguage()->text() . 00652 ':' . $t->getPrefixedText(), 00653 'fulltext' => $this->msg( 'search' )->text() 00654 ) 00655 ); 00656 00657 $related = ' -- ' . Linker::linkKnown( 00658 $st, 00659 $this->msg( 'search-relatedarticle' )->text(), 00660 array(), 00661 $stParams 00662 ); 00663 } 00664 00665 // Include a thumbnail for media files... 00666 if( $t->getNamespace() == NS_FILE ) { 00667 $img = wfFindFile( $t ); 00668 if( $img ) { 00669 $thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) ); 00670 if( $thumb ) { 00671 $desc = $this->msg( 'parentheses' )->rawParams( $img->getShortDesc() )->escaped(); 00672 wfProfileOut( __METHOD__ ); 00673 // Float doesn't seem to interact well with the bullets. 00674 // Table messes up vertical alignment of the bullets. 00675 // Bullets are therefore disabled (didn't look great anyway). 00676 return "<li>" . 00677 '<table class="searchResultImage">' . 00678 '<tr>' . 00679 '<td style="width: 120px; text-align: center; vertical-align: top;">' . 00680 $thumb->toHtml( array( 'desc-link' => true ) ) . 00681 '</td>' . 00682 '<td style="vertical-align: top;">' . 00683 $link . 00684 $extract . 00685 "<div class='mw-search-result-data'>{$score}{$desc} - {$date}{$related}</div>" . 00686 '</td>' . 00687 '</tr>' . 00688 '</table>' . 00689 "</li>\n"; 00690 } 00691 } 00692 } 00693 00694 $html = null; 00695 00696 if ( wfRunHooks( 'ShowSearchHit', array ( 00697 $this, $result, $terms, 00698 &$link, &$redirect, &$section, &$extract, 00699 &$score, &$size, &$date, &$related, 00700 &$html 00701 ) ) ) { 00702 $html = "<li><div class='mw-search-result-heading'>{$link} {$redirect} {$section}</div> {$extract}\n" . 00703 "<div class='mw-search-result-data'>{$score}{$size} - {$date}{$related}</div>" . 00704 "</li>\n"; 00705 } 00706 00707 wfProfileOut( __METHOD__ ); 00708 return $html; 00709 } 00710 00719 protected function showInterwiki( &$matches, $query ) { 00720 global $wgContLang; 00721 wfProfileIn( __METHOD__ ); 00722 $terms = $wgContLang->convertForSearchResult( $matches->termMatches() ); 00723 00724 $out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>". 00725 $this->msg( 'search-interwiki-caption' )->text() . "</div>\n"; 00726 $out .= "<ul class='mw-search-iwresults'>\n"; 00727 00728 // work out custom project captions 00729 $customCaptions = array(); 00730 $customLines = explode( "\n", $this->msg( 'search-interwiki-custom' )->text() ); // format per line <iwprefix>:<caption> 00731 foreach( $customLines as $line ) { 00732 $parts = explode( ":", $line, 2 ); 00733 if( count( $parts ) == 2 ) { // validate line 00734 $customCaptions[$parts[0]] = $parts[1]; 00735 } 00736 } 00737 00738 $prev = null; 00739 $result = $matches->next(); 00740 while( $result ) { 00741 $out .= $this->showInterwikiHit( $result, $prev, $terms, $query, $customCaptions ); 00742 $prev = $result->getInterwikiPrefix(); 00743 $result = $matches->next(); 00744 } 00745 // TODO: should support paging in a non-confusing way (not sure how though, maybe via ajax).. 00746 $out .= "</ul></div>\n"; 00747 00748 // convert the whole thing to desired language variant 00749 $out = $wgContLang->convert( $out ); 00750 wfProfileOut( __METHOD__ ); 00751 return $out; 00752 } 00753 00765 protected function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions) { 00766 wfProfileIn( __METHOD__ ); 00767 00768 if( $result->isBrokenTitle() ) { 00769 wfProfileOut( __METHOD__ ); 00770 return "<!-- Broken link in search result -->\n"; 00771 } 00772 00773 $t = $result->getTitle(); 00774 00775 $titleSnippet = $result->getTitleSnippet( $terms ); 00776 00777 if( $titleSnippet == '' ) 00778 $titleSnippet = null; 00779 00780 $link = Linker::linkKnown( 00781 $t, 00782 $titleSnippet 00783 ); 00784 00785 // format redirect if any 00786 $redirectTitle = $result->getRedirectTitle(); 00787 $redirectText = $result->getRedirectSnippet( $terms ); 00788 $redirect = ''; 00789 if( !is_null( $redirectTitle ) ) { 00790 if( $redirectText == '' ) 00791 $redirectText = null; 00792 00793 $redirect = "<span class='searchalttitle'>" . 00794 $this->msg( 'search-redirect' )->rawParams( 00795 Linker::linkKnown( $redirectTitle, $redirectText ) )->text() . 00796 "</span>"; 00797 } 00798 00799 $out = ""; 00800 // display project name 00801 if( is_null( $lastInterwiki ) || $lastInterwiki != $t->getInterwiki() ) { 00802 if( array_key_exists( $t->getInterwiki(), $customCaptions ) ) { 00803 // captions from 'search-interwiki-custom' 00804 $caption = $customCaptions[$t->getInterwiki()]; 00805 } else { 00806 // default is to show the hostname of the other wiki which might suck 00807 // if there are many wikis on one hostname 00808 $parsed = wfParseUrl( $t->getFullURL() ); 00809 $caption = $this->msg( 'search-interwiki-default', $parsed['host'] )->text(); 00810 } 00811 // "more results" link (special page stuff could be localized, but we might not know target lang) 00812 $searchTitle = Title::newFromText( $t->getInterwiki() . ":Special:Search" ); 00813 $searchLink = Linker::linkKnown( 00814 $searchTitle, 00815 $this->msg( 'search-interwiki-more' )->text(), 00816 array(), 00817 array( 00818 'search' => $query, 00819 'fulltext' => 'Search' 00820 ) 00821 ); 00822 $out .= "</ul><div class='mw-search-interwiki-project'><span class='mw-search-interwiki-more'> 00823 {$searchLink}</span>{$caption}</div>\n<ul>"; 00824 } 00825 00826 $out .= "<li>{$link} {$redirect}</li>\n"; 00827 wfProfileOut( __METHOD__ ); 00828 return $out; 00829 } 00830 00836 protected function getProfileForm( $profile, $term ) { 00837 // Hidden stuff 00838 $opts = array(); 00839 $opts['redirs'] = $this->searchRedirects; 00840 $opts['profile'] = $this->profile; 00841 00842 if ( $profile === 'advanced' ) { 00843 return $this->powerSearchBox( $term, $opts ); 00844 } else { 00845 $form = ''; 00846 wfRunHooks( 'SpecialSearchProfileForm', array( $this, &$form, $profile, $term, $opts ) ); 00847 return $form; 00848 } 00849 } 00850 00858 protected function powerSearchBox( $term, $opts ) { 00859 // Groups namespaces into rows according to subject 00860 $rows = array(); 00861 foreach( SearchEngine::searchableNamespaces() as $namespace => $name ) { 00862 $subject = MWNamespace::getSubject( $namespace ); 00863 if( !array_key_exists( $subject, $rows ) ) { 00864 $rows[$subject] = ""; 00865 } 00866 $name = str_replace( '_', ' ', $name ); 00867 if( $name == '' ) { 00868 $name = $this->msg( 'blanknamespace' )->text(); 00869 } 00870 $rows[$subject] .= 00871 Xml::openElement( 00872 'td', array( 'style' => 'white-space: nowrap' ) 00873 ) . 00874 Xml::checkLabel( 00875 $name, 00876 "ns{$namespace}", 00877 "mw-search-ns{$namespace}", 00878 in_array( $namespace, $this->namespaces ) 00879 ) . 00880 Xml::closeElement( 'td' ); 00881 } 00882 $rows = array_values( $rows ); 00883 $numRows = count( $rows ); 00884 00885 // Lays out namespaces in multiple floating two-column tables so they'll 00886 // be arranged nicely while still accommodating different screen widths 00887 $namespaceTables = ''; 00888 for( $i = 0; $i < $numRows; $i += 4 ) { 00889 $namespaceTables .= Xml::openElement( 00890 'table', 00891 array( 'cellpadding' => 0, 'cellspacing' => 0 ) 00892 ); 00893 for( $j = $i; $j < $i + 4 && $j < $numRows; $j++ ) { 00894 $namespaceTables .= Xml::tags( 'tr', null, $rows[$j] ); 00895 } 00896 $namespaceTables .= Xml::closeElement( 'table' ); 00897 } 00898 00899 $showSections = array( 'namespaceTables' => $namespaceTables ); 00900 00901 // Show redirects check only if backend supports it 00902 if( $this->getSearchEngine()->supports( 'list-redirects' ) ) { 00903 $showSections['redirects'] = 00904 Xml::checkLabel( $this->msg( 'powersearch-redir' )->text(), 'redirs', 'redirs', $this->searchRedirects ); 00905 } 00906 00907 wfRunHooks( 'SpecialSearchPowerBox', array( &$showSections, $term, $opts ) ); 00908 00909 $hidden = ''; 00910 unset( $opts['redirs'] ); 00911 foreach( $opts as $key => $value ) { 00912 $hidden .= Html::hidden( $key, $value ); 00913 } 00914 // Return final output 00915 return 00916 Xml::openElement( 00917 'fieldset', 00918 array( 'id' => 'mw-searchoptions', 'style' => 'margin:0em;' ) 00919 ) . 00920 Xml::element( 'legend', null, $this->msg( 'powersearch-legend' )->text() ) . 00921 Xml::tags( 'h4', null, $this->msg( 'powersearch-ns' )->parse() ) . 00922 Html::element( 'div', array( 'id' => 'mw-search-togglebox' ) ) . 00923 Xml::element( 'div', array( 'class' => 'divider' ), '', false ) . 00924 implode( Xml::element( 'div', array( 'class' => 'divider' ), '', false ), $showSections ) . 00925 $hidden . 00926 Xml::closeElement( 'fieldset' ); 00927 } 00928 00932 protected function getSearchProfiles() { 00933 // Builds list of Search Types (profiles) 00934 $nsAllSet = array_keys( SearchEngine::searchableNamespaces() ); 00935 00936 $profiles = array( 00937 'default' => array( 00938 'message' => 'searchprofile-articles', 00939 'tooltip' => 'searchprofile-articles-tooltip', 00940 'namespaces' => SearchEngine::defaultNamespaces(), 00941 'namespace-messages' => SearchEngine::namespacesAsText( 00942 SearchEngine::defaultNamespaces() 00943 ), 00944 ), 00945 'images' => array( 00946 'message' => 'searchprofile-images', 00947 'tooltip' => 'searchprofile-images-tooltip', 00948 'namespaces' => array( NS_FILE ), 00949 ), 00950 'help' => array( 00951 'message' => 'searchprofile-project', 00952 'tooltip' => 'searchprofile-project-tooltip', 00953 'namespaces' => SearchEngine::helpNamespaces(), 00954 'namespace-messages' => SearchEngine::namespacesAsText( 00955 SearchEngine::helpNamespaces() 00956 ), 00957 ), 00958 'all' => array( 00959 'message' => 'searchprofile-everything', 00960 'tooltip' => 'searchprofile-everything-tooltip', 00961 'namespaces' => $nsAllSet, 00962 ), 00963 'advanced' => array( 00964 'message' => 'searchprofile-advanced', 00965 'tooltip' => 'searchprofile-advanced-tooltip', 00966 'namespaces' => self::NAMESPACES_CURRENT, 00967 ) 00968 ); 00969 00970 wfRunHooks( 'SpecialSearchProfiles', array( &$profiles ) ); 00971 00972 foreach( $profiles as &$data ) { 00973 if ( !is_array( $data['namespaces'] ) ) continue; 00974 sort( $data['namespaces'] ); 00975 } 00976 00977 return $profiles; 00978 } 00979 00986 protected function formHeader( $term, $resultsShown, $totalNum ) { 00987 $out = Xml::openElement( 'div', array( 'class' => 'mw-search-formheader' ) ); 00988 00989 $bareterm = $term; 00990 if( $this->startsWithImage( $term ) ) { 00991 // Deletes prefixes 00992 $bareterm = substr( $term, strpos( $term, ':' ) + 1 ); 00993 } 00994 00995 $profiles = $this->getSearchProfiles(); 00996 $lang = $this->getLanguage(); 00997 00998 // Outputs XML for Search Types 00999 $out .= Xml::openElement( 'div', array( 'class' => 'search-types' ) ); 01000 $out .= Xml::openElement( 'ul' ); 01001 foreach ( $profiles as $id => $profile ) { 01002 if ( !isset( $profile['parameters'] ) ) { 01003 $profile['parameters'] = array(); 01004 } 01005 $profile['parameters']['profile'] = $id; 01006 01007 $tooltipParam = isset( $profile['namespace-messages'] ) ? 01008 $lang->commaList( $profile['namespace-messages'] ) : null; 01009 $out .= Xml::tags( 01010 'li', 01011 array( 01012 'class' => $this->profile === $id ? 'current' : 'normal' 01013 ), 01014 $this->makeSearchLink( 01015 $bareterm, 01016 array(), 01017 $this->msg( $profile['message'] )->text(), 01018 $this->msg( $profile['tooltip'], $tooltipParam )->text(), 01019 $profile['parameters'] 01020 ) 01021 ); 01022 } 01023 $out .= Xml::closeElement( 'ul' ); 01024 $out .= Xml::closeElement( 'div' ); 01025 01026 // Results-info 01027 if ( $resultsShown > 0 ) { 01028 if ( $totalNum > 0 ) { 01029 $top = $this->msg( 'showingresultsheader' ) 01030 ->numParams( $this->offset + 1, $this->offset + $resultsShown, $totalNum ) 01031 ->params( wfEscapeWikiText( $term ) ) 01032 ->numParams( $resultsShown ) 01033 ->parse(); 01034 } elseif ( $resultsShown >= $this->limit ) { 01035 $top = $this->msg( 'showingresults' ) 01036 ->numParams( $this->limit, $this->offset + 1 ) 01037 ->parse(); 01038 } else { 01039 $top = $this->msg( 'showingresultsnum' ) 01040 ->numParams( $this->limit, $this->offset + 1, $resultsShown ) 01041 ->parse(); 01042 } 01043 $out .= Xml::tags( 'div', array( 'class' => 'results-info' ), 01044 Xml::tags( 'ul', null, Xml::tags( 'li', null, $top ) ) 01045 ); 01046 } 01047 01048 $out .= Xml::element( 'div', array( 'style' => 'clear:both' ), '', false ); 01049 $out .= Xml::closeElement( 'div' ); 01050 01051 return $out; 01052 } 01053 01058 protected function shortDialog( $term ) { 01059 $out = Html::hidden( 'title', $this->getTitle()->getPrefixedText() ); 01060 $out .= Html::hidden( 'profile', $this->profile ) . "\n"; 01061 // Term box 01062 $out .= Html::input( 'search', $term, 'search', array( 01063 'id' => $this->profile === 'advanced' ? 'powerSearchText' : 'searchText', 01064 'size' => '50', 01065 'autofocus' 01066 ) ) . "\n"; 01067 $out .= Html::hidden( 'fulltext', 'Search' ) . "\n"; 01068 $out .= Xml::submitButton( $this->msg( 'searchbutton' )->text() ) . "\n"; 01069 return $out . $this->didYouMeanHtml; 01070 } 01071 01082 protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params = array() ) { 01083 $opt = $params; 01084 foreach( $namespaces as $n ) { 01085 $opt['ns' . $n] = 1; 01086 } 01087 $opt['redirs'] = $this->searchRedirects; 01088 01089 $stParams = array_merge( 01090 array( 01091 'search' => $term, 01092 'fulltext' => $this->msg( 'search' )->text() 01093 ), 01094 $opt 01095 ); 01096 01097 return Xml::element( 01098 'a', 01099 array( 01100 'href' => $this->getTitle()->getLocalURL( $stParams ), 01101 'title' => $tooltip), 01102 $label 01103 ); 01104 } 01105 01112 protected function startsWithImage( $term ) { 01113 global $wgContLang; 01114 01115 $p = explode( ':', $term ); 01116 if( count( $p ) > 1 ) { 01117 return $wgContLang->getNsIndex( $p[0] ) == NS_FILE; 01118 } 01119 return false; 01120 } 01121 01128 protected function startsWithAll( $term ) { 01129 01130 $allkeyword = $this->msg( 'searchall' )->inContentLanguage()->text(); 01131 01132 $p = explode( ':', $term ); 01133 if( count( $p ) > 1 ) { 01134 return $p[0] == $allkeyword; 01135 } 01136 return false; 01137 } 01138 01144 public function getSearchEngine() { 01145 if ( $this->searchEngine === null ) { 01146 $this->searchEngine = SearchEngine::create(); 01147 } 01148 return $this->searchEngine; 01149 } 01150 01160 public function setExtraParam( $key, $value ) { 01161 $this->extraParams[$key] = $value; 01162 } 01163 01164 protected function getGroupName() { 01165 return 'redirects'; 01166 } 01167 }