From ce597275536c68b1f1f64332a39dcb6d1550c0e6 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Niklas=20Laxstr=C3=B6m?= Date: Fri, 22 Apr 2011 16:13:58 +0000 Subject: [PATCH] Allow extensions to customize the search forms. This required some cleanup and refactoring to special:search and search engine. Should be fully backwards compatible. Lightly tested, but only with MySQL search backend. Introduces concept of search profiles, which replace long list of namespaces in the url. --- docs/hooks.txt | 12 ++ includes/search/SearchEngine.php | 36 +++++- includes/search/SearchMySQL.php | 27 +++-- includes/specials/SpecialSearch.php | 179 +++++++++++++++++----------- 4 files changed, 175 insertions(+), 79 deletions(-) diff --git a/docs/hooks.txt b/docs/hooks.txt index 8601c49790..66895d4e3a 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -1665,6 +1665,18 @@ target doesn't exist 'SpecialSearchProfiles': allows modification of search profiles &$profiles: profiles, which can be modified. +'SpecialSearchProfileForm': allows modification of search profile forms +$search: special page object +&$form: String: form html +$profile: String: current search profile +$term: String: search term +$opts: Array: key => value of hidden options for inclusion in custom forms + +'SpecialSearchSetupEngine': allows passing custom data to search engine +$search: special page object +$profile: String: current search profile +$engine: the search engine + 'SpecialSearchResults': called before search result display when there are matches $term: string of search term diff --git a/includes/search/SearchEngine.php b/includes/search/SearchEngine.php index ba203cacfa..4ef728a2e9 100644 --- a/includes/search/SearchEngine.php +++ b/includes/search/SearchEngine.php @@ -22,6 +22,9 @@ class SearchEngine { var $namespaces = array( NS_MAIN ); var $showRedirects = false; + /// Feature values + protected $features = array(); + /** * @var DatabaseBase */ @@ -59,9 +62,38 @@ class SearchEngine { return null; } - /** If this search backend can list/unlist redirects */ + /** + * If this search backend can list/unlist redirects + * @deprecated Call supports( 'list-redirects' ); + */ function acceptListRedirects() { - return true; + return $this->supports( 'list-redirects' ); + } + + /** + * @since 1.18 + * @param $feature String + * @return Boolean + */ + public function supports( $feature ) { + switch( $feature ) { + case 'list-redirects': + return true; + case 'title-suffix-filter': + default: + return false; + } + } + + /** + * Way to pass custom data for engines + * @since 1.18 + * @param $feature String + * @param $data Mixed + * @return Noolean + */ + public function setFeatureData( $feature, $data ) { + $this->features[$feature] = $data; } /** diff --git a/includes/search/SearchMySQL.php b/includes/search/SearchMySQL.php index cc41795579..4948ec72b8 100644 --- a/includes/search/SearchMySQL.php +++ b/includes/search/SearchMySQL.php @@ -199,15 +199,28 @@ class SearchMySQL extends SearchEngine { return new MySQLSearchResultSet( $resultSet, $this->searchTerms, $total ); } + public function supports( $feature ) { + switch( $feature ) { + case 'list-redirects': + case 'title-suffix-filter': + return true; + default: + return false; + } + } /** - * Add redirect conditions + * Add special conditions * @param $query Array - * @since 1.18 (changed) + * @since 1.18 */ - function queryRedirect( &$query ) { - if( !$this->showRedirects ) { - $query['conds']['page_is_redirect'] = 0; + protected function queryFeatures( &$query ) { + foreach ( $this->features as $feature => $value ) { + if ( $feature === 'list-redirects' && !$value ) { + $query['conds']['page_is_redirect'] = 0; + } elseif( $feature === 'title-suffix-filter' && $value ) { + $query['conds'][] = 'page_title' . $this->db->buildLike( $this->db->anyString(), $value ); + } } } @@ -253,7 +266,7 @@ class SearchMySQL extends SearchEngine { ); $this->queryMain( $query, $filteredTerm, $fulltext ); - $this->queryRedirect( $query ); + $this->queryFeatures( $query ); $this->queryNamespaces( $query ); $this->limitResult( $query ); @@ -301,7 +314,7 @@ class SearchMySQL extends SearchEngine { 'joins' => array(), ); - $this->queryRedirect( $query ); + $this->queryFeatures( $query ); $this->queryNamespaces( $query ); return $query; diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index 79ca41cc50..25ceda63e8 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -28,6 +28,13 @@ * @ingroup SpecialPage */ class SpecialSearch extends SpecialPage { + /// Current search profile + protected $profile; + + /// Search engine + protected $searchEngine; + + const NAMESPACES_CURRENT = 'sense'; public function __construct() { parent::__construct( 'Search' ); @@ -75,14 +82,40 @@ class SpecialSearch extends SpecialPage { public function load( &$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 ); + + + # Extract manually requested namespaces + $nslist = $this->powerSearch( $request ); + $this->profile = $profile = $request->getVal( 'profile', null ); + $profiles = $this->getSearchProfiles(); + if ( $profile === null) { + // BC with old request format + $this->profile = 'advanced'; + if ( count( $nslist ) ) { + foreach( $profiles as $key => $data ) { + if ( $nslist === $data['namespaces'] && $key !== 'advanced') { + $this->profile = $key; + } + } + $this->namespaces = $nslist; + } else { + $this->namespaces = SearchEngine::userNamespaces( $user ); + } + } elseif ( $profile === 'advanced' ) { + $this->namespaces = $nslist; + } else { + if ( isset( $profiles[$profile]['namespaces'] ) ) { + $this->namespaces = $profiles[$profile]['namespaces']; + } else { + // Unknown profile requested + $this->profile = 'default'; + $this->namespaces = $profiles['default']['namespaces']; + } } - $this->searchRedirects = $request->getCheck( 'redirs' ); - $this->searchAdvanced = $request->getVal( 'advanced' ); - $this->active = 'advanced'; + + // Redirects defaults to true, but we don't know whether it was ticked of or just missing + $default = $request->getBool( 'profile' ) ? 0 : 1; + $this->searchRedirects = $request->getBool( 'redirs', $default ) ? 1 : 0; $this->sk = $user->getSkin(); $this->didYouMeanHtml = ''; # html of did you mean... link $this->fulltext = $request->getVal('fulltext'); @@ -139,14 +172,16 @@ class SpecialSearch extends SpecialPage { $sk = $wgUser->getSkin(); - $this->searchEngine = SearchEngine::create(); - $search =& $this->searchEngine; + $search = $this->getSearchEngine(); $search->setLimitOffset( $this->limit, $this->offset ); $search->setNamespaces( $this->namespaces ); - $search->showRedirects = $this->searchRedirects; + $search->showRedirects = $this->searchRedirects; // BC + $search->setFeatureData( 'list-redirects', $this->searchRedirects ); $search->prefix = $this->mPrefix; $term = $search->transformSearchTerm($term); + wfRunHooks( 'SpecialSearchSetupEngine', array( $this, $this->profile, $search ) ); + $this->setupPage( $term ); if( $wgDisableTextSearch ) { @@ -216,7 +251,7 @@ class SpecialSearch extends SpecialPage { Xml::openElement( 'form', array( - 'id' => ( $this->searchAdvanced ? 'powersearch' : 'search' ), + 'id' => ( $this->profile === 'advanced' ? 'powersearch' : 'search' ), 'method' => 'get', 'action' => $wgScript ) @@ -225,7 +260,7 @@ class SpecialSearch extends SpecialPage { $wgOut->addHtml( Xml::openElement( 'table', array( 'id'=>'mw-search-top-table', 'border'=>0, 'cellpadding'=>0, 'cellspacing'=>0 ) ) . Xml::openElement( 'tr' ) . - Xml::openElement( 'td' ) . "\n" . + Xml::openElement( 'td' ) . "\n" . $this->shortDialog( $term ) . Xml::closeElement('td') . Xml::closeElement('tr') . @@ -241,10 +276,8 @@ class SpecialSearch extends SpecialPage { $filePrefix = $wgContLang->getFormattedNsText(NS_FILE).':'; if( trim( $term ) === '' || $filePrefix === trim( $term ) ) { - $wgOut->addHTML( $this->formHeader($term, 0, 0)); - if( $this->searchAdvanced ) { - $wgOut->addHTML( $this->powerSearchBox( $term ) ); - } + $wgOut->addHTML( $this->formHeader( $term, 0, 0 ) ); + $wgOut->addHtml( $this->getProfileForm( $this->profile, $term ) ); $wgOut->addHTML( '' ); // Empty query -- straight view of search form wfProfileOut( __METHOD__ ); @@ -271,10 +304,9 @@ class SpecialSearch extends SpecialPage { $totalRes += $textMatches->getTotalHits(); // show number of results and current offset - $wgOut->addHTML( $this->formHeader($term, $num, $totalRes)); - if( $this->searchAdvanced ) { - $wgOut->addHTML( $this->powerSearchBox( $term ) ); - } + $wgOut->addHTML( $this->formHeader( $term, $num, $totalRes ) ); + $wgOut->addHtml( $this->getProfileForm( $this->profile, $term ) ); + $wgOut->addHtml( Xml::closeElement( 'form' ) ); $wgOut->addHtml( "
" ); @@ -361,21 +393,10 @@ class SpecialSearch extends SpecialPage { */ protected function setupPage( $term ) { global $wgOut; - // Figure out the active search profile header - if( $this->searchAdvanced ) { - $this->active = 'advanced'; - } else { - $profiles = $this->getSearchProfiles(); - foreach( $profiles as $key => $data ) { - if ( $this->namespaces == $data['namespaces'] && $key != 'advanced') - $this->active = $key; - } - - } # Should advanced UI be used? - $this->searchAdvanced = ($this->active === 'advanced'); - if( !empty( $term ) ) { + $this->searchAdvanced = ($this->profile === 'advanced'); + if( strval( $term ) !== '' ) { $wgOut->setPageTitle( wfMsg( 'searchresults') ); $wgOut->setHTMLTitle( wfMsg( 'pagetitle', wfMsg( 'searchresults-title', $term ) ) ); } @@ -398,6 +419,7 @@ class SpecialSearch extends SpecialPage { $arr[] = $ns; } } + return $arr; } @@ -412,8 +434,8 @@ class SpecialSearch extends SpecialPage { $opt['ns' . $n] = 1; } $opt['redirs'] = $this->searchRedirects ? 1 : 0; - if( $this->searchAdvanced ) { - $opt['advanced'] = $this->searchAdvanced; + if( $this->profile ) { + $opt['profile'] = $this->profile; } return $opt; } @@ -744,14 +766,28 @@ class SpecialSearch extends SpecialPage { return $out; } + protected function getProfileForm( $profile, $term ) { + // Hidden stuff + $opts = array(); + $opts['redirs'] = $this->searchRedirects; + $opts['profile'] = $this->profile; + + if ( $profile === 'advanced' ) { + return $this->powerSearchBox( $term, $opts ); + } else { + $form = ''; + wfRunHooks( 'SpecialSearchProfileForm', array( $this, &$form, $profile, $term, $opts ) ); + return $form; + } + } /** - * Generates the power search box at bottom of [[Special:Search]] + * Generates the power search box at [[Special:Search]] * * @param $term String: search term * @return String: HTML form */ - protected function powerSearchBox( $term ) { + protected function powerSearchBox( $term, $opts ) { // Groups namespaces into rows according to subject $rows = array(); foreach( SearchEngine::searchableNamespaces() as $namespace => $name ) { @@ -793,13 +829,15 @@ class SpecialSearch extends SpecialPage { } // Show redirects check only if backend supports it $redirects = ''; - if( $this->searchEngine->acceptListRedirects() ) { + if( $this->getSearchEngine()->supports( 'list-redirects' ) ) { $redirects = - Xml::check( - 'redirs', $this->searchRedirects, array( 'value' => '1', 'id' => 'redirs' ) - ) . - ' ' . - Xml::label( wfMsg( 'powersearch-redir' ), 'redirs' ); + Xml::checkLabel( wfMsg( 'powersearch-redir' ), 'redirs', 'redirs', $this->searchRedirects ); + } + + $hidden = ''; + unset( $opts['redirs'] ); + foreach( $opts as $key => $value ) { + $hidden .= Html::hidden( $key, $value ); } // Return final output return @@ -835,10 +873,7 @@ class SpecialSearch extends SpecialPage { Xml::element( 'div', array( 'class' => 'divider' ), '', false ) . $namespaceTables . Xml::element( 'div', array( 'class' => 'divider' ), '', false ) . - $redirects . - Html::hidden( 'title', SpecialPage::getTitleFor( 'Search' )->getPrefixedText() ) . - Html::hidden( 'advanced', $this->searchAdvanced ) . - Html::hidden( 'fulltext', 'Advanced search' ) . + $redirects . $hidden . Xml::closeElement( 'fieldset' ); } @@ -876,15 +911,15 @@ class SpecialSearch extends SpecialPage { 'advanced' => array( 'message' => 'searchprofile-advanced', 'tooltip' => 'searchprofile-advanced-tooltip', - 'namespaces' => $this->namespaces, - 'parameters' => array( 'advanced' => 1 ), + 'namespaces' => self::NAMESPACES_CURRENT, ) ); wfRunHooks( 'SpecialSearchProfiles', array( &$profiles ) ); foreach( $profiles as &$data ) { - sort($data['namespaces']); + if ( !is_array( $data['namespaces'] ) ) continue; + sort( $data['namespaces'] ); } return $profiles; @@ -907,19 +942,24 @@ class SpecialSearch extends SpecialPage { $out .= Xml::openElement( 'div', array( 'class' => 'search-types' ) ); $out .= Xml::openElement( 'ul' ); foreach ( $profiles as $id => $profile ) { + if ( !isset( $profile['parameters'] ) ) { + $profile['parameters'] = array(); + } + $profile['parameters']['profile'] = $id; + $tooltipParam = isset( $profile['namespace-messages'] ) ? $wgLang->commaList( $profile['namespace-messages'] ) : null; $out .= Xml::tags( 'li', array( - 'class' => $this->active == $id ? 'current' : 'normal' + 'class' => $this->profile === $id ? 'current' : 'normal' ), $this->makeSearchLink( $bareterm, - $profile['namespaces'], + array(), wfMsg( $profile['message'] ), wfMsg( $profile['tooltip'], $tooltipParam ), - isset( $profile['parameters'] ) ? $profile['parameters'] : array() + $profile['parameters'] ) ); } @@ -949,24 +989,14 @@ class SpecialSearch extends SpecialPage { $out .= Xml::element( 'div', array( 'style' => 'clear:both' ), '', false ); $out .= Xml::closeElement('div'); - // Adds hidden namespace fields - if ( !$this->searchAdvanced ) { - foreach( $this->namespaces as $ns ) { - $out .= Html::hidden( "ns{$ns}", '1' ); - } - } - return $out; } protected function shortDialog( $term ) { - $searchTitle = SpecialPage::getTitleFor( 'Search' ); - $out = Html::hidden( 'title', $searchTitle->getPrefixedText() ) . "\n"; - // Keep redirect setting - $out .= Html::hidden( "redirs", (int)$this->searchRedirects ) . "\n"; + $out = Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n"; // Term box $out .= Html::input( 'search', $term, 'search', array( - 'id' => $this->searchAdvanced ? 'powerSearchText' : 'searchText', + 'id' => $this->profile === 'advanced' ? 'powerSearchText' : 'searchText', 'size' => '50', 'autofocus' ) ) . "\n"; @@ -979,20 +1009,19 @@ class SpecialSearch extends SpecialPage { * Make a search link with some target namespaces * * @param $term String - * @param $namespaces Array + * @param $namespaces Array ignored * @param $label String: link's text * @param $tooltip String: link's tooltip * @param $params Array: query string parameters * @return String: HTML fragment */ - protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params=array() ) { + protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params = array() ) { $opt = $params; foreach( $namespaces as $n ) { $opt['ns' . $n] = 1; } - $opt['redirs'] = $this->searchRedirects ? 1 : 0; + $opt['redirs'] = $this->searchRedirects; - $st = SpecialPage::getTitleFor( 'Search' ); $stParams = array_merge( array( 'search' => $term, @@ -1004,7 +1033,7 @@ class SpecialSearch extends SpecialPage { return Xml::element( 'a', array( - 'href' => $st->getLocalURL( $stParams ), + 'href' => $this->getTitle()->getLocalURL( $stParams ), 'title' => $tooltip, 'onmousedown' => 'mwSearchHeaderClick(this);', 'onkeydown' => 'mwSearchHeaderClick(this);'), @@ -1044,4 +1073,14 @@ class SpecialSearch extends SpecialPage { } return false; } + + /** + * @since 1.18 + */ + public function getSearchEngine() { + if ( $this->searchEngine === null ) { + $this->searchEngine = SearchEngine::create(); + } + return $this->searchEngine; + } } -- 2.20.1