Merge "SpecialSearchResults hooks does not use by-ref anymore"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 17 Jan 2017 23:50:03 +0000 (23:50 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 17 Jan 2017 23:50:03 +0000 (23:50 +0000)
autoload.php
includes/specials/SpecialSearch.php
includes/widget/search/SearchFormWidget.php [new file with mode: 0644]
tests/phpunit/includes/specials/SpecialSearchTest.php

index c8033cf..c8740ea 100644 (file)
@@ -933,6 +933,7 @@ $wgAutoloadLocalClasses = [
        'MediaWiki\\Widget\\NamespaceInputWidget' => __DIR__ . '/includes/widget/NamespaceInputWidget.php',
        'MediaWiki\\Widget\\SearchInputWidget' => __DIR__ . '/includes/widget/SearchInputWidget.php',
        'MediaWiki\\Widget\\Search\\FullSearchResultWidget' => __DIR__ . '/includes/widget/search/FullSearchResultWidget.php',
+       'MediaWiki\\Widget\\Search\\SearchFormWidget' => __DIR__ . '/includes/widget/search/SearchFormWidget.php',
        'MediaWiki\\Widget\\Search\\SearchResultWidget' => __DIR__ . '/includes/widget/search/SearchResultWidget.php',
        'MediaWiki\\Widget\\Search\\SimpleSearchResultWidget' => __DIR__ . '/includes/widget/search/SimpleSearchResultWidget.php',
        'MediaWiki\\Widget\\TitleInputWidget' => __DIR__ . '/includes/widget/TitleInputWidget.php',
index 255e618..37d86c3 100644 (file)
@@ -101,35 +101,29 @@ class SpecialSearch extends SpecialPage {
         */
        public function execute( $par ) {
                $request = $this->getRequest();
+               $out = $this->getOutput();
 
                // Fetch the search term
-               $search = str_replace( "\n", " ", $request->getText( 'search' ) );
+               $term = str_replace( "\n", " ", $request->getText( 'search' ) );
 
                // Historically search terms have been accepted not only in the search query
                // parameter, but also as part of the primary url. This can have PII implications
                // in releasing page view data. As such issue a 301 redirect to the correct
                // URL.
-               if ( strlen( $par ) && !strlen( $search ) ) {
+               if ( strlen( $par ) && !strlen( $term ) ) {
                        $query = $request->getValues();
                        unset( $query['title'] );
                        // Strip underscores from title parameter; most of the time we'll want
                        // text form here. But don't strip underscores from actual text params!
                        $query['search'] = str_replace( '_', ' ', $par );
-                       $this->getOutput()->redirect( $this->getPageTitle()->getFullURL( $query ), 301 );
+                       $out->redirect( $this->getPageTitle()->getFullURL( $query ), 301 );
                        return;
                }
 
-               $this->setHeaders();
-               $this->outputHeader();
-               $out = $this->getOutput();
-               $out->allowClickjacking();
-               $out->addModuleStyles( [
-                       'mediawiki.special', 'mediawiki.special.search.styles', 'mediawiki.ui', 'mediawiki.ui.button',
-                       'mediawiki.ui.input', 'mediawiki.widgets.SearchInputWidget.styles',
-               ] );
-               $this->addHelpLink( 'Help:Searching' );
-
+               // Need to load selected namespaces before handling nsRemember
                $this->load();
+               // TODO: This performs database actions on GET request, which is going to
+               // be a problem for our multi-datacenter work.
                if ( !is_null( $request->getVal( 'nsRemember' ) ) ) {
                        $this->saveNamespaces();
                        // Remove the token from the URL to prevent the user from inadvertently
@@ -141,16 +135,48 @@ class SpecialSearch extends SpecialPage {
                        return;
                }
 
-               $out->addJsConfigVars( [ 'searchTerm' => $search ] );
                $this->searchEngineType = $request->getVal( 'srbackend' );
-
-               if ( $request->getVal( 'fulltext' )
-                       || !is_null( $request->getVal( 'offset' ) )
+               if (
+                       !$request->getVal( 'fulltext' ) &&
+                       $request->getVal( 'offset' ) === null
                ) {
-                       $this->showResults( $search );
-               } else {
-                       $this->goResult( $search );
+                       $url = $this->goResult( $term );
+                       if ( $url !== null ) {
+                               // successful 'go'
+                               $out->redirect( $url );
+                               return;
+                       }
                }
+
+               $this->setupPage( $term );
+
+               if ( $this->getConfig()->get( 'DisableTextSearch' ) ) {
+                       $searchForwardUrl = $this->getConfig()->get( 'SearchForwardUrl' );
+                       if ( $searchForwardUrl ) {
+                               $url = str_replace( '$1', urlencode( $term ), $searchForwardUrl );
+                               $out->redirect( $url );
+                       } else {
+                               $out->addHTML(
+                                       "<fieldset>" .
+                                               "<legend>" .
+                                                       $this->msg( 'search-external' )->escaped() .
+                                               "</legend>" .
+                                               "<p class='mw-searchdisabled'>" .
+                                                       $this->msg( 'searchdisabled' )->escaped() .
+                                               "</p>" .
+                                               $this->msg( 'googlesearch' )->rawParams(
+                                                       htmlspecialchars( $term ),
+                                                       'UTF-8',
+                                                       $this->msg( 'searchbutton' )->escaped()
+                                               )->text() .
+                                       "</fieldset>"
+                               );
+                       }
+
+                       return;
+               }
+
+               $this->showResults( $term );
        }
 
        /**
@@ -209,32 +235,25 @@ class SpecialSearch extends SpecialPage {
         * If an exact title match can be found, jump straight ahead to it.
         *
         * @param string $term
+        * @return string|null The url to redirect to, or null if no redirect.
         */
        public function goResult( $term ) {
-               $this->setupPage( $term );
-               # Try to go to page as entered.
-               $title = Title::newFromText( $term );
                # If the string cannot be used to create a title
-               if ( is_null( $title ) ) {
-                       $this->showResults( $term );
-
-                       return;
+               if ( is_null( Title::newFromText( $term ) ) ) {
+                       return null;
                }
                # If there's an exact or very near match, jump right there.
                $title = $this->getSearchEngine()
                        ->getNearMatcher( $this->getConfig() )->getNearMatch( $term );
-
-               if ( !is_null( $title ) &&
-                       Hooks::run( 'SpecialSearchGoResult', [ $term, $title, &$url ] )
-               ) {
-                       if ( $url === null ) {
-                               $url = $title->getFullURL();
-                       }
-                       $this->getOutput()->redirect( $url );
-
-                       return;
+               if ( is_null( $title ) ) {
+                       return null;
                }
-               $this->showResults( $term );
+               $url = null;
+               if ( !Hooks::run( 'SpecialSearchGoResult', [ $term, $title, &$url ] ) ) {
+                       return null;
+               }
+
+               return $url === null ? $title->getFullURL() : $url;
        }
 
        /**
@@ -243,6 +262,33 @@ class SpecialSearch extends SpecialPage {
        public function showResults( $term ) {
                global $wgContLang;
 
+               if ( $this->searchEngineType !== null ) {
+                       $this->setExtraParam( 'srbackend', $this->searchEngineType );
+               }
+
+               $out = $this->getOutput();
+               $formWidget = new MediaWiki\Widget\Search\SearchFormWidget(
+                       $this,
+                       $this->searchConfig,
+                       $this->getSearchProfiles()
+               );
+               $filePrefix = $wgContLang->getFormattedNsText( NS_FILE ) . ':';
+               if ( trim( $term ) === '' || $filePrefix === trim( $term ) ) {
+                       // Empty query -- straight view of search form
+                       if ( !Hooks::run( 'SpecialSearchResultsPrepend', [ $this, $out, $term ] ) ) {
+                               # Hook requested termination
+                               return;
+                       }
+                       $out->enableOOUI();
+                       // The form also contains the 'Showing results 0 - 20 of 1234' so we can
+                       // only do the form render here for the empty $term case. Rendering
+                       // the form when a search is provided is repeated below.
+                       $out->addHTML( $formWidget->render(
+                               $this->profile, $term, 0, 0, $this->offset, $this->isPowerSearch()
+                       ) );
+                       return;
+               }
+
                $search = $this->getSearchEngine();
                $search->setFeatureData( 'rewrite', $this->runSuggestion );
                $search->setLimitOffset( $this->limit, $this->offset );
@@ -251,34 +297,8 @@ class SpecialSearch extends SpecialPage {
                $term = $search->transformSearchTerm( $term );
 
                Hooks::run( 'SpecialSearchSetupEngine', [ $this, $this->profile, $search ] );
-
-               $this->setupPage( $term );
-
-               $out = $this->getOutput();
-
-               if ( $this->getConfig()->get( 'DisableTextSearch' ) ) {
-                       $searchFowardUrl = $this->getConfig()->get( 'SearchForwardUrl' );
-                       if ( $searchFowardUrl ) {
-                               $url = str_replace( '$1', urlencode( $term ), $searchFowardUrl );
-                               $out->redirect( $url );
-                       } else {
-                               $out->addHTML(
-                                       Xml::openElement( 'fieldset' ) .
-                                       Xml::element( 'legend', null, $this->msg( 'search-external' )->text() ) .
-                                       Xml::element(
-                                               'p',
-                                               [ 'class' => 'mw-searchdisabled' ],
-                                               $this->msg( 'searchdisabled' )->text()
-                                       ) .
-                                       $this->msg( 'googlesearch' )->rawParams(
-                                               htmlspecialchars( $term ),
-                                               'UTF-8',
-                                               $this->msg( 'searchbutton' )->escaped()
-                                       )->text() .
-                                       Xml::closeElement( 'fieldset' )
-                               );
-                       }
-
+               if ( !Hooks::run( 'SpecialSearchResultsPrepend', [ $this, $out, $term ] ) ) {
+                       # Hook requested termination
                        return;
                }
 
@@ -298,33 +318,6 @@ class SpecialSearch extends SpecialPage {
                        $textMatches = $textStatus->getValue();
                }
 
-               // did you mean... suggestions
-               $didYouMeanHtml = '';
-               if ( $showSuggestion && $textMatches ) {
-                       if ( $textMatches->hasRewrittenQuery() ) {
-                               $didYouMeanHtml = $this->getDidYouMeanRewrittenHtml( $term, $textMatches );
-                       } elseif ( $textMatches->hasSuggestion() ) {
-                               $didYouMeanHtml = $this->getDidYouMeanHtml( $textMatches );
-                       }
-               }
-
-               if ( !Hooks::run( 'SpecialSearchResultsPrepend', [ $this, $out, $term ] ) ) {
-                       # Hook requested termination
-                       return;
-               }
-
-               // start rendering the page
-               $out->addHTML(
-                       Xml::openElement(
-                               'form',
-                               [
-                                       'id' => ( $this->isPowerSearch() ? 'powersearch' : 'search' ),
-                                       'method' => 'get',
-                                       'action' => wfScript(),
-                               ]
-                       )
-               );
-
                // Get number of results
                $titleMatchesNum = $textMatchesNum = $numTitleMatches = $numTextMatches = 0;
                if ( $titleMatches ) {
@@ -338,29 +331,27 @@ class SpecialSearch extends SpecialPage {
                $num = $titleMatchesNum + $textMatchesNum;
                $totalRes = $numTitleMatches + $numTextMatches;
 
+               // start rendering the page
                $out->enableOOUI();
-               $out->addHTML(
-                       # This is an awful awful ID name. It's not a table, but we
-                       # named it poorly from when this was a table so now we're
-                       # stuck with it
-                       Xml::openElement( 'div', [ 'id' => 'mw-search-top-table' ] ) .
-                       $this->shortDialog( $term, $num, $totalRes ) .
-                       Xml::closeElement( 'div' ) .
-                       $this->searchProfileTabs( $term ) .
-                       $this->searchOptions( $term ) .
-                       Xml::closeElement( 'form' ) .
-                       $didYouMeanHtml
-               );
+               $out->addHTML( $formWidget->render(
+                       $this->profile, $term, $num, $totalRes, $this->offset, $this->isPowerSearch()
+               ) );
 
-               $filePrefix = $wgContLang->getFormattedNsText( NS_FILE ) . ':';
-               if ( trim( $term ) === '' || $filePrefix === trim( $term ) ) {
-                       // Empty query -- straight view of search form
-                       return;
+               // did you mean... suggestions
+               if ( $textMatches ) {
+                       if ( $textMatches->hasRewrittenQuery() ) {
+                               $out->addHTML( $this->getDidYouMeanRewrittenHtml( $term, $textMatches ) );
+                       } elseif ( $textMatches->hasSuggestion() ) {
+                               $out->addHTML( $this->getDidYouMeanHtml( $textMatches ) );
+                       }
                }
 
                $out->addHTML( "<div class='searchresults'>" );
 
                $hasErrors = $textStatus && $textStatus->getErrors();
+               $hasOtherResults = $textMatches &&
+                       $textMatches->hasInterwikiResults( SearchResultSet::INLINE_RESULTS );
+
                if ( $hasErrors ) {
                        list( $error, $warning ) = $textStatus->splitByErrorType();
                        if ( $error->getErrors() ) {
@@ -379,25 +370,18 @@ class SpecialSearch extends SpecialPage {
                        }
                }
 
-               // prev/next links
-               $prevnext = null;
-               if ( $num || $this->offset ) {
-                       // Show the create link ahead
-                       $this->showCreateLink( $title, $num, $titleMatches, $textMatches );
-                       if ( $totalRes > $this->limit || $this->offset ) {
-                               if ( $this->searchEngineType !== null ) {
-                                       $this->setExtraParam( 'srbackend', $this->searchEngineType );
-                               }
-                               $prevnext = $this->getLanguage()->viewPrevNext(
-                                       $this->getPageTitle(),
-                                       $this->offset,
-                                       $this->limit,
-                                       $this->powerSearchOptions() + [ 'search' => $term ],
-                                       $this->limit + $this->offset >= $totalRes
-                               );
-                       }
+               // Show the create link ahead
+               $this->showCreateLink( $title, $num, $titleMatches, $textMatches );
+
+               // If we have no results and have not already displayed an error message
+               if ( $num === 0 && !$hasErrors ) {
+                       $out->wrapWikiMsg( "<p class=\"mw-search-nonefound\">\n$1</p>", [
+                               $hasOtherResults ? 'search-nonefound-thiswiki' : 'search-nonefound',
+                               wfEscapeWikiText( $term )
+                       ] );
                }
-               Hooks::run( 'SpecialSearchResults', [ $term, &$titleMatches, &$textMatches ] );
+
+               Hooks::run( 'SpecialSearchResults', [ $term, $titleMatches, $textMatches ] );
 
                $out->parserOptions()->setEditSection( false );
                if ( $titleMatches ) {
@@ -429,23 +413,6 @@ class SpecialSearch extends SpecialPage {
                        }
                }
 
-               $hasOtherResults = $textMatches &&
-                       $textMatches->hasInterwikiResults( SearchResultSet::INLINE_RESULTS );
-
-               // If we have no results and we have not already displayed an error message
-               if ( $num === 0 && !$hasErrors ) {
-                       if ( !$this->offset ) {
-                               // If we have an offset the create link was rendered earlier in this function.
-                               // This class needs a good de-spaghettification, but for now this will
-                               // do the job.
-                               $this->showCreateLink( $title, $num, $titleMatches, $textMatches );
-                       }
-                       $out->wrapWikiMsg( "<p class=\"mw-search-nonefound\">\n$1</p>", [
-                               $hasOtherResults ? 'search-nonefound-thiswiki' : 'search-nonefound',
-                               wfEscapeWikiText( $term )
-                       ] );
-               }
-
                if ( $hasOtherResults ) {
                        foreach ( $textMatches->getInterwikiResults( SearchResultSet::INLINE_RESULTS )
                                                as $interwiki => $interwikiResult ) {
@@ -464,10 +431,19 @@ class SpecialSearch extends SpecialPage {
 
                $out->addHTML( '<div class="mw-search-visualclear"></div>' );
 
-               if ( $prevnext ) {
+               // prev/next links
+               if ( $totalRes > $this->limit || $this->offset ) {
+                       $prevnext = $this->getLanguage()->viewPrevNext(
+                               $this->getPageTitle(),
+                               $this->offset,
+                               $this->limit,
+                               $this->powerSearchOptions() + [ 'search' => $term ],
+                               $this->limit + $this->offset >= $totalRes
+                       );
                        $out->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" );
                }
 
+               // Close <div class='searchresults'>
                $out->addHTML( "</div>" );
 
                Hooks::run( 'SpecialSearchResultsAppend', [ $this, $out, $term ] );
@@ -622,10 +598,21 @@ class SpecialSearch extends SpecialPage {
        }
 
        /**
+        * Sets up everything for the HTML output page including styles, javascript,
+        * page title, etc.
+        *
         * @param string $term
         */
        protected function setupPage( $term ) {
                $out = $this->getOutput();
+
+               $this->setHeaders();
+               $this->outputHeader();
+               // TODO: Is this true? The namespace remember uses a user token
+               // on save.
+               $out->allowClickjacking();
+               $this->addHelpLink( 'Help:Searching' );
+
                if ( strval( $term ) !== '' ) {
                        $out->setPageTitle( $this->msg( 'searchresults' ) );
                        $out->setHTMLTitle( $this->msg( 'pagetitle' )
@@ -633,8 +620,13 @@ class SpecialSearch extends SpecialPage {
                                ->inContentLanguage()->text()
                        );
                }
-               // add javascript specific to special:search
+
+               $out->addJsConfigVars( [ 'searchTerm' => $term ] );
                $out->addModules( 'mediawiki.special.search' );
+               $out->addModuleStyles( [
+                       'mediawiki.special', 'mediawiki.special.search.styles', 'mediawiki.ui', 'mediawiki.ui.button',
+                       'mediawiki.ui.input', 'mediawiki.widgets.SearchInputWidget.styles',
+               ] );
        }
 
        /**
@@ -671,12 +663,12 @@ class SpecialSearch extends SpecialPage {
         */
        protected function powerSearchOptions() {
                $opt = [];
-               if ( !$this->isPowerSearch() ) {
-                       $opt['profile'] = $this->profile;
-               } else {
+               if ( $this->isPowerSearch() ) {
                        foreach ( $this->namespaces as $n ) {
                                $opt['ns' . $n] = 1;
                        }
+               } else {
+                       $opt['profile'] = $this->profile;
                }
 
                return $opt + $this->extraParams;
@@ -859,96 +851,6 @@ class SpecialSearch extends SpecialPage {
                        "</div>";
        }
 
-       /**
-        * Generates the power search box at [[Special:Search]]
-        *
-        * @param string $term Search term
-        * @param array $opts
-        * @return string HTML form
-        */
-       protected function powerSearchBox( $term, $opts ) {
-               global $wgContLang;
-
-               // Groups namespaces into rows according to subject
-               $rows = [];
-               foreach ( $this->searchConfig->searchableNamespaces() as $namespace => $name ) {
-                       $subject = MWNamespace::getSubject( $namespace );
-                       if ( !array_key_exists( $subject, $rows ) ) {
-                               $rows[$subject] = "";
-                       }
-
-                       $name = $wgContLang->getConverter()->convertNamespace( $namespace );
-                       if ( $name == '' ) {
-                               $name = $this->msg( 'blanknamespace' )->text();
-                       }
-
-                       $rows[$subject] .=
-                               Xml::openElement( 'td' ) .
-                               Xml::checkLabel(
-                                       $name,
-                                       "ns{$namespace}",
-                                       "mw-search-ns{$namespace}",
-                                       in_array( $namespace, $this->namespaces )
-                               ) .
-                               Xml::closeElement( 'td' );
-               }
-
-               $rows = array_values( $rows );
-               $numRows = count( $rows );
-
-               // Lays out namespaces in multiple floating two-column tables so they'll
-               // be arranged nicely while still accommodating different screen widths
-               $namespaceTables = '';
-               for ( $i = 0; $i < $numRows; $i += 4 ) {
-                       $namespaceTables .= Xml::openElement( 'table' );
-
-                       for ( $j = $i; $j < $i + 4 && $j < $numRows; $j++ ) {
-                               $namespaceTables .= Xml::tags( 'tr', null, $rows[$j] );
-                       }
-
-                       $namespaceTables .= Xml::closeElement( 'table' );
-               }
-
-               $showSections = [ 'namespaceTables' => $namespaceTables ];
-
-               Hooks::run( 'SpecialSearchPowerBox', [ &$showSections, $term, $opts ] );
-
-               $hidden = '';
-               foreach ( $opts as $key => $value ) {
-                       $hidden .= Html::hidden( $key, $value );
-               }
-
-               # Stuff to feed saveNamespaces()
-               $remember = '';
-               $user = $this->getUser();
-               if ( $user->isLoggedIn() ) {
-                       $remember .= Xml::checkLabel(
-                               $this->msg( 'powersearch-remember' )->text(),
-                               'nsRemember',
-                               'mw-search-powersearch-remember',
-                               false,
-                               // The token goes here rather than in a hidden field so it
-                               // is only sent when necessary (not every form submission).
-                               [ 'value' => $user->getEditToken(
-                                       'searchnamespace',
-                                       $this->getRequest()
-                               ) ]
-                       );
-               }
-
-               // Return final output
-               return Xml::openElement( 'fieldset', [ 'id' => 'mw-searchoptions' ] ) .
-                       Xml::element( 'legend', null, $this->msg( 'powersearch-legend' )->text() ) .
-                       Xml::tags( 'h4', null, $this->msg( 'powersearch-ns' )->parse() ) .
-                       Xml::element( 'div', [ 'id' => 'mw-search-togglebox' ], '', false ) .
-                       Xml::element( 'div', [ 'class' => 'divider' ], '', false ) .
-                       implode( Xml::element( 'div', [ 'class' => 'divider' ], '', false ), $showSections ) .
-                       $hidden .
-                       Xml::element( 'div', [ 'class' => 'divider' ], '', false ) .
-                       $remember .
-                       Xml::closeElement( 'fieldset' );
-       }
-
        /**
         * @return array
         */
@@ -994,169 +896,6 @@ class SpecialSearch extends SpecialPage {
                return $profiles;
        }
 
-       /**
-        * @param string $term
-        * @return string
-        */
-       protected function searchProfileTabs( $term ) {
-               $out = Html::element( 'div', [ 'class' => 'mw-search-visualclear' ] ) .
-                       Xml::openElement( 'div', [ 'class' => 'mw-search-profile-tabs' ] );
-
-               $bareterm = $term;
-               if ( $this->startsWithImage( $term ) ) {
-                       // Deletes prefixes
-                       $bareterm = substr( $term, strpos( $term, ':' ) + 1 );
-               }
-
-               $profiles = $this->getSearchProfiles();
-               $lang = $this->getLanguage();
-
-               // Outputs XML for Search Types
-               $out .= Xml::openElement( 'div', [ 'class' => 'search-types' ] );
-               $out .= Xml::openElement( 'ul' );
-               foreach ( $profiles as $id => $profile ) {
-                       if ( !isset( $profile['parameters'] ) ) {
-                               $profile['parameters'] = [];
-                       }
-                       $profile['parameters']['profile'] = $id;
-
-                       $tooltipParam = isset( $profile['namespace-messages'] ) ?
-                               $lang->commaList( $profile['namespace-messages'] ) : null;
-                       $out .= Xml::tags(
-                               'li',
-                               [
-                                       'class' => $this->profile === $id ? 'current' : 'normal'
-                               ],
-                               $this->makeSearchLink(
-                                       $bareterm,
-                                       [],
-                                       $this->msg( $profile['message'] )->text(),
-                                       $this->msg( $profile['tooltip'], $tooltipParam )->text(),
-                                       $profile['parameters']
-                               )
-                       );
-               }
-               $out .= Xml::closeElement( 'ul' );
-               $out .= Xml::closeElement( 'div' );
-               $out .= Xml::element( 'div', [ 'style' => 'clear:both' ], '', false );
-               $out .= Xml::closeElement( 'div' );
-
-               return $out;
-       }
-
-       /**
-        * @param string $term Search term
-        * @return string
-        */
-       protected function searchOptions( $term ) {
-               $out = '';
-               $opts = [];
-               $opts['profile'] = $this->profile;
-
-               if ( $this->isPowerSearch() ) {
-                       $out .= $this->powerSearchBox( $term, $opts );
-               } else {
-                       $form = '';
-                       Hooks::run( 'SpecialSearchProfileForm', [ $this, &$form, $this->profile, $term, $opts ] );
-                       $out .= $form;
-               }
-
-               return $out;
-       }
-
-       /**
-        * @param string $term
-        * @param int $resultsShown
-        * @param int $totalNum
-        * @return string
-        */
-       protected function shortDialog( $term, $resultsShown, $totalNum ) {
-               $searchWidget = new MediaWiki\Widget\SearchInputWidget( [
-                       'id' => 'searchText',
-                       'name' => 'search',
-                       'autofocus' => trim( $term ) === '',
-                       'value' => $term,
-                       'dataLocation' => 'content',
-                       'infusable' => true,
-               ] );
-
-               $layout = new OOUI\ActionFieldLayout( $searchWidget, new OOUI\ButtonInputWidget( [
-                       'type' => 'submit',
-                       'label' => $this->msg( 'searchbutton' )->text(),
-                       'flags' => [ 'progressive', 'primary' ],
-               ] ), [
-                       'align' => 'top',
-               ] );
-
-               $out =
-                       Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
-                       Html::hidden( 'profile', $this->profile ) .
-                       Html::hidden( 'fulltext', 'Search' ) .
-                       $layout;
-
-               // Results-info
-               if ( $totalNum > 0 && $this->offset < $totalNum ) {
-                       $top = $this->msg( 'search-showingresults' )
-                               ->numParams( $this->offset + 1, $this->offset + $resultsShown, $totalNum )
-                               ->numParams( $resultsShown )
-                               ->parse();
-                       $out .= Xml::tags( 'div', [ 'class' => 'results-info' ], $top );
-               }
-
-               return $out;
-       }
-
-       /**
-        * Make a search link with some target namespaces
-        *
-        * @param string $term
-        * @param array $namespaces Ignored
-        * @param string $label Link's text
-        * @param string $tooltip Link's tooltip
-        * @param array $params Query string parameters
-        * @return string HTML fragment
-        */
-       protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params = [] ) {
-               $opt = $params;
-               foreach ( $namespaces as $n ) {
-                       $opt['ns' . $n] = 1;
-               }
-
-               $stParams = array_merge(
-                       [
-                               'search' => $term,
-                               'fulltext' => $this->msg( 'search' )->text()
-                       ],
-                       $opt
-               );
-
-               return Xml::element(
-                       'a',
-                       [
-                               'href' => $this->getPageTitle()->getLocalURL( $stParams ),
-                               'title' => $tooltip
-                       ],
-                       $label
-               );
-       }
-
-       /**
-        * Check if query starts with image: prefix
-        *
-        * @param string $term The string to check
-        * @return bool
-        */
-       protected function startsWithImage( $term ) {
-               global $wgContLang;
-
-               $parts = explode( ':', $term );
-               if ( count( $parts ) > 1 ) {
-                       return $wgContLang->getNsIndex( $parts[0] ) == NS_FILE;
-               }
-
-               return false;
-       }
-
        /**
         * @since 1.18
         *
diff --git a/includes/widget/search/SearchFormWidget.php b/includes/widget/search/SearchFormWidget.php
new file mode 100644 (file)
index 0000000..e223b95
--- /dev/null
@@ -0,0 +1,312 @@
+<?php
+
+namespace MediaWiki\Widget\Search;
+
+use Hooks;
+use Html;
+use MediaWiki\Widget\SearchInputWidget;
+use MWNamespace;
+use SearchEngineConfig;
+use SpecialSearch;
+use Xml;
+
+class SearchFormWidget {
+       /** @var SpecialSearch */
+       protected $specialSearch;
+       /** @var SearchEngineConfig */
+       protected $searchConfig;
+       /** @var array */
+       protected $profiles;
+
+       /**
+        * @param SpecialSearch $specialSearch
+        * @param SearchEngineConfig $searchConfig
+        * @param array $profiles
+        */
+       public function __construct(
+               SpecialSearch $specialSearch,
+               SearchEngineConfig $searchConfig,
+               array $profiles
+       ) {
+               $this->specialSearch = $specialSearch;
+               $this->searchConfig = $searchConfig;
+               $this->profiles = $profiles;
+       }
+
+       /**
+        * @param string $profile The current search profile
+        * @param string $term The current search term
+        * @param int $numResults The number of results shown
+        * @param int $totalResults The total estimated results found
+        * @param int $offset Current offset in search results
+        * @param bool $isPowerSearch Is the 'advanced' section open?
+        * @return string HTML
+        */
+       public function render(
+               $profile,
+               $term,
+               $numResults,
+               $totalResults,
+               $offset,
+               $isPowerSearch
+       ) {
+               return Xml::openElement(
+                               'form',
+                               [
+                                       'id' => $isPowerSearch ? 'powersearch' : 'search',
+                                       'method' => 'get',
+                                       'action' => wfScript(),
+                               ]
+                       ) .
+                               '<div id="mw-search-top-table">' .
+                                       $this->shortDialogHtml( $profile, $term, $numResults, $totalResults, $offset ) .
+                               '</div>' .
+                               "<div class='mw-search-visualclear'></div>" .
+                               "<div class='mw-search-profile-tabs'>" .
+                                       $this->profileTabsHtml( $profile, $term ) .
+                                       "<div style='clear:both'></div>" .
+                               "</div>" .
+                               $this->optionsHtml( $term, $isPowerSearch, $profile ) .
+                       '</form>';
+       }
+
+       /**
+        * @param string $profile The current search profile
+        * @param string $term The current search term
+        * @param int $numResults The number of results shown
+        * @param int $totalResults The total estimated results found
+        * @param int $offset Current offset in search results
+        * @return string HTML
+        */
+       protected function shortDialogHtml( $profile, $term, $numResults, $totalResults, $offset ) {
+               $searchWidget = new SearchInputWidget( [
+                       'id' => 'searchText',
+                       'name' => 'search',
+                       'autofocus' => trim( $term ) === '',
+                       'value' => $term,
+                       'dataLocation' => 'content',
+                       'infusable' => true,
+               ] );
+
+               $layout = new \OOUI\ActionFieldLayout( $searchWidget, new \OOUI\ButtonInputWidget( [
+                       'type' => 'submit',
+                       'label' => $this->specialSearch->msg( 'searchbutton' )->text(),
+                       'flags' => [ 'progressive', 'primary' ],
+               ] ), [
+                       'align' => 'top',
+               ] );
+
+               $html =
+                       Html::hidden( 'title', $this->specialSearch->getPageTitle()->getPrefixedText() ) .
+                       Html::hidden( 'profile', $profile ) .
+                       Html::hidden( 'fulltext', '1' ) .
+                       $layout;
+
+               if ( $totalResults > 0 && $offset < $totalResults ) {
+                       $html .= Xml::tags(
+                               'div',
+                               [ 'class' => 'results-info' ],
+                               $this->specialSearch->msg( 'search-showingresults' )
+                                       ->numParams( $offset + 1, $offset + $numResults, $totalResults )
+                                       ->numParams( $numResults )
+                                       ->parse()
+                       );
+               }
+
+               return $html;
+       }
+
+       /**
+        * Generates HTML for the list of available search profiles.
+        *
+        * @param string $profile The currently selected profile
+        * @param string $term The user provided search terms
+        * @return string HTML
+        */
+       protected function profileTabsHtml( $profile, $term ) {
+               $bareterm = $this->startsWithImage( $term )
+                       ? substr( $term, strpos( $term, ':' ) + 1 )
+                       : $term;
+               $lang = $this->specialSearch->getLanguage();
+               $items = [];
+               foreach ( $this->profiles as $id => $profileConfig ) {
+                       $profileConfig['parameters']['profile'] = $id;
+                       $tooltipParam = isset( $profileConfig['namespace-messages'] )
+                               ? $lang->commaList( $profileConfig['namespace-messages'] )
+                               : null;
+                       $items[] = Xml::tags(
+                               'li',
+                               [ 'class' => $profile === $id ? 'current' : 'normal' ],
+                               $this->makeSearchLink(
+                                       $bareterm,
+                                       $this->specialSearch->msg( $profileConfig['message'] )->text(),
+                                       $this->specialSearch->msg( $profileConfig['tooltip'], $tooltipParam )->text(),
+                                       $profileConfig['parameters']
+                               )
+                       );
+               }
+
+               return
+                               "<div class='search-types'>" .
+                                       "<ul>" . implode( '', $items ) . "</ul>" .
+                               "</div>";
+       }
+
+       /**
+        * Check if query starts with image: prefix
+        *
+        * @param string $term The string to check
+        * @return bool
+        */
+       protected function startsWithImage( $term ) {
+               global $wgContLang;
+
+               $parts = explode( ':', $term );
+               return count( $parts ) > 1
+                       ? $wgContLang->getNsIndex( $parts[0] ) === NS_FILE
+                       : false;
+       }
+
+       /**
+        * Make a search link with some target namespaces
+        *
+        * @param string $term The term to search for
+        * @param string $label Link's text
+        * @param string $tooltip Link's tooltip
+        * @param array $params Query string parameters
+        * @return string HTML fragment
+        */
+       protected function makeSearchLink( $term, $label, $tooltip, array $params = [] ) {
+               $params += [
+                       'search' => $term,
+                       'fulltext' => 1,
+               ];
+
+               return Xml::element(
+                       'a',
+                       [
+                               'href' => $this->specialSearch->getPageTitle()->getLocalURL( $params ),
+                               'title' => $tooltip,
+                       ],
+                       $label
+               );
+       }
+
+       /**
+        * Generates HTML for advanced options available with the currently
+        * selected search profile.
+        *
+        * @param string $term User provided search term
+        * @param bool $isPowerSearch Is the advanced search profile enabled?
+        * @param string $profile The current search profile
+        * @return string HTML
+        */
+       protected function optionsHtml( $term, $isPowerSearch, $profile ) {
+               $html = '';
+               $opts = [
+                       'profile' => $profile,
+               ];
+
+               if ( $isPowerSearch ) {
+                       $html .= $this->powerSearchBox( $term, $opts );
+               } else {
+                       $form = '';
+                       Hooks::run( 'SpecialSearchProfileForm', [
+                               $this->specialSearch, &$form, $profile, $term, $opts
+                       ] );
+                       $html .= $form;
+               }
+
+               return $html;
+       }
+
+       /**
+        * @param string $term The current search term
+        * @param array $opts Additional key/value pairs that will be submitted
+        *  with the generated form.
+        * @return string HTML
+        */
+       protected function powerSearchBox( $term, array $opts ) {
+               global $wgContLang;
+
+               $rows = [];
+               $activeNamespaces = $this->specialSearch->getNamespaces();
+               foreach ( $this->searchConfig->searchableNamespaces() as $namespace => $name ) {
+                       $subject = MWNamespace::getSubject( $namespace );
+                       if ( !isset( $rows[$subject] ) ) {
+                               $rows[$subject] = "";
+                       }
+
+                       $name = $wgContLang->getConverter()->convertNamespace( $namespace );
+                       if ( $name === '' ) {
+                               $name = $this->specialSearch->msg( 'blanknamespace' )->text();
+                       }
+
+                       $rows[$subject] .=
+                               '<td>' .
+                                       Xml::checkLabel(
+                                               $name,
+                                               "ns{$namespace}",
+                                               "mw-search-ns{$namespace}",
+                                               in_array( $namespace, $activeNamespaces )
+                                       ) .
+                               '</td>';
+               }
+
+               // Lays out namespaces in multiple floating two-column tables so they'll
+               // be arranged nicely while still accomodating diferent screen widths
+               $tableRows = [];
+               foreach ( $rows as $row ) {
+                       $tableRows[] = "<tr>{$row}</tr>";
+               }
+               $namespaceTables = [];
+               foreach ( array_chunk( $tableRows, 4 ) as $chunk ) {
+                       $namespaceTables[] = implode( '', $chunk );
+               }
+
+               $showSections = [
+                       'namespaceTables' => "<table>" . implode( '</table><table>', $namespaceTables ) . '</table>',
+               ];
+               Hooks::run( 'SpecialSearchPowerBox', [ &$showSections, $term, $opts ] );
+
+               $hidden = '';
+               foreach ( $opts as $key => $value ) {
+                       $hidden .= Html::hidden( $key, $value );
+               }
+
+               $divider = "<div class='divider'></div>";
+
+               // Stuff to feed SpecialSearch::saveNamespaces()
+               $user = $this->specialSearch->getUser();
+               $remember = '';
+               if ( $user->isLoggedIn() ) {
+                       $remember = $divider . Xml::checkLabel(
+                               $this->specialSearch->msg( 'powersearch-remember' )->text(),
+                               'nsRemember',
+                               'mw-search-powersearch-remember',
+                               false,
+                               // The token goes here rather than in a hidden field so it
+                               // is only sent when necessary (not every form submission)
+                               [ 'value' => $user->getEditToken(
+                                       'searchnamespace',
+                                       $this->specialSearch->getRequest()
+                               ) ]
+                       );
+               }
+
+               return
+                       "<fieldset id='mw-searchoptions'>" .
+                               "<legend>" . $this->specialSearch->msg( 'powersearch-legend' )->escaped() . '</legend>' .
+                               "<h4>" . $this->specialSearch->msg( 'powersearch-ns' )->escaped() . '</h4>' .
+                               // populated by js if available
+                               "<div id='mw-search-togglebox'></div>" .
+                               $divider .
+                               implode(
+                                       $divider,
+                                       $showSections
+                               ) .
+                               $hidden .
+                               $remember .
+                       "</fieldset>";
+       }
+}
index 3fa8a9f..e9cf6a3 100644 (file)
@@ -121,13 +121,15 @@ class SpecialSearchTest extends MediaWikiTestCase {
                ] );
 
                # Initialize [[Special::Search]]
+               $ctx = new RequestContext();
+               $term = '{{SITENAME}}';
+               $ctx->setRequest( new FauxRequest( [ 'search' => $term, 'fulltext' => 1 ] ) );
+               $ctx->setTitle( Title::newFromText( 'Special:Search' ) );
                $search = new SpecialSearch();
-               $search->getContext()->setTitle( Title::newFromText( 'Special:Search' ) );
-               $search->load();
+               $search->setContext( $ctx );
 
                # Simulate a user searching for a given term
-               $term = '{{SITENAME}}';
-               $search->showResults( $term );
+               $search->execute( '' );
 
                # Lookup the HTML page title set for that page
                $pageTitle = $search