PrefixSearch: Implement searching in multiple namespaces
authorNiklas Laxström <niklas.laxstrom@gmail.com>
Mon, 2 Jun 2014 18:34:52 +0000 (18:34 +0000)
committerNiklas Laxström <niklas.laxstrom@gmail.com>
Thu, 15 Sep 2016 12:48:35 +0000 (14:48 +0200)
I thought there was just an issue with capitalization, but in fact the
code explicitly only searched one namespace anyway. Fixed that while
taking capitalization differences in namespaces into account.

This by extend also brings support for multiple namespaces to the
opensearch API.

Follows-up I3487bb69.

Bug: T67752
Bug: T32323
Change-Id: I4bec7b5548fc27ac51a1b4d4961c3bbc31eb7337

includes/PrefixSearch.php
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/PrefixSearchTest.php

index 49e596d..98bc885 100644 (file)
@@ -57,35 +57,55 @@ abstract class PrefixSearch {
                if ( $search == '' ) {
                        return []; // Return empty result
                }
-               $namespaces = $this->validateNamespaces( $namespaces );
-
-               // Find a Title which is not an interwiki and is in NS_MAIN
-               $title = Title::newFromText( $search );
-               if ( $title && !$title->isExternal() ) {
-                       $ns = [ $title->getNamespace() ];
-                       $search = $title->getText();
-                       if ( $ns[0] == NS_MAIN ) {
-                               $ns = $namespaces; // no explicit prefix, use default namespaces
-                               Hooks::run( 'PrefixSearchExtractNamespace', [ &$ns, &$search ] );
-                       }
-                       return $this->searchBackend( $ns, $search, $limit, $offset );
-               }
 
-               // Is this a namespace prefix?
-               $title = Title::newFromText( $search . 'Dummy' );
-               if ( $title && $title->getText() == 'Dummy'
-                       && $title->getNamespace() != NS_MAIN
-                       && !$title->isExternal() )
-               {
-                       $namespaces = [ $title->getNamespace() ];
-                       $search = '';
+               $hasNamespace = $this->extractNamespace( $search );
+               if ( $hasNamespace ) {
+                       list( $namespace, $search ) = $hasNamespace;
+                       $namespaces = [ $namespace ];
                } else {
+                       $namespaces = $this->validateNamespaces( $namespaces );
                        Hooks::run( 'PrefixSearchExtractNamespace', [ &$namespaces, &$search ] );
                }
 
                return $this->searchBackend( $namespaces, $search, $limit, $offset );
        }
 
+       /**
+        * Figure out if given input contains an explicit namespace.
+        *
+        * @param string $input
+        * @return false|array Array of namespace and remaining text, or false if no namespace given.
+        */
+       protected function extractNamespace( $input ) {
+               if ( strpos( $input, ':' ) === false ) {
+                       return false;
+               }
+
+               // Namespace prefix only
+               $title = Title::newFromText( $input . 'Dummy' );
+               if (
+                       $title &&
+                       $title->getText() === 'Dummy' &&
+                       !$title->inNamespace( NS_MAIN ) &&
+                       !$title->isExternal()
+               ) {
+                       return [ $title->getNamespace(), '' ];
+               }
+
+               // Namespace prefix with additional input
+               $title = Title::newFromText( $input );
+               if (
+                       $title &&
+                       !$title->inNamespace( NS_MAIN ) &&
+                       !$title->isExternal()
+               ) {
+                       // getText provides correct capitalization
+                       return [ $title->getNamespace(), $title->getText() ];
+               }
+
+               return false;
+       }
+
        /**
         * Do a prefix search for all possible variants of the prefix
         * @param string $search
@@ -254,43 +274,60 @@ abstract class PrefixSearch {
         * be automatically capitalized by Title::secureAndSpit()
         * later on depending on $wgCapitalLinks)
         *
-        * @param array $namespaces Namespaces to search in
+        * @param array|null $namespaces Namespaces to search in
         * @param string $search Term
         * @param int $limit Max number of items to return
         * @param int $offset Number of items to skip
-        * @return array Array of Title objects
+        * @return Title[] Array of Title objects
         */
        public function defaultSearchBackend( $namespaces, $search, $limit, $offset ) {
-               $ns = array_shift( $namespaces ); // support only one namespace
-               if ( is_null( $ns ) || in_array( NS_MAIN, $namespaces ) ) {
-                       $ns = NS_MAIN; // if searching on many always default to main
+               // Backwards compatability with old code. Default to NS_MAIN if no namespaces provided.
+               if ( $namespaces === null ) {
+                       $namespaces = [];
+               }
+               if ( !$namespaces ) {
+                       $namespaces[] = NS_MAIN;
                }
 
-               if ( $ns == NS_SPECIAL ) {
-                       return $this->specialSearch( $search, $limit, $offset );
+               // Construct suitable prefix for each namespace. They differ in cases where
+               // some namespaces always capitalize and some don't.
+               $prefixes = [];
+               foreach ( $namespaces as $namespace ) {
+                       // For now, if special is included, ignore the other namespaces
+                       if ( $namespace == NS_SPECIAL ) {
+                               return $this->specialSearch( $search, $limit, $offset );
+                       }
+
+                       $title = Title::makeTitleSafe( $namespace, $search );
+                       // Why does the prefix default to empty?
+                       $prefix = $title ? $title->getDBkey() : '';
+                       $prefixes[$prefix][] = $namespace;
                }
 
-               $t = Title::newFromText( $search, $ns );
-               $prefix = $t ? $t->getDBkey() : '';
                $dbr = wfGetDB( DB_REPLICA );
-               $res = $dbr->select( 'page',
-                       [ 'page_id', 'page_namespace', 'page_title' ],
-                       [
-                               'page_namespace' => $ns,
-                               'page_title ' . $dbr->buildLike( $prefix, $dbr->anyString() )
-                       ],
-                       __METHOD__,
-                       [
-                               'LIMIT' => $limit,
-                               'ORDER BY' => 'page_title',
-                               'OFFSET' => $offset
-                       ]
-               );
-               $srchres = [];
-               foreach ( $res as $row ) {
-                       $srchres[] = Title::newFromRow( $row );
+               // Often there is only one prefix that applies to all requested namespaces,
+               // but sometimes there are two if some namespaces do not always capitalize.
+               $conds = [];
+               foreach ( $prefixes as $prefix => $namespaces ) {
+                       $condition = [
+                               'page_namespace' => $namespaces,
+                               'page_title' . $dbr->buildLike( $prefix, $dbr->anyString() ),
+                       ];
+                       $conds[] = $dbr->makeList( $condition, LIST_AND );
                }
-               return $srchres;
+
+               $table = 'page';
+               $fields = [ 'page_id', 'page_namespace', 'page_title' ];
+               $conds = $dbr->makeList( $conds, LIST_OR );
+               $options = [
+                       'LIMIT' => $limit,
+                       'ORDER BY' => [ 'page_title', 'page_namespace' ],
+                       'OFFSET' => $offset
+               ];
+
+               $res = $dbr->select( $table, $fields, $conds, __METHOD__, $options );
+
+               return iterator_to_array( TitleArray::newFromResult( $res ) );
        }
 
        /**
index 920dbb3..cfeb44f 100644 (file)
@@ -920,13 +920,22 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
         *
         * Should be called from addDBData().
         *
-        * @since 1.25
-        * @param string $pageName Page name
+        * @since 1.25 ($namespace in 1.28)
+        * @param string|title $pageName Page name or title
         * @param string $text Page's content
+        * @param int $namespace Namespace id (name cannot already contain namespace)
         * @return array Title object and page id
         */
-       protected function insertPage( $pageName, $text = 'Sample page for unit test.' ) {
-               $title = Title::newFromText( $pageName, 0 );
+       protected function insertPage(
+               $pageName,
+               $text = 'Sample page for unit test.',
+               $namespace = null
+       ) {
+               if ( is_string( $pageName ) ) {
+                       $title = Title::newFromText( $pageName, $namespace );
+               } else {
+                       $title = $pageName;
+               }
 
                $user = static::getTestSysop()->getUser();
                $comment = __METHOD__ . ': Sample page for unit test.';
index 0ec200c..bc43709 100644 (file)
@@ -2,8 +2,11 @@
 /**
  * @group Search
  * @group Database
+ * @covers PrefixSearch
  */
 class PrefixSearchTest extends MediaWikiLangTestCase {
+       const NS_NONCAP = 12346;
+
        private $originalHandlers;
 
        public function addDBDataOnce() {
@@ -31,6 +34,10 @@ class PrefixSearchTest extends MediaWikiLangTestCase {
                $this->insertPage( 'Talk:Example' );
 
                $this->insertPage( 'User:Example' );
+
+               $this->insertPage( Title::makeTitle( self::NS_NONCAP, 'Bar' ) );
+               $this->insertPage( Title::makeTitle( self::NS_NONCAP, 'Upper' ) );
+               $this->insertPage( Title::makeTitle( self::NS_NONCAP, 'sandbox' ) );
        }
 
        protected function setUp() {
@@ -44,11 +51,17 @@ class PrefixSearchTest extends MediaWikiLangTestCase {
                $this->setMwGlobals( [
                        'wgSpecialPages' => [],
                        'wgHooks' => [],
+                       'wgExtraNamespaces' => [ self::NS_NONCAP => 'NonCap' ],
+                       'wgCapitalLinkOverrides' => [ self::NS_NONCAP => false ],
                ] );
 
                $this->originalHandlers = TestingAccessWrapper::newFromClass( 'Hooks' )->handlers;
                TestingAccessWrapper::newFromClass( 'Hooks' )->handlers = [];
 
+               // Clear caches so that our new namespace appears
+               MWNamespace::getCanonicalNamespaces( true );
+               Language::factory( 'en' )->resetNamespaces();
+
                SpecialPageFactory::resetList();
        }
 
@@ -158,6 +171,29 @@ class PrefixSearchTest extends MediaWikiLangTestCase {
                                        'Special:EditWatchlist/clear',
                                ],
                        ] ],
+                       [ [
+                               'Namespace with case sensitive first letter',
+                               'query' => 'NonCap:upper',
+                               'results' => []
+                       ] ],
+                       [ [
+                               'Multinamespace search',
+                               'query' => 'B',
+                               'results' => [
+                                       'Bar',
+                                       'NonCap:Bar',
+                               ],
+                               'namespaces' => [ NS_MAIN, self::NS_NONCAP ],
+                       ] ],
+                       [ [
+                               'Multinamespace search with lowercase first letter',
+                               'query' => 'sand',
+                               'results' => [
+                                       'Sandbox',
+                                       'NonCap:sandbox',
+                               ],
+                               'namespaces' => [ NS_MAIN, self::NS_NONCAP ],
+                       ] ],
                ];
        }
 
@@ -168,8 +204,11 @@ class PrefixSearchTest extends MediaWikiLangTestCase {
         */
        public function testSearch( array $case ) {
                $this->searchProvision( null );
+
+               $namespaces = isset( $case['namespaces'] ) ? $case['namespaces'] : [];
+
                $searcher = new StringPrefixSearch;
-               $results = $searcher->search( $case['query'], 3 );
+               $results = $searcher->search( $case['query'], 3, $namespaces );
                $this->assertEquals(
                        $case['results'],
                        $results,
@@ -184,8 +223,11 @@ class PrefixSearchTest extends MediaWikiLangTestCase {
         */
        public function testSearchWithOffset( array $case ) {
                $this->searchProvision( null );
+
+               $namespaces = isset( $case['namespaces'] ) ? $case['namespaces'] : [];
+
                $searcher = new StringPrefixSearch;
-               $results = $searcher->search( $case['query'], 3, [], 1 );
+               $results = $searcher->search( $case['query'], 3, $namespaces, 1 );
 
                // We don't expect the first result when offsetting
                array_shift( $case['results'] );