MediaWiki
REL1_23
|
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 }