From 34b02d87acc11da59c4c07eceb38bb65566e15bf Mon Sep 17 00:00:00 2001 From: Stanislav Malyshev Date: Sun, 3 Apr 2016 11:37:11 +0300 Subject: [PATCH] Convert SearchEngine to service containers Change-Id: Icef1ecbed3d831557e0256fdfa53743b194007cc --- autoload.php | 3 + includes/MediaWikiServices.php | 22 ++ includes/ServiceWiring.php | 11 + includes/api/ApiOpenSearch.php | 18 +- includes/api/ApiQueryPrefixSearch.php | 4 +- includes/api/ApiQuerySearch.php | 15 +- includes/deferred/SearchUpdate.php | 19 +- includes/search/SearchEngine.php | 324 +++++------------- includes/search/SearchEngineConfig.php | 110 ++++++ includes/search/SearchEngineFactory.php | 42 +++ includes/search/SearchNearMatchResultSet.php | 2 +- includes/search/SearchNearMatcher.php | 163 +++++++++ includes/search/SearchResult.php | 16 +- includes/specialpage/SpecialPage.php | 4 +- .../specials/SpecialFileDuplicateSearch.php | 4 +- includes/specials/SpecialSearch.php | 31 +- includes/user/User.php | 4 +- .../includes/MediaWikiServicesTest.php | 5 + .../includes/deferred/SearchUpdateTest.php | 8 +- .../search/SearchEnginePrefixTest.php | 4 +- .../includes/specials/SpecialSearchTest.php | 5 +- 21 files changed, 537 insertions(+), 277 deletions(-) create mode 100644 includes/search/SearchEngineConfig.php create mode 100644 includes/search/SearchEngineFactory.php create mode 100644 includes/search/SearchNearMatcher.php diff --git a/autoload.php b/autoload.php index 9b155bb57e..f802ddd874 100644 --- a/autoload.php +++ b/autoload.php @@ -1135,12 +1135,15 @@ $wgAutoloadLocalClasses = [ 'SearchDatabase' => __DIR__ . '/includes/search/SearchDatabase.php', 'SearchDump' => __DIR__ . '/maintenance/dumpIterator.php', 'SearchEngine' => __DIR__ . '/includes/search/SearchEngine.php', + 'SearchEngineConfig' => __DIR__ . '/includes/search/SearchEngineConfig.php', 'SearchEngineDummy' => __DIR__ . '/includes/search/SearchEngine.php', + 'SearchEngineFactory' => __DIR__ . '/includes/search/SearchEngineFactory.php', 'SearchExactMatchRescorer' => __DIR__ . '/includes/search/SearchExactMatchRescorer.php', 'SearchHighlighter' => __DIR__ . '/includes/search/SearchHighlighter.php', 'SearchMssql' => __DIR__ . '/includes/search/SearchMssql.php', 'SearchMySQL' => __DIR__ . '/includes/search/SearchMySQL.php', 'SearchNearMatchResultSet' => __DIR__ . '/includes/search/SearchNearMatchResultSet.php', + 'SearchNearMatcher' => __DIR__ . '/includes/search/SearchNearMatcher.php', 'SearchOracle' => __DIR__ . '/includes/search/SearchOracle.php', 'SearchPostgres' => __DIR__ . '/includes/search/SearchPostgres.php', 'SearchResult' => __DIR__ . '/includes/search/SearchResult.php', diff --git a/includes/MediaWikiServices.php b/includes/MediaWikiServices.php index 9a942a55d0..612d09e85e 100644 --- a/includes/MediaWikiServices.php +++ b/includes/MediaWikiServices.php @@ -159,6 +159,28 @@ class MediaWikiServices extends ServiceContainer { return $this->getService( 'EventRelayerGroup' ); } + /** + * @return SearchEngine + */ + public function newSearchEngine() { + // New engine object every time, since they keep state + return $this->getService( 'SearchEngineFactory' )->create(); + } + + /** + * @return SearchEngineFactory + */ + public function getSearchEngineFactory() { + return $this->getService( 'SearchEngineFactory' ); + } + + /** + * @return SearchEngineConfig + */ + public function getSearchEngineConfig() { + return $this->getService( 'SearchEngineConfig' ); + } + /////////////////////////////////////////////////////////////////////////// // NOTE: When adding a service getter here, don't forget to add a test // case for it in MediaWikiServicesTest::provideGetters() and in diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index 991a67d737..defe698c28 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -82,6 +82,17 @@ return [ return new EventRelayerGroup( $services->getMainConfig()->get( 'EventRelayerConfig' ) ); }, + 'SearchEngineFactory' => function( MediaWikiServices $services ) { + // Create search engine + return new SearchEngineFactory( $services->getService( 'SearchEngineConfig' ) ); + }, + + 'SearchEngineConfig' => function( MediaWikiServices $services ) { + // Create a search engine config from main config. + $config = $services->getService( 'MainConfig' ); + return new SearchEngineConfig( $config ); + } + /////////////////////////////////////////////////////////////////////////// // NOTE: When adding a service here, don't forget to add a getter function // in the MediaWikiServices class. The convenience getter should just call diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php index effa520a2d..058e0a3909 100644 --- a/includes/api/ApiOpenSearch.php +++ b/includes/api/ApiOpenSearch.php @@ -24,6 +24,8 @@ * @file */ +use MediaWiki\MediaWikiServices; + /** * @ingroup API */ @@ -123,8 +125,7 @@ class ApiOpenSearch extends ApiBase { * @param array &$results Put results here. Keys have to be integers. */ protected function search( $search, $limit, $namespaces, $resolveRedir, &$results ) { - - $searchEngine = SearchEngine::create(); + $searchEngine = MediaWikiServices::getInstance()->newSearchEngine(); $searchEngine->setLimitOffset( $limit ); $searchEngine->setNamespaces( $namespaces ); $titles = $searchEngine->extractTitles( $searchEngine->completionSearchWithVariants( $search ) ); @@ -350,24 +351,25 @@ class ApiOpenSearch extends ApiBase { * @throws MWException */ public static function getOpenSearchTemplate( $type ) { - global $wgOpenSearchTemplate, $wgCanonicalServer; + $config = MediaWikiServices::getInstance()->getSearchEngineConfig(); + $template = $config->getConfig()->get( 'OpenSearchTemplate' ); - if ( $wgOpenSearchTemplate && $type === 'application/x-suggestions+json' ) { - return $wgOpenSearchTemplate; + if ( $template && $type === 'application/x-suggestions+json' ) { + return $template; } - $ns = implode( '|', SearchEngine::defaultNamespaces() ); + $ns = implode( '|', $config->defaultNamespaces() ); if ( !$ns ) { $ns = '0'; } switch ( $type ) { case 'application/x-suggestions+json': - return $wgCanonicalServer . wfScript( 'api' ) + return $config->getConfig()->get( 'CanonicalServer' ) . wfScript( 'api' ) . '?action=opensearch&search={searchTerms}&namespace=' . $ns; case 'application/x-suggestions+xml': - return $wgCanonicalServer . wfScript( 'api' ) + return $config->getConfig()->get( 'CanonicalServer' ) . wfScript( 'api' ) . '?action=opensearch&format=xml&search={searchTerms}&namespace=' . $ns; default: diff --git a/includes/api/ApiQueryPrefixSearch.php b/includes/api/ApiQueryPrefixSearch.php index d04796c91d..5c50273261 100644 --- a/includes/api/ApiQueryPrefixSearch.php +++ b/includes/api/ApiQueryPrefixSearch.php @@ -1,4 +1,6 @@ newSearchEngine(); $searchEngine->setLimitOffset( $limit + 1, $offset ); $searchEngine->setNamespaces( $namespaces ); $titles = $searchEngine->extractTitles( $searchEngine->completionSearchWithVariants( $search ) ); diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php index 3955cc5f67..f57d3a30cf 100644 --- a/includes/api/ApiQuerySearch.php +++ b/includes/api/ApiQuerySearch.php @@ -24,6 +24,8 @@ * @file */ +use MediaWiki\MediaWikiServices; + /** * Query module to perform full text search within wiki titles and content * @@ -78,8 +80,9 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { } // Create search engine instance and set options - $search = isset( $params['backend'] ) && $params['backend'] != self::BACKEND_NULL_PARAM ? - SearchEngine::create( $params['backend'] ) : SearchEngine::create(); + $type = isset( $params['backend'] ) && $params['backend'] != self::BACKEND_NULL_PARAM ? + $params['backend'] : null; + $search = MediaWikiServices::getInstance()->getSearchEngineFactory()->create( $type ); $search->setLimitOffset( $limit + 1, $params['offset'] ); $search->setNamespaces( $params['namespace'] ); $search->setFeatureData( 'rewrite', (bool)$params['enablerewrites'] ); @@ -95,7 +98,8 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { } elseif ( $what == 'nearmatch' ) { // near matches must receive the user input as provided, otherwise // the near matches within namespaces are lost. - $matches = SearchEngine::getNearMatchResultSet( $params['search'] ); + $matches = $search->getNearMatcher( $this->getConfig() ) + ->getNearMatchResultSet( $params['search'] ); } else { // We default to title searches; this is a terrible legacy // of the way we initially set up the MySQL fulltext-based @@ -358,13 +362,14 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { 'enablerewrites' => false, ]; - $alternatives = SearchEngine::getSearchTypes(); + $searchConfig = MediaWikiServices::getInstance()->getSearchEngineConfig(); + $alternatives = $searchConfig->getSearchTypes(); if ( count( $alternatives ) > 1 ) { if ( $alternatives[0] === null ) { $alternatives[0] = self::BACKEND_NULL_PARAM; } $params['backend'] = [ - ApiBase::PARAM_DFLT => $this->getConfig()->get( 'SearchType' ), + ApiBase::PARAM_DFLT => $searchConfig->getSearchType(), ApiBase::PARAM_TYPE => $alternatives, ]; } diff --git a/includes/deferred/SearchUpdate.php b/includes/deferred/SearchUpdate.php index 2abf02875e..62c8b00f39 100644 --- a/includes/deferred/SearchUpdate.php +++ b/includes/deferred/SearchUpdate.php @@ -23,6 +23,8 @@ * @ingroup Search */ +use MediaWiki\MediaWikiServices; + /** * Database independant search index updater * @@ -75,14 +77,15 @@ class SearchUpdate implements DeferrableUpdate { * Perform actual update for the entry */ public function doUpdate() { - global $wgDisableSearchUpdate; + $config = MediaWikiServices::getInstance()->getSearchEngineConfig(); - if ( $wgDisableSearchUpdate || !$this->id ) { + if ( $config->getConfig()->get( 'DisableSearchUpdate' ) || !$this->id ) { return; } - foreach ( SearchEngine::getSearchTypes() as $type ) { - $search = SearchEngine::create( $type ); + $seFactory = MediaWikiServices::getInstance()->getSearchEngineFactory(); + foreach ( $config->getSearchTypes() as $type ) { + $search = $seFactory->create( $type ); if ( !$search->supports( 'search-update' ) ) { continue; } @@ -99,7 +102,7 @@ class SearchUpdate implements DeferrableUpdate { $text = $search->getTextFromContent( $this->title, $this->content ); if ( !$search->textAlreadyUpdatedForIndex() ) { - $text = self::updateText( $text ); + $text = $this->updateText( $text, $search ); } # Perform the actual update @@ -113,14 +116,16 @@ class SearchUpdate implements DeferrableUpdate { * If you're using a real search engine, you'll probably want to override * this behavior and do something nicer with the original wikitext. * @param string $text + * @param SearchEngine $se Search engine * @return string */ - public static function updateText( $text ) { + public function updateText( $text, SearchEngine $se = null ) { global $wgContLang; # Language-specific strip/conversion $text = $wgContLang->normalizeForSearch( $text ); - $lc = SearchEngine::legalSearchChars() . '&#;'; + $se = $se ?: MediaWikiServices::getInstance()->newSearchEngine(); + $lc = $se->legalSearchChars() . '&#;'; $text = preg_replace( "/<\\/?\\s*[A-Za-z][^>]*?>/", ' ', $wgContLang->lc( " " . $text . " " ) ); # Strip HTML markup diff --git a/includes/search/SearchEngine.php b/includes/search/SearchEngine.php index b263fb39e9..3d2057c528 100644 --- a/includes/search/SearchEngine.php +++ b/includes/search/SearchEngine.php @@ -25,11 +25,13 @@ * @defgroup Search Search */ +use MediaWiki\MediaWikiServices; + /** * Contain a class for special pages * @ingroup Search */ -class SearchEngine { +abstract class SearchEngine { /** @var string */ public $prefix = ''; @@ -124,155 +126,55 @@ class SearchEngine { * @param string $term * @return string */ - function transformSearchTerm( $term ) { + public function transformSearchTerm( $term ) { return $term; } + /** + * Get service class to finding near matches. + * @param Config $config Configuration to use for the matcher. + * @return SearchNearMatcher + */ + public function getNearMatcher( Config $config ) { + return new SearchNearMatcher( $config ); + } + + /** + * Get near matcher for default SearchEngine. + * @return SearchNearMatcher + */ + protected static function defaultNearMatcher() { + $config = MediaWikiServices::getInstance()->getMainConfig(); + return MediaWikiServices::getInstance()->newSearchEngine()->getNearMatcher( $config ); + } + /** * If an exact title match can be found, or a very slightly close match, * return the title. If no match, returns NULL. - * + * @deprecated since 1.27; Use SearchEngine::getNearMatcher() * @param string $searchterm * @return Title */ public static function getNearMatch( $searchterm ) { - $title = self::getNearMatchInternal( $searchterm ); - - Hooks::run( 'SearchGetNearMatchComplete', [ $searchterm, &$title ] ); - return $title; + return static::defaultNearMatcher()->getNearMatch( $searchterm ); } /** * Do a near match (see SearchEngine::getNearMatch) and wrap it into a * SearchResultSet. - * + * @deprecated since 1.27; Use SearchEngine::getNearMatcher() * @param string $searchterm * @return SearchResultSet */ public static function getNearMatchResultSet( $searchterm ) { - return new SearchNearMatchResultSet( self::getNearMatch( $searchterm ) ); + return static::defaultNearMatcher()->getNearMatchResultSet( $searchterm ); } /** - * Really find the title match. - * @param string $searchterm - * @return null|Title + * Get chars legal for search. + * NOTE: usage as static is deprecated and preserved only as BC measure + * @return string */ - private static function getNearMatchInternal( $searchterm ) { - global $wgContLang, $wgEnableSearchContributorsByIP; - - $allSearchTerms = [ $searchterm ]; - - if ( $wgContLang->hasVariants() ) { - $allSearchTerms = array_unique( array_merge( - $allSearchTerms, - $wgContLang->autoConvertToAllVariants( $searchterm ) - ) ); - } - - $titleResult = null; - if ( !Hooks::run( 'SearchGetNearMatchBefore', [ $allSearchTerms, &$titleResult ] ) ) { - return $titleResult; - } - - foreach ( $allSearchTerms as $term ) { - - # Exact match? No need to look further. - $title = Title::newFromText( $term ); - if ( is_null( $title ) ) { - return null; - } - - # Try files if searching in the Media: namespace - if ( $title->getNamespace() == NS_MEDIA ) { - $title = Title::makeTitle( NS_FILE, $title->getText() ); - } - - if ( $title->isSpecialPage() || $title->isExternal() || $title->exists() ) { - return $title; - } - - # See if it still otherwise has content is some sane sense - $page = WikiPage::factory( $title ); - if ( $page->hasViewableContent() ) { - return $title; - } - - if ( !Hooks::run( 'SearchAfterNoDirectMatch', [ $term, &$title ] ) ) { - return $title; - } - - # Now try all lower case (i.e. first letter capitalized) - $title = Title::newFromText( $wgContLang->lc( $term ) ); - if ( $title && $title->exists() ) { - return $title; - } - - # Now try capitalized string - $title = Title::newFromText( $wgContLang->ucwords( $term ) ); - if ( $title && $title->exists() ) { - return $title; - } - - # Now try all upper case - $title = Title::newFromText( $wgContLang->uc( $term ) ); - if ( $title && $title->exists() ) { - return $title; - } - - # Now try Word-Caps-Breaking-At-Word-Breaks, for hyphenated names etc - $title = Title::newFromText( $wgContLang->ucwordbreaks( $term ) ); - if ( $title && $title->exists() ) { - return $title; - } - - // Give hooks a chance at better match variants - $title = null; - if ( !Hooks::run( 'SearchGetNearMatch', [ $term, &$title ] ) ) { - return $title; - } - } - - $title = Title::newFromText( $searchterm ); - - # Entering an IP address goes to the contributions page - if ( $wgEnableSearchContributorsByIP ) { - if ( ( $title->getNamespace() == NS_USER && User::isIP( $title->getText() ) ) - || User::isIP( trim( $searchterm ) ) ) { - return SpecialPage::getTitleFor( 'Contributions', $title->getDBkey() ); - } - } - - # Entering a user goes to the user page whether it's there or not - if ( $title->getNamespace() == NS_USER ) { - return $title; - } - - # Go to images that exist even if there's no local page. - # There may have been a funny upload, or it may be on a shared - # file repository such as Wikimedia Commons. - if ( $title->getNamespace() == NS_FILE ) { - $image = wfFindFile( $title ); - if ( $image ) { - return $title; - } - } - - # MediaWiki namespace? Page may be "implied" if not customized. - # Just return it, with caps forced as the message system likes it. - if ( $title->getNamespace() == NS_MEDIAWIKI ) { - return Title::makeTitle( NS_MEDIAWIKI, $wgContLang->ucfirst( $title->getText() ) ); - } - - # Quoted term? Try without the quotes... - $matches = []; - if ( preg_match( '/^"([^"]+)"$/', $searchterm, $matches ) ) { - return SearchEngine::getNearMatch( $matches[1] ); - } - - return null; - } - public static function legalSearchChars() { return "A-Za-z_'.0-9\\x80-\\xFF\\-"; } @@ -390,44 +292,8 @@ class SearchEngine { return $parsed; } - /** - * Make a list of searchable namespaces and their canonical names. - * @return array - */ - public static function searchableNamespaces() { - global $wgContLang; - $arr = []; - foreach ( $wgContLang->getNamespaces() as $ns => $name ) { - if ( $ns >= NS_MAIN ) { - $arr[$ns] = $name; - } - } - - Hooks::run( 'SearchableNamespaces', [ &$arr ] ); - return $arr; - } - - /** - * Extract default namespaces to search from the given user's - * settings, returning a list of index numbers. - * - * @param user $user - * @return array - */ - public static function userNamespaces( $user ) { - $arr = []; - foreach ( SearchEngine::searchableNamespaces() as $ns => $name ) { - if ( $user->getOption( 'searchNs' . $ns ) ) { - $arr[] = $ns; - } - } - - return $arr; - } - /** * Find snippet highlight settings for all users - * * @return array Contextlines, contextchars */ public static function userHighlightPrefs() { @@ -436,77 +302,6 @@ class SearchEngine { return [ $contextlines, $contextchars ]; } - /** - * An array of namespaces indexes to be searched by default - * - * @return array - */ - public static function defaultNamespaces() { - global $wgNamespacesToBeSearchedDefault; - - return array_keys( $wgNamespacesToBeSearchedDefault, true ); - } - - /** - * Get a list of namespace names useful for showing in tooltips - * and preferences - * - * @param array $namespaces - * @return array - */ - public static function namespacesAsText( $namespaces ) { - global $wgContLang; - - $formatted = array_map( [ $wgContLang, 'getFormattedNsText' ], $namespaces ); - foreach ( $formatted as $key => $ns ) { - if ( empty( $ns ) ) { - $formatted[$key] = wfMessage( 'blanknamespace' )->text(); - } - } - return $formatted; - } - - /** - * Load up the appropriate search engine class for the currently - * active database backend, and return a configured instance. - * - * @param string $type Type of search backend, if not the default - * @return SearchEngine - */ - public static function create( $type = null ) { - global $wgSearchType; - $dbr = null; - - $alternatives = self::getSearchTypes(); - - if ( $type && in_array( $type, $alternatives ) ) { - $class = $type; - } elseif ( $wgSearchType !== null ) { - $class = $wgSearchType; - } else { - $dbr = wfGetDB( DB_SLAVE ); - $class = $dbr->getSearchEngine(); - } - - $search = new $class( $dbr ); - return $search; - } - - /** - * Return the search engines we support. If only $wgSearchType - * is set, it'll be an array of just that one item. - * - * @return array - */ - public static function getSearchTypes() { - global $wgSearchType, $wgSearchTypeAlternatives; - - $alternatives = $wgSearchTypeAlternatives ?: []; - array_unshift( $alternatives, $wgSearchType ); - - return $alternatives; - } - /** * Create or update the search index record for the given page. * Title and text should be pre-processed. @@ -774,6 +569,67 @@ class SearchEngine { return $backend->defaultSearchBackend( $this->namespaces, $search, $this->limit, $this->offset ); } + /** + * Make a list of searchable namespaces and their canonical names. + * @deprecated since 1.27; use SearchEngineConfig::searchableNamespaces() + * @return array + */ + public static function searchableNamespaces() { + return MediaWikiServices::getInstance()->getSearchEngineConfig()->searchableNamespaces(); + } + + /** + * Extract default namespaces to search from the given user's + * settings, returning a list of index numbers. + * @deprecated since 1.27; use SearchEngineConfig::userNamespaces() + * @param user $user + * @return array + */ + public static function userNamespaces( $user ) { + return MediaWikiServices::getInstance()->getSearchEngineConfig()->userNamespaces( $user ); + } + + /** + * An array of namespaces indexes to be searched by default + * @deprecated since 1.27; use SearchEngineConfig::defaultNamespaces() + * @return array + */ + public static function defaultNamespaces() { + return MediaWikiServices::getInstance()->getSearchEngineConfig()->defaultNamespaces(); + } + + /** + * Get a list of namespace names useful for showing in tooltips + * and preferences + * @deprecated since 1.27; use SearchEngineConfig::namespacesAsText() + * @param array $namespaces + * @return array + */ + public static function namespacesAsText( $namespaces ) { + return MediaWikiServices::getInstance()->getSearchEngineConfig()->namespacesAsText(); + } + + /** + * Load up the appropriate search engine class for the currently + * active database backend, and return a configured instance. + * @deprecated since 1.27; Use SearchEngineFactory::create + * @param string $type Type of search backend, if not the default + * @return SearchEngine + */ + public static function create( $type = null ) { + return MediaWikiServices::getInstance()->getSearchEngineFactory()->create( $type ); + } + + /** + * Return the search engines we support. If only $wgSearchType + * is set, it'll be an array of just that one item. + * @deprecated since 1.27; use SearchEngineConfig::getSearchTypes() + * @return array + */ + public static function getSearchTypes() { + return MediaWikiServices::getInstance()->getSearchEngineConfig()->getSearchTypes(); + } + } /** diff --git a/includes/search/SearchEngineConfig.php b/includes/search/SearchEngineConfig.php new file mode 100644 index 0000000000..3d996baa6a --- /dev/null +++ b/includes/search/SearchEngineConfig.php @@ -0,0 +1,110 @@ +config = $config; + } + + /** + * Retrieve original config. + * @return Config + */ + public function getConfig() { + return $this->config; + } + + /** + * Make a list of searchable namespaces and their canonical names. + * @return array Namespace ID => name + */ + public function searchableNamespaces() { + $arr = []; + foreach ( $this->config->get( 'ContLang' )->getNamespaces() as $ns => $name ) { + if ( $ns >= NS_MAIN ) { + $arr[$ns] = $name; + } + } + + Hooks::run( 'SearchableNamespaces', [ &$arr ] ); + return $arr; + } + + /** + * Extract default namespaces to search from the given user's + * settings, returning a list of index numbers. + * + * @param user $user + * @return int[] + */ + public function userNamespaces( $user ) { + $arr = []; + foreach ( $this->searchableNamespaces() as $ns => $name ) { + if ( $user->getOption( 'searchNs' . $ns ) ) { + $arr[] = $ns; + } + } + + return $arr; + } + + /** + * An array of namespaces indexes to be searched by default + * + * @return int[] Namespace IDs + */ + public function defaultNamespaces() { + return array_keys( $this->config->get( 'NamespacesToBeSearchedDefault' ), true ); + } + + /** + * Return the search engines we support. If only $wgSearchType + * is set, it'll be an array of just that one item. + * + * @return array + */ + public function getSearchTypes() { + $alternatives = $this->config->get( 'SearchTypeAlternatives' ) ?: []; + array_unshift( $alternatives, $this->config->get( 'SearchType' ) ); + + return $alternatives; + } + + /** + * Return the search engine configured in $wgSearchType, etc. + * + * @return string|null + */ + public function getSearchType() { + return $this->config->get( 'SearchType' ); + } + + /** + * Get a list of namespace names useful for showing in tooltips + * and preferences. + * + * @param int[] $namespaces + * @return string[] List of names + */ + public function namespacesAsText( $namespaces ) { + $formatted = array_map( [ $this->config->get( 'ContLang' ), 'getFormattedNsText' ], $namespaces ); + foreach ( $formatted as $key => $ns ) { + if ( empty( $ns ) ) { + $formatted[$key] = wfMessage( 'blanknamespace' )->text(); + } + } + return $formatted; + } +} diff --git a/includes/search/SearchEngineFactory.php b/includes/search/SearchEngineFactory.php new file mode 100644 index 0000000000..67f500c1ce --- /dev/null +++ b/includes/search/SearchEngineFactory.php @@ -0,0 +1,42 @@ +config = $config; + } + + /** + * Create SearchEngine of the given type. + * @param string $type + * @return SearchEngine + */ + public function create( $type = null ) { + $dbr = null; + + $configType = $this->config->getSearchType(); + $alternatives = $this->config->getSearchTypes(); + + if ( $type && in_array( $type, $alternatives ) ) { + $class = $type; + } elseif ( $configType !== null ) { + $class = $configType; + } else { + $dbr = wfGetDB( DB_SLAVE ); + $class = $dbr->getSearchEngine(); + } + + $search = new $class( $dbr ); + return $search; + } +} diff --git a/includes/search/SearchNearMatchResultSet.php b/includes/search/SearchNearMatchResultSet.php index 510726baaa..6d667074ee 100644 --- a/includes/search/SearchNearMatchResultSet.php +++ b/includes/search/SearchNearMatchResultSet.php @@ -1,6 +1,6 @@ config = $config; + } + + /** + * If an exact title match can be found, or a very slightly close match, + * return the title. If no match, returns NULL. + * + * @param string $searchterm + * @return Title + */ + public function getNearMatch( $searchterm ) { + $title = $this->getNearMatchInternal( $searchterm ); + + Hooks::run( 'SearchGetNearMatchComplete', [ $searchterm, &$title ] ); + return $title; + } + + /** + * Do a near match (see SearchEngine::getNearMatch) and wrap it into a + * SearchResultSet. + * + * @param string $searchterm + * @return SearchResultSet + */ + public function getNearMatchResultSet( $searchterm ) { + return new SearchNearMatchResultSet( $this->getNearMatch( $searchterm ) ); + } + + /** + * Really find the title match. + * @param string $searchterm + * @return null|Title + */ + protected function getNearMatchInternal( $searchterm ) { + $lang = $this->config->get( 'ContLang' ); + + $allSearchTerms = [ $searchterm ]; + + if ( $lang->hasVariants() ) { + $allSearchTerms = array_unique( array_merge( + $allSearchTerms, + $lang->autoConvertToAllVariants( $searchterm ) + ) ); + } + + $titleResult = null; + if ( !Hooks::run( 'SearchGetNearMatchBefore', [ $allSearchTerms, &$titleResult ] ) ) { + return $titleResult; + } + + foreach ( $allSearchTerms as $term ) { + + # Exact match? No need to look further. + $title = Title::newFromText( $term ); + if ( is_null( $title ) ) { + return null; + } + + # Try files if searching in the Media: namespace + if ( $title->getNamespace() == NS_MEDIA ) { + $title = Title::makeTitle( NS_FILE, $title->getText() ); + } + + if ( $title->isSpecialPage() || $title->isExternal() || $title->exists() ) { + return $title; + } + + # See if it still otherwise has content is some sane sense + $page = WikiPage::factory( $title ); + if ( $page->hasViewableContent() ) { + return $title; + } + + if ( !Hooks::run( 'SearchAfterNoDirectMatch', [ $term, &$title ] ) ) { + return $title; + } + + # Now try all lower case (i.e. first letter capitalized) + $title = Title::newFromText( $lang->lc( $term ) ); + if ( $title && $title->exists() ) { + return $title; + } + + # Now try capitalized string + $title = Title::newFromText( $lang->ucwords( $term ) ); + if ( $title && $title->exists() ) { + return $title; + } + + # Now try all upper case + $title = Title::newFromText( $lang->uc( $term ) ); + if ( $title && $title->exists() ) { + return $title; + } + + # Now try Word-Caps-Breaking-At-Word-Breaks, for hyphenated names etc + $title = Title::newFromText( $lang->ucwordbreaks( $term ) ); + if ( $title && $title->exists() ) { + return $title; + } + + // Give hooks a chance at better match variants + $title = null; + if ( !Hooks::run( 'SearchGetNearMatch', [ $term, &$title ] ) ) { + return $title; + } + } + + $title = Title::newFromText( $searchterm ); + + # Entering an IP address goes to the contributions page + if ( $this->config->get( 'EnableSearchContributorsByIP' ) ) { + if ( ( $title->getNamespace() == NS_USER && User::isIP( $title->getText() ) ) + || User::isIP( trim( $searchterm ) ) ) { + return SpecialPage::getTitleFor( 'Contributions', $title->getDBkey() ); + } + } + + # Entering a user goes to the user page whether it's there or not + if ( $title->getNamespace() == NS_USER ) { + return $title; + } + + # Go to images that exist even if there's no local page. + # There may have been a funny upload, or it may be on a shared + # file repository such as Wikimedia Commons. + if ( $title->getNamespace() == NS_FILE ) { + $image = wfFindFile( $title ); + if ( $image ) { + return $title; + } + } + + # MediaWiki namespace? Page may be "implied" if not customized. + # Just return it, with caps forced as the message system likes it. + if ( $title->getNamespace() == NS_MEDIAWIKI ) { + return Title::makeTitle( NS_MEDIAWIKI, $lang->ucfirst( $title->getText() ) ); + } + + # Quoted term? Try without the quotes... + $matches = []; + if ( preg_match( '/^"([^"]+)"$/', $searchterm, $matches ) ) { + return self::getNearMatch( $matches[1] ); + } + + return null; + } +} diff --git a/includes/search/SearchResult.php b/includes/search/SearchResult.php index 6c406e7e16..21effbbc98 100644 --- a/includes/search/SearchResult.php +++ b/includes/search/SearchResult.php @@ -21,6 +21,8 @@ * @ingroup Search */ +use MediaWiki\MediaWikiServices; + /** * @todo FIXME: This class is horribly factored. It would probably be better to * have a useful base class to which you pass some standard information, then @@ -49,6 +51,11 @@ class SearchResult { */ protected $mText; + /** + * @var SearchEngine + */ + protected $searchEngine; + /** * Return a new SearchResult and initializes it with a title. * @@ -56,7 +63,7 @@ class SearchResult { * @return SearchResult */ public static function newFromTitle( $title ) { - $result = new self(); + $result = new static(); $result->initFromTitle( $title ); return $result; } @@ -78,6 +85,7 @@ class SearchResult { $this->mImage = wfFindFile( $this->mTitle ); } } + $this->searchEngine = MediaWikiServices::getInstance()->newSearchEngine(); } /** @@ -119,8 +127,8 @@ class SearchResult { protected function initText() { if ( !isset( $this->mText ) ) { if ( $this->mRevision != null ) { - $this->mText = SearchEngine::create() - ->getTextFromContent( $this->mTitle, $this->mRevision->getContent() ); + $this->mText = $this->searchEngine->getTextFromContent( + $this->mTitle, $this->mRevision->getContent() ); } else { // TODO: can we fetch raw wikitext for commons images? $this->mText = ''; } @@ -136,7 +144,7 @@ class SearchResult { $this->initText(); // TODO: make highliter take a content object. Make ContentHandler a factory for SearchHighliter. - list( $contextlines, $contextchars ) = SearchEngine::userHighlightPrefs(); + list( $contextlines, $contextchars ) = $this->searchEngine->userHighlightPrefs(); $h = new SearchHighlighter(); if ( count( $terms ) > 0 ) { diff --git a/includes/specialpage/SpecialPage.php b/includes/specialpage/SpecialPage.php index fb153fcee2..b9237137ce 100644 --- a/includes/specialpage/SpecialPage.php +++ b/includes/specialpage/SpecialPage.php @@ -1,4 +1,6 @@ newSearchEngine(); $searchEngine->setLimitOffset( $limit, $offset ); $searchEngine->setNamespaces( [] ); $result = $searchEngine->defaultPrefixSearch( $search ); diff --git a/includes/specials/SpecialFileDuplicateSearch.php b/includes/specials/SpecialFileDuplicateSearch.php index bb82d03da9..6de127d3b8 100644 --- a/includes/specials/SpecialFileDuplicateSearch.php +++ b/includes/specials/SpecialFileDuplicateSearch.php @@ -1,4 +1,6 @@ newSearchEngine(); $searchEngine->setLimitOffset( $limit, $offset ); // Autocomplete subpage the same as a normal search, but just for files $searchEngine->setNamespaces( [ NS_FILE ] ); diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index 2bf8385745..20696382d4 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -23,6 +23,8 @@ * @ingroup SpecialPage */ +use MediaWiki\MediaWikiServices; + /** * implements Special:Search - Run text & title search and display the output * @ingroup SpecialPage @@ -79,10 +81,17 @@ class SpecialSearch extends SpecialPage { */ protected $customCaptions; + /** + * Search engine configurations. + * @var SearchEngineConfig + */ + protected $searchConfig; + const NAMESPACES_CURRENT = 'sense'; public function __construct() { parent::__construct( 'Search' ); + $this->searchConfig = MediaWikiServices::getInstance()->getSearchEngineConfig(); } /** @@ -150,7 +159,7 @@ class SpecialSearch extends SpecialPage { $nslist = $this->powerSearch( $request ); if ( !count( $nslist ) ) { # Fallback to user preference - $nslist = SearchEngine::userNamespaces( $user ); + $nslist = $this->searchConfig->userNamespaces( $user ); } $profile = null; @@ -202,7 +211,8 @@ class SpecialSearch extends SpecialPage { return; } # If there's an exact or very near match, jump right there. - $title = SearchEngine::getNearMatch( $term ); + $title = $this->newSearchEngine()-> + getNearMatcher( $this->getConfig() )->getNearMatch( $term ); if ( !is_null( $title ) && Hooks::run( 'SpecialSearchGoResult', [ $term, $title, &$url ] ) @@ -620,7 +630,7 @@ class SpecialSearch extends SpecialPage { */ protected function powerSearch( &$request ) { $arr = []; - foreach ( SearchEngine::searchableNamespaces() as $ns => $name ) { + foreach ( $this->searchConfig->searchableNamespaces() as $ns => $name ) { if ( $request->getCheck( 'ns' . $ns ) ) { $arr[] = $ns; } @@ -1021,7 +1031,7 @@ class SpecialSearch extends SpecialPage { // Groups namespaces into rows according to subject $rows = []; - foreach ( SearchEngine::searchableNamespaces() as $namespace => $name ) { + foreach ( $this->searchConfig->searchableNamespaces() as $namespace => $name ) { $subject = MWNamespace::getSubject( $namespace ); if ( !array_key_exists( $subject, $rows ) ) { $rows[$subject] = ""; @@ -1104,15 +1114,15 @@ class SpecialSearch extends SpecialPage { */ protected function getSearchProfiles() { // Builds list of Search Types (profiles) - $nsAllSet = array_keys( SearchEngine::searchableNamespaces() ); - + $nsAllSet = array_keys( $this->searchConfig->searchableNamespaces() ); + $defaultNs = $this->searchConfig->defaultNamespaces(); $profiles = [ 'default' => [ 'message' => 'searchprofile-articles', 'tooltip' => 'searchprofile-articles-tooltip', - 'namespaces' => SearchEngine::defaultNamespaces(), - 'namespace-messages' => SearchEngine::namespacesAsText( - SearchEngine::defaultNamespaces() + 'namespaces' => $defaultNs, + 'namespace-messages' => $this->searchConfig->namespacesAsText( + $defaultNs ), ], 'images' => [ @@ -1328,7 +1338,8 @@ class SpecialSearch extends SpecialPage { public function getSearchEngine() { if ( $this->searchEngine === null ) { $this->searchEngine = $this->searchEngineType ? - SearchEngine::create( $this->searchEngineType ) : SearchEngine::create(); + MediaWikiServices::getInstance()->getSearchEngineFactory()->create( $this->searchEngineType ) : + MediaWikiServices::getInstance()->newSearchEngine(); } return $this->searchEngine; diff --git a/includes/user/User.php b/includes/user/User.php index 601f9a5340..7c32c3b5a2 100644 --- a/includes/user/User.php +++ b/includes/user/User.php @@ -20,6 +20,7 @@ * @file */ +use MediaWiki\MediaWikiServices; use MediaWiki\Session\SessionManager; use MediaWiki\Session\Token; @@ -1534,7 +1535,8 @@ class User implements IDBAccessObject { foreach ( LanguageConverter::$languagesWithVariants as $langCode ) { $defOpt[$langCode == $wgContLang->getCode() ? 'variant' : "variant-$langCode"] = $langCode; } - foreach ( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) { + $namespaces = MediaWikiServices::getInstance()->getSearchEngineConfig()->searchableNamespaces(); + foreach ( $namespaces as $nsnum => $nsname ) { $defOpt['searchNs' . $nsnum] = !empty( $wgNamespacesToBeSearchedDefault[$nsnum] ); } $defOpt['skin'] = Skin::normalizeKey( $wgDefaultSkin ); diff --git a/tests/phpunit/includes/MediaWikiServicesTest.php b/tests/phpunit/includes/MediaWikiServicesTest.php index a89bd90931..0741445680 100644 --- a/tests/phpunit/includes/MediaWikiServicesTest.php +++ b/tests/phpunit/includes/MediaWikiServicesTest.php @@ -26,6 +26,9 @@ class MediaWikiServicesTest extends PHPUnit_Framework_TestCase { 'SiteLookup' => [ 'getSiteLookup', SiteLookup::class ], 'StatsdDataFactory' => [ 'getStatsdDataFactory', StatsdDataFactory::class ], 'EventRelayerGroup' => [ 'getEventRelayerGroup', EventRelayerGroup::class ], + 'SearchEngine' => [ 'newSearchEngine', SearchEngine::class ], + 'SearchEngineFactory' => [ 'getSearchEngineFactory', SearchEngineFactory::class ], + 'SearchEngineConfig' => [ 'getSearchEngineConfig', SearchEngineConfig::class ], ]; } @@ -51,6 +54,8 @@ class MediaWikiServicesTest extends PHPUnit_Framework_TestCase { 'SiteLookup' => [ 'SiteLookup', SiteLookup::class ], 'StatsdDataFactory' => [ 'StatsdDataFactory', StatsdDataFactory::class ], 'EventRelayerGroup' => [ 'EventRelayerGroup', EventRelayerGroup::class ], + 'SearchEngineFactory' => [ 'SearchEngineFactory', SearchEngineFactory::class ], + 'SearchEngineConfig' => [ 'SearchEngineConfig', SearchEngineConfig::class ], ]; } diff --git a/tests/phpunit/includes/deferred/SearchUpdateTest.php b/tests/phpunit/includes/deferred/SearchUpdateTest.php index 90438a0436..602a1757ec 100644 --- a/tests/phpunit/includes/deferred/SearchUpdateTest.php +++ b/tests/phpunit/includes/deferred/SearchUpdateTest.php @@ -20,13 +20,19 @@ class MockSearch extends SearchEngine { */ class SearchUpdateTest extends MediaWikiTestCase { + /** + * @var SearchUpdate + */ + private $su; + protected function setUp() { parent::setUp(); $this->setMwGlobals( 'wgSearchType', 'MockSearch' ); + $this->su = new SearchUpdate( 0, "" ); } public function updateText( $text ) { - return trim( SearchUpdate::updateText( $text ) ); + return trim( $this->su->updateText( $text ) ); } /** diff --git a/tests/phpunit/includes/search/SearchEnginePrefixTest.php b/tests/phpunit/includes/search/SearchEnginePrefixTest.php index 6a3f95bb22..9ed52444ea 100644 --- a/tests/phpunit/includes/search/SearchEnginePrefixTest.php +++ b/tests/phpunit/includes/search/SearchEnginePrefixTest.php @@ -1,4 +1,6 @@ setMwGlobals( 'wgSpecialPages', [] ); - $this->search = SearchEngine::create(); + $this->search = MediaWikiServices::getInstance()->newSearchEngine(); $this->search->setNamespaces( [] ); } diff --git a/tests/phpunit/includes/specials/SpecialSearchTest.php b/tests/phpunit/includes/specials/SpecialSearchTest.php index a4e4df166f..4ea9686680 100644 --- a/tests/phpunit/includes/specials/SpecialSearchTest.php +++ b/tests/phpunit/includes/specials/SpecialSearchTest.php @@ -1,4 +1,6 @@ getSearchEngineConfig()->defaultNamespaces(); $EMPTY_REQUEST = []; $NO_USER_PREF = null; -- 2.20.1