# http://www.mediawiki.org/ # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # http://www.gnu.org/copyleft/gpl.html /** * Run text & title search and display the output * @file * @ingroup SpecialPage */ /** * Entry point * * @param $par String: (default '') */ function wfSpecialSearch( $par = '' ) { global $wgRequest, $wgUser, $wgUseOldSearchUI; // Strip underscores from title parameter; most of the time we'll want // text form here. But don't strip underscores from actual text params! $titleParam = str_replace( '_', ' ', $par ); // Fetch the search term $search = str_replace( "\n", " ", $wgRequest->getText( 'search', $titleParam ) ); $class = $wgUseOldSearchUI ? 'SpecialSearchOld' : 'SpecialSearch'; $searchPage = new $class( $wgRequest, $wgUser ); if( $wgRequest->getVal( 'fulltext' ) || !is_null( $wgRequest->getVal( 'offset' )) || !is_null( $wgRequest->getVal( 'searchx' )) ) { $searchPage->showResults( $search ); } else { $searchPage->goResult( $search ); } } /** * implements Special:Search - Run text & title search and display the output * @ingroup SpecialPage */ class SpecialSearch { /** * Set up basic search parameters from the request and user settings. * Typically you'll pass $wgRequest and $wgUser. * * @param WebRequest $request * @param User $user * @public */ function __construct( &$request, &$user ) { list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' ); $this->mPrefix = $request->getVal('prefix', ''); # Extract requested namespaces $this->namespaces = $this->powerSearch( $request ); if( empty( $this->namespaces ) ) { $this->namespaces = SearchEngine::userNamespaces( $user ); } $this->searchRedirects = $request->getcheck( 'redirs' ) ? true : false; $this->searchAdvanced = $request->getVal( 'advanced' ); $this->active = 'advanced'; $this->sk = $user->getSkin(); $this->didYouMeanHtml = ''; # html of did you mean... link $this->fulltext = $request->getVal('fulltext'); } /** * If an exact title match can be found, jump straight ahead to it. * @param string $term */ public function goResult( $term ) { global $wgOut; $this->setupPage( $term ); # Try to go to page as entered. $t = Title::newFromText( $term ); # If the string cannot be used to create a title if( is_null( $t ) ) { return $this->showResults( $term ); } # If there's an exact or very near match, jump right there. $t = SearchEngine::getNearMatch( $term ); if( !is_null( $t ) ) { $wgOut->redirect( $t->getFullURL() ); return; } # No match, generate an edit URL $t = Title::newFromText( $term ); if( !is_null( $t ) ) { global $wgGoToEdit; wfRunHooks( 'SpecialSearchNogomatch', array( &$t ) ); # If the feature is enabled, go straight to the edit page if( $wgGoToEdit ) { $wgOut->redirect( $t->getFullURL( 'action=edit' ) ); return; } } return $this->showResults( $term ); } /** * @param string $term */ public function showResults( $term ) { global $wgOut, $wgUser, $wgDisableTextSearch, $wgContLang; wfProfileIn( __METHOD__ ); $sk = $wgUser->getSkin(); $this->searchEngine = SearchEngine::create(); $search =& $this->searchEngine; $search->setLimitOffset( $this->limit, $this->offset ); $search->setNamespaces( $this->namespaces ); $search->showRedirects = $this->searchRedirects; $search->prefix = $this->mPrefix; $term = $search->transformSearchTerm($term); $this->setupPage( $term ); if( $wgDisableTextSearch ) { global $wgSearchForwardUrl; if( $wgSearchForwardUrl ) { $url = str_replace( '$1', urlencode( $term ), $wgSearchForwardUrl ); $wgOut->redirect( $url ); wfProfileOut( __METHOD__ ); return; } global $wgInputEncoding; $wgOut->addHTML( Xml::openElement( 'fieldset' ) . Xml::element( 'legend', null, wfMsg( 'search-external' ) ) . Xml::element( 'p', array( 'class' => 'mw-searchdisabled' ), wfMsg( 'searchdisabled' ) ) . wfMsg( 'googlesearch', htmlspecialchars( $term ), htmlspecialchars( $wgInputEncoding ), htmlspecialchars( wfMsg( 'searchbutton' ) ) ) . Xml::closeElement( 'fieldset' ) ); wfProfileOut( __METHOD__ ); return; } $t = Title::newFromText( $term ); // fetch search results $rewritten = $search->replacePrefixes($term); $titleMatches = $search->searchTitle( $rewritten ); if( !($titleMatches instanceof SearchResultTooMany)) $textMatches = $search->searchText( $rewritten ); // did you mean... suggestions if( $textMatches && $textMatches->hasSuggestion() ) { $st = SpecialPage::getTitleFor( 'Search' ); # mirror Go/Search behaviour of original request .. $didYouMeanParams = array( 'search' => $textMatches->getSuggestionQuery() ); if($this->fulltext != NULL) $didYouMeanParams['fulltext'] = $this->fulltext; $stParams = wfArrayToCGI( $didYouMeanParams, $this->powerSearchOptions() ); $suggestLink = $sk->makeKnownLinkObj( $st, $textMatches->getSuggestionSnippet(), $stParams ); $this->didYouMeanHtml = '
'.wfMsg('search-suggest',$suggestLink).'
'; } // start rendering the page $wgOut->addHtml( Xml::openElement( 'table', array( 'border'=>0, 'cellpadding'=>0, 'cellspacing'=>0 ) ) . Xml::openElement( 'tr' ) . Xml::openElement( 'td' ) . "\n" . ( $this->searchAdvanced ? $this->powerSearchBox( $term ) : $this->shortDialog( $term ) ) . Xml::closeElement('td') . Xml::closeElement('tr') . Xml::closeElement('table') ); // Sometimes the search engine knows there are too many hits if( $titleMatches instanceof SearchResultTooMany ) { $wgOut->addWikiText( '==' . wfMsg( 'toomanymatches' ) . "==\n" ); wfProfileOut( __METHOD__ ); return; } $filePrefix = $wgContLang->getFormattedNsText(NS_FILE).':'; if( '' === trim( $term ) || $filePrefix === trim( $term ) ) { $wgOut->addHTML( $this->searchAdvanced ? $this->powerSearchFocus() : $this->searchFocus() ); // Empty query -- straight view of search form wfProfileOut( __METHOD__ ); return; } // show direct page/create link if( !is_null($t) ) { if( !$t->exists() ) { $wgOut->addWikiMsg( 'searchmenu-new', wfEscapeWikiText( $t->getPrefixedText() ) ); } else { $wgOut->addWikiMsg( 'searchmenu-exists', wfEscapeWikiText( $t->getPrefixedText() ) ); } } // Get number of results $titleMatchesSQL = $titleMatches ? $titleMatches->numRows() : 0; $textMatchesSQL = $textMatches ? $textMatches->numRows() : 0; // Total initial query matches (possible false positives) $numSQL = $titleMatchesSQL + $textMatchesSQL; // Get total actual results (after second filtering, if any) $numTitleMatches = $titleMatches && !is_null( $titleMatches->getTotalHits() ) ? $titleMatches->getTotalHits() : $titleMatchesSQL; $numTextMatches = $textMatches && !is_null( $textMatches->getTotalHits() ) ? $textMatches->getTotalHits() : $textMatchesSQL; $totalRes = $numTitleMatches + $numTextMatches; // show number of results and current offset if( $numSQL > 0 ) { if( $numSQL > 0 ) { $top = wfMsgExt('showingresultstotal', array( 'parseinline' ), $this->offset+1, $this->offset+$numSQL, $totalRes, $numSQL ); } elseif( $numSQL >= $this->limit ) { $top = wfShowingResults( $this->offset, $this->limit ); } else { $top = wfShowingResultsNum( $this->offset, $this->limit, $numSQL ); } $wgOut->addHTML( "

{$top}

\n" ); } // prev/next links if( $numSQL || $this->offset ) { $prevnext = wfViewPrevNext( $this->offset, $this->limit, SpecialPage::getTitleFor( 'Search' ), wfArrayToCGI( $this->powerSearchOptions(), array( 'search' => $term ) ), max( $titleMatchesSQL, $textMatchesSQL ) < $this->limit ); $wgOut->addHTML( "

{$prevnext}

\n" ); wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) ); } else { wfRunHooks( 'SpecialSearchNoResults', array( $term ) ); } $wgOut->addHtml( "
" ); if( $titleMatches ) { if( $numTitleMatches > 0 ) { $wgOut->wrapWikiMsg( "==$1==\n", 'titlematches' ); $wgOut->addHTML( $this->showMatches( $titleMatches ) ); } $titleMatches->free(); } if( $textMatches ) { // output appropriate heading if( $numTextMatches > 0 && $numTitleMatches > 0 ) { // if no title matches the heading is redundant $wgOut->wrapWikiMsg( "==$1==\n", 'textmatches' ); } elseif( $totalRes == 0 ) { # Don't show the 'no text matches' if we received title matches $wgOut->wrapWikiMsg( "==$1==\n", 'notextmatches' ); } // show interwiki results if any if( $textMatches->hasInterwikiResults() ) { $wgOut->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ) ); } // show results if( $numTextMatches > 0 ) { $wgOut->addHTML( $this->showMatches( $textMatches ) ); } $textMatches->free(); } if( $totalRes === 0 ) { $wgOut->addWikiMsg( 'search-nonefound' ); } $wgOut->addHtml( "
" ); if( $totalRes === 0 ) { $wgOut->addHTML( $this->searchAdvanced ? $this->powerSearchFocus() : $this->searchFocus() ); } if( $numSQL || $this->offset ) { $wgOut->addHTML( "

{$prevnext}

\n" ); } wfProfileOut( __METHOD__ ); } /** * */ protected function setupPage( $term ) { global $wgOut; // Figure out the active search profile header $nsAllSet = array_keys( SearchEngine::searchableNamespaces() ); if( $this->searchAdvanced ) $this->active = 'advanced'; else if( $this->namespaces === NS_FILE || $this->startsWithImage( $term ) ) $this->active = 'images'; elseif( $this->namespaces === $nsAllSet ) $this->active = 'all'; elseif( $this->namespaces === SearchEngine::defaultNamespaces() ) $this->active = 'default'; elseif( $this->namespaces === SearchEngine::projectNamespaces() ) $this->active = 'project'; else $this->active = 'advanced'; # Should advanced UI be used? $this->searchAdvanced = ($this->active === 'advanced'); if( !empty( $term ) ) { $wgOut->setPageTitle( wfMsg( 'searchresults') ); $wgOut->setHTMLTitle( wfMsg( 'pagetitle', wfMsg( 'searchresults-title', $term ) ) ); } $wgOut->setArticleRelated( false ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); } /** * Extract "power search" namespace settings from the request object, * returning a list of index numbers to search. * * @param WebRequest $request * @return array */ protected function powerSearch( &$request ) { $arr = array(); foreach( SearchEngine::searchableNamespaces() as $ns => $name ) { if( $request->getCheck( 'ns' . $ns ) ) { $arr[] = $ns; } } return $arr; } /** * Reconstruct the 'power search' options for links * @return array */ protected function powerSearchOptions() { $opt = array(); foreach( $this->namespaces as $n ) { $opt['ns' . $n] = 1; } $opt['redirs'] = $this->searchRedirects ? 1 : 0; if( $this->searchAdvanced ) { $opt['advanced'] = $this->searchAdvanced; } return $opt; } /** * Show whole set of results * * @param SearchResultSet $matches */ protected function showMatches( &$matches ) { global $wgContLang; wfProfileIn( __METHOD__ ); $terms = $wgContLang->convertForSearchResult( $matches->termMatches() ); $out = ""; $infoLine = $matches->getInfo(); if( !is_null($infoLine) ) { $out .= "\n\n"; } $off = $this->offset + 1; $out .= "\n"; // convert the whole thing to desired language variant $out = $wgContLang->convert( $out ); wfProfileOut( __METHOD__ ); return $out; } /** * Format a single hit result * @param SearchResult $result * @param array $terms terms to highlight */ protected function showHit( $result, $terms ) { global $wgContLang, $wgLang, $wgUser; wfProfileIn( __METHOD__ ); if( $result->isBrokenTitle() ) { wfProfileOut( __METHOD__ ); return "\n"; } $sk = $wgUser->getSkin(); $t = $result->getTitle(); $link = $this->sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms)); //If page content is not readable, just return the title. //This is not quite safe, but better than showing excerpts from non-readable pages //Note that hiding the entry entirely would screw up paging. if( !$t->userCanRead() ) { wfProfileOut( __METHOD__ ); return "
  • {$link}
  • \n"; } // If the page doesn't *exist*... our search index is out of date. // The least confusing at this point is to drop the result. // You may get less results, but... oh well. :P if( $result->isMissingRevision() ) { wfProfileOut( __METHOD__ ); return "\n"; } // format redirects / relevant sections $redirectTitle = $result->getRedirectTitle(); $redirectText = $result->getRedirectSnippet($terms); $sectionTitle = $result->getSectionTitle(); $sectionText = $result->getSectionSnippet($terms); $redirect = ''; if( !is_null($redirectTitle) ) $redirect = "" .wfMsg('search-redirect',$this->sk->makeKnownLinkObj( $redirectTitle, $redirectText)) .""; $section = ''; if( !is_null($sectionTitle) ) $section = "" .wfMsg('search-section', $this->sk->makeKnownLinkObj( $sectionTitle, $sectionText)) .""; // format text extract $extract = "
    ".$result->getTextSnippet($terms)."
    "; // format score if( is_null( $result->getScore() ) ) { // Search engine doesn't report scoring info $score = ''; } else { $percent = sprintf( '%2.1f', $result->getScore() * 100 ); $score = wfMsg( 'search-result-score', $wgLang->formatNum( $percent ) ) . ' - '; } // format description $byteSize = $result->getByteSize(); $wordCount = $result->getWordCount(); $timestamp = $result->getTimestamp(); $size = wfMsgExt( 'search-result-size', array( 'parsemag', 'escape' ), $this->sk->formatSize( $byteSize ), $wordCount ); $date = $wgLang->timeanddate( $timestamp ); // link to related articles if supported $related = ''; if( $result->hasRelated() ) { $st = SpecialPage::getTitleFor( 'Search' ); $stParams = wfArrayToCGI( $this->powerSearchOptions(), array('search' => wfMsgForContent('searchrelated').':'.$t->getPrefixedText(), 'fulltext' => wfMsg('search') )); $related = ' -- ' . $sk->makeKnownLinkObj( $st, wfMsg('search-relatedarticle'), $stParams ); } // Include a thumbnail for media files... if( $t->getNamespace() == NS_FILE ) { $img = wfFindFile( $t ); if( $img ) { $thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) ); if( $thumb ) { $desc = $img->getShortDesc(); wfProfileOut( __METHOD__ ); // Float doesn't seem to interact well with the bullets. // Table messes up vertical alignment of the bullets. // Bullets are therefore disabled (didn't look great anyway). return "
  • " . '' . '' . '' . '' . '' . '
    ' . $thumb->toHtml( array( 'desc-link' => true ) ) . '' . $link . $extract . "
    {$score}{$desc} - {$date}{$related}
    " . '
    ' . "
  • \n"; } } } wfProfileOut( __METHOD__ ); return "
  • {$link} {$redirect} {$section} {$extract}\n" . "
    {$score}{$size} - {$date}{$related}
    " . "
  • \n"; } /** * Show results from other wikis * * @param SearchResultSet $matches */ protected function showInterwiki( &$matches, $query ) { global $wgContLang; wfProfileIn( __METHOD__ ); $terms = $wgContLang->convertForSearchResult( $matches->termMatches() ); $out = "
    ". wfMsg('search-interwiki-caption')."
    \n"; $off = $this->offset + 1; $out .= "
    \n"; // convert the whole thing to desired language variant $out = $wgContLang->convert( $out ); wfProfileOut( __METHOD__ ); return $out; } /** * Show single interwiki link * * @param SearchResult $result * @param string $lastInterwiki * @param array $terms * @param string $query * @param array $customCaptions iw prefix -> caption */ protected function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions) { wfProfileIn( __METHOD__ ); global $wgContLang, $wgLang; if( $result->isBrokenTitle() ) { wfProfileOut( __METHOD__ ); return "\n"; } $t = $result->getTitle(); $link = $this->sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms)); // format redirect if any $redirectTitle = $result->getRedirectTitle(); $redirectText = $result->getRedirectSnippet($terms); $redirect = ''; if( !is_null($redirectTitle) ) $redirect = "" .wfMsg('search-redirect',$this->sk->makeKnownLinkObj( $redirectTitle, $redirectText)) .""; $out = ""; // display project name if(is_null($lastInterwiki) || $lastInterwiki != $t->getInterwiki()) { if( key_exists($t->getInterwiki(),$customCaptions) ) // captions from 'search-interwiki-custom' $caption = $customCaptions[$t->getInterwiki()]; else{ // default is to show the hostname of the other wiki which might suck // if there are many wikis on one hostname $parsed = parse_url($t->getFullURL()); $caption = wfMsg('search-interwiki-default', $parsed['host']); } // "more results" link (special page stuff could be localized, but we might not know target lang) $searchTitle = Title::newFromText($t->getInterwiki().":Special:Search"); $searchLink = $this->sk->makeKnownLinkObj( $searchTitle, wfMsg('search-interwiki-more'), wfArrayToCGI(array('search' => $query, 'fulltext' => 'Search'))); $out .= "
    {$searchLink}{$caption}
    \n
    {$searchLink}{$caption}
    \n