From 915af4890f48ba9549414b911517d3a98b971dad Mon Sep 17 00:00:00 2001 From: Aaron Schulz Date: Mon, 17 Nov 2008 04:56:14 +0000 Subject: [PATCH] * Re-commit new search UI code, disabled by default. Old form messages still used and thus kept. * Update old-fashioned profile calls --- includes/AutoLoader.php | 1 + includes/DefaultSettings.php | 17 +- includes/SearchEngine.php | 55 +- includes/specials/SpecialSearch.php | 825 +++++++++++++++++++++++++++- languages/messages/MessagesEn.php | 104 ++-- maintenance/language/messages.inc | 20 + skins/common/shared.css | 14 + skins/monobook/main.css | 4 + 8 files changed, 966 insertions(+), 74 deletions(-) diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index c26916cf1a..b531599c24 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -485,6 +485,7 @@ $wgAutoloadLocalClasses = array( 'SpecialRecentchanges' => 'includes/specials/SpecialRecentchanges.php', 'SpecialRecentchangeslinked' => 'includes/specials/SpecialRecentchangeslinked.php', 'SpecialSearch' => 'includes/specials/SpecialSearch.php', + 'SpecialSearchOld' => 'includes/specials/SpecialSearch.php', 'SpecialVersion' => 'includes/specials/SpecialVersion.php', 'UncategorizedCategoriesPage' => 'includes/specials/SpecialUncategorizedcategories.php', 'UncategorizedPagesPage' => 'includes/specials/SpecialUncategorizedpages.php', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index d0f918d0ba..32c04fc308 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -1436,7 +1436,7 @@ $wgCacheEpoch = '20030516000000'; * to ensure that client-side caches don't keep obsolete copies of global * styles. */ -$wgStyleVersion = '183'; +$wgStyleVersion = '184'; # Server-side caching: @@ -1923,6 +1923,21 @@ $wgNamespacesToBeSearchedDefault = array( NS_MAIN => true, ); +/** + * Additional namespaces to those in $wgNamespacesToBeSearchedDefault that + * will be added to default search for "project" page inclusive searches + * + * Same format as $wgNamespacesToBeSearchedDefault + */ +$wgNamespacesToBeSearchedProject = array( + NS_USER => true, + NS_PROJECT => true, + NS_HELP => true, + NS_CATEGORY => true, +); + +$wgUseOldSearchUI = true; // temp testing variable + /** * Site notice shown at the top of each page * diff --git a/includes/SearchEngine.php b/includes/SearchEngine.php index 85071d5c96..032865e20d 100644 --- a/includes/SearchEngine.php +++ b/includes/SearchEngine.php @@ -43,6 +43,11 @@ class SearchEngine { return null; } + /** If this search backend can list/unlist redirects */ + function acceptListRedirects() { + return true; + } + /** * If an exact title match can be find, or a very slightly close match, * return the title. If no match, returns NULL. @@ -262,7 +267,51 @@ class SearchEngine { return array_keys($wgNamespacesToBeSearchedDefault, true); } - + + /** + * Get a list of namespace names useful for showing in tooltips + * and preferences + * + * @param unknown_type $namespaces + */ + public static function namespacesAsText( $namespaces ){ + global $wgContLang; + + $formatted = array_map( array($wgContLang,'getFormattedNsText'), $namespaces ); + foreach( $formatted as $key => $ns ){ + if ( empty($ns) ) + $formatted[$key] = wfMsg( 'blanknamespace' ); + } + return $formatted; + } + + /** + * An array of "project" namespaces indexes typically searched + * by logged-in users + * + * @return array + * @static + */ + public static function projectNamespaces() { + global $wgNamespacesToBeSearchedDefault, $wgNamespacesToBeSearchedProject; + + return array_keys( $wgNamespacesToBeSearchedProject, true ); + } + + /** + * An array of "project" namespaces indexes typically searched + * by logged-in users in addition to the default namespaces + * + * @return array + * @static + */ + public static function defaultAndProjectNamespaces() { + global $wgNamespacesToBeSearchedDefault, $wgNamespacesToBeSearchedProject; + + return array_keys( $wgNamespacesToBeSearchedDefault + + $wgNamespacesToBeSearchedProject, true); + } + /** * Return a 'cleaned up' search string * @@ -485,11 +534,11 @@ class SearchResult { var $mRevision = null; var $mImage = null; - function SearchResult( $row ) { + function __construct( $row ) { $this->mTitle = Title::makeTitle( $row->page_namespace, $row->page_title ); if( !is_null($this->mTitle) ){ $this->mRevision = Revision::newFromTitle( $this->mTitle ); - if($this->mTitle->getNamespace() == NS_IMAGE) + if( $this->mTitle->getNamespace() === NS_IMAGE ) $this->mImage = wfFindFile( $this->mTitle ); } } diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index 83b855bc9b..7b5a533490 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -29,17 +29,19 @@ * @param $par String: (default '') */ function wfSpecialSearch( $par = '' ) { - global $wgRequest, $wgUser; + 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 ); $search = str_replace( "\n", " ", $wgRequest->getText( 'search', $titleParam ) ); - $searchPage = new SpecialSearch( $wgRequest, $wgUser ); + $class = $wgUseOldSearchUI ? 'SpecialSearchOld' : 'SpecialSearch'; + $searchPage = new $class( $wgRequest, $wgUser ); if( $wgRequest->getVal( 'fulltext' ) || !is_null( $wgRequest->getVal( 'offset' )) - || !is_null( $wgRequest->getVal( 'searchx' ))) { + || !is_null( $wgRequest->getVal( 'searchx' )) ) + { $searchPage->showResults( $search, 'search' ); } else { $searchPage->goResult( $search ); @@ -60,7 +62,779 @@ class SpecialSearch { * @param User $user * @public */ - function SpecialSearch( &$request, &$user ) { + function __construct( &$request, &$user ) { + list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' ); + + $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'); + } + + /** + * If an exact title match can be found, jump straight ahead to it. + * @param string $term + * @public + */ + function goResult( $term ) { + global $wgOut, $wgGoToEdit; + + $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 ) ) { + 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 ) { + wfProfileIn( __METHOD__ ); + global $wgOut, $wgUser; + $sk = $wgUser->getSkin(); + + $this->setupPage( $term ); + $this->searchEngine = SearchEngine::create(); + + $t = Title::newFromText( $term ); + + $wgOut->addHtml( + Xml::openElement( 'table', array( 'border'=>0, 'cellpadding'=>0, 'cellspacing'=>0 ) ) . + Xml::openElement( 'tr' ) . + Xml::openElement( 'td' ) . "\n" + ); + if( $this->searchAdvanced ) { + $wgOut->addHTML( $this->powerSearchBox( $term ) ); + $showMenu = false; + } else { + $wgOut->addHTML( $this->shortDialog( $term ) ); + $showMenu = true; + } + $wgOut->addHtml( + Xml::closeElement('td') . + Xml::closeElement('tr') . + Xml::closeElement('table') + ); + + if( '' === trim( $term ) ) { + // Empty query -- straight view of search form + wfProfileOut( __METHOD__ ); + return; + } + + global $wgDisableTextSearch; + if( $wgDisableTextSearch ) { + global $wgSearchForwardUrl; + if( $wgSearchForwardUrl ) { + $url = str_replace( '$1', urlencode( $term ), $wgSearchForwardUrl ); + $wgOut->redirect( $url ); + 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; + } + + $search =& $this->searchEngine; + $search->setLimitOffset( $this->limit, $this->offset ); + $search->setNamespaces( $this->namespaces ); + $search->showRedirects = $this->searchRedirects; + $rewritten = $search->replacePrefixes($term); + + $titleMatches = $search->searchTitle( $rewritten ); + + // Sometimes the search engine knows there are too many hits + if( $titleMatches instanceof SearchResultTooMany ) { + $wgOut->addWikiText( '==' . wfMsg( 'toomanymatches' ) . "==\n" ); + wfProfileOut( __METHOD__ ); + return; + } + + $textMatches = $search->searchText( $rewritten ); + + // did you mean... suggestions + if( $textMatches && $textMatches->hasSuggestion() ) { + $st = SpecialPage::getTitleFor( 'Search' ); + $stParams = wfArrayToCGI( + array( 'search' => $textMatches->getSuggestionQuery(), 'fulltext' => wfMsg('search') ), + $this->powerSearchOptions() + ); + $suggestLink = ''. + $textMatches->getSuggestionSnippet().''; + + $wgOut->addHTML('
'.wfMsg('search-suggest',$suggestLink).'
'); + } + + // 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() ) ); + } + } + + // show number of results + $numTitleMatches = $titleMatches ? $titleMatches->numRows() : 0; + $numTextMatches = $textMatches ? $textMatches->numRows() : 0; + $highestNum = max( $numTitleMatches, $numTextMatches ); + // Total query matches (possible false positives) + $num = $numTitleMatches + $numTextMatches; + // Get total actual results + $totalNum = 0; + if( $titleMatches && !is_null($titleMatches->getTotalHits()) ) + $totalNum += $titleMatches->getTotalHits(); + if( $textMatches && !is_null($textMatches->getTotalHits()) ) + $totalNum += $textMatches->getTotalHits(); + if( $num > 0 ) { + if( $totalNum > 0 ) { + $top = wfMsgExt('showingresultstotal', array( 'parseinline' ), + $this->offset+1, $this->offset+$num, $totalNum, $num ); + } elseif( $num >= $this->limit ) { + $top = wfShowingResults( $this->offset, $this->limit ); + } else { + $top = wfShowingResultsNum( $this->offset, $this->limit, $num ); + } + $wgOut->addHTML( "

{$top}

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

{$prevnext}

\n" ); + wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) ); + } else { + wfRunHooks( 'SpecialSearchNoResults', array( $term ) ); + } + + $wgOut->addHtml( "
" ); + + if( $titleMatches ) { + if( $titleMatches->numRows() ) { + $wgOut->wrapWikiMsg( "==$1==\n", 'titlematches' ); + $wgOut->addHTML( $this->showMatches( $titleMatches ) ); + } + $titleMatches->free(); + } + + if( $textMatches ) { + // output appropriate heading + if( $textMatches->numRows() ) { + if($titleMatches) + $wgOut->wrapWikiMsg( "==$1==\n", 'textmatches' ); + else // if no title matches the heading is redundant + $wgOut->addHTML("
"); + } elseif( $num == 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( $textMatches->numRows() ) + $wgOut->addHTML( $this->showMatches( $textMatches ) ); + + $textMatches->free(); + } + + if( $num == 0 ) { + $wgOut->addWikiMsg( 'search-nonefound' ); + } + + $wgOut->addHtml( "
" ); + + if( $num || $this->offset ) { + $wgOut->addHTML( "

{$prevnext}

\n" ); + } + wfProfileOut( __METHOD__ ); + } + + /** + * + */ + protected function setupPage( $term ) { + global $wgOut; + 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 ) { + wfProfileIn( __METHOD__ ); + global $wgUser, $wgContLang, $wgLang; + + if( $result->isBrokenTitle() ) { + wfProfileOut( __METHOD__ ); + return "\n"; + } + + $t = $result->getTitle(); + $sk = $wgUser->getSkin(); + + $link = $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',$sk->makeKnownLinkObj( $redirectTitle, $redirectText)) + .""; + $section = ''; + if( !is_null($sectionTitle) ) + $section = "" + .wfMsg('search-section', $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' ), + $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 = ' -- '. + wfMsg('search-relatedarticle').''; + } + + // Include a thumbnail for media files... + if( $t->getNamespace() == NS_IMAGE ) { + $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 ) { + wfProfileIn( __METHOD__ ); + + global $wgContLang; + $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 + global $wgContLang; + $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 $wgUser, $wgContLang, $wgLang; + + if( $result->isBrokenTitle() ) { + wfProfileOut( __METHOD__ ); + return "\n"; + } + + $t = $result->getTitle(); + $sk = $wgUser->getSkin(); + + $link = $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',$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 = $sk->makeKnownLinkObj( $searchTitle, wfMsg('search-interwiki-more'), + wfArrayToCGI(array('search' => $query, 'fulltext' => 'Search'))); + $out .= "
    + {$searchLink}{$caption}
    \n