From 025f15a208a75de47a71d3d8515e4b2b975fae1d Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 30 Oct 2015 23:04:52 +0100 Subject: [PATCH] Factor InterwikiLookup out of Interwiki class. This keeps the existing app logic for looking up interwiki information intact in ClassicInterwikiLookup. The idea is to seamlessly switch to a new implementation when it becomes available, while also allowing us to switch back in case of problems. Change-Id: I7d7424345d0ce3ce90ba284006ee9615e3d99baa --- autoload.php | 2 + includes/MediaWikiServices.php | 9 + includes/ServiceWiring.php | 14 + includes/interwiki/ClassicInterwikiLookup.php | 453 ++++++++++++++++++ includes/interwiki/Interwiki.php | 335 +------------ includes/interwiki/InterwikiLookup.php | 63 +++ .../includes/MediaWikiServicesTest.php | 2 + .../interwiki/ClassicInterwikiLookupTest.php | 236 +++++++++ .../includes/interwiki/InterwikiTest.php | 19 +- 9 files changed, 805 insertions(+), 328 deletions(-) create mode 100644 includes/interwiki/ClassicInterwikiLookup.php create mode 100644 includes/interwiki/InterwikiLookup.php create mode 100644 tests/phpunit/includes/interwiki/ClassicInterwikiLookupTest.php diff --git a/autoload.php b/autoload.php index 4875fcbb54..aeb69fdd66 100644 --- a/autoload.php +++ b/autoload.php @@ -793,6 +793,8 @@ $wgAutoloadLocalClasses = [ 'MediaWikiSite' => __DIR__ . '/includes/site/MediaWikiSite.php', 'MediaWikiTitleCodec' => __DIR__ . '/includes/title/MediaWikiTitleCodec.php', 'MediaWikiVersionFetcher' => __DIR__ . '/includes/MediaWikiVersionFetcher.php', + 'MediaWiki\\Interwiki\\ClassicInterwikiLookup' => __DIR__ . '/includes/interwiki/ClassicInterwikiLookup.php', + 'MediaWiki\\Interwiki\\InterwikiLookup' => __DIR__ . '/includes/interwiki/InterwikiLookup.php', 'MediaWiki\\Auth\\AbstractAuthenticationProvider' => __DIR__ . '/includes/auth/AbstractAuthenticationProvider.php', 'MediaWiki\\Auth\\AbstractPasswordPrimaryAuthenticationProvider' => __DIR__ . '/includes/auth/AbstractPasswordPrimaryAuthenticationProvider.php', 'MediaWiki\\Auth\\AbstractPreAuthenticationProvider' => __DIR__ . '/includes/auth/AbstractPreAuthenticationProvider.php', diff --git a/includes/MediaWikiServices.php b/includes/MediaWikiServices.php index 71e58afdb7..e2dc691acc 100644 --- a/includes/MediaWikiServices.php +++ b/includes/MediaWikiServices.php @@ -24,6 +24,7 @@ use WatchedItemStore; use SkinFactory; use TitleFormatter; use TitleParser; +use MediaWiki\Interwiki\InterwikiLookup; /** * Service locator for MediaWiki core services. @@ -384,6 +385,14 @@ class MediaWikiServices extends ServiceContainer { return $this->getService( 'SiteStore' ); } + /** + * @since 1.28 + * @return InterwikiLookup + */ + public function getInterwikiLookup() { + return $this->getService( 'InterwikiLookup' ); + } + /** * @since 1.27 * @return StatsdDataFactory diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index 293e6eb176..6bdacf082e 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -37,6 +37,7 @@ * MediaWiki code base. */ +use MediaWiki\Interwiki\ClassicInterwikiLookup; use MediaWiki\MediaWikiServices; return [ @@ -88,6 +89,19 @@ return [ return $services->getConfigFactory()->makeConfig( 'main' ); }, + 'InterwikiLookup' => function( MediaWikiServices $services ) { + global $wgContLang; // TODO: manage $wgContLang as a service + $config = $services->getMainConfig(); + return new ClassicInterwikiLookup( + $wgContLang, + ObjectCache::getMainWANInstance(), + $config->get( 'InterwikiExpiry' ), + $config->get( 'InterwikiCache' ), + $config->get( 'InterwikiScopes' ), + $config->get( 'InterwikiFallbackSite' ) + ); + }, + 'StatsdDataFactory' => function( MediaWikiServices $services ) { return new BufferingStatsdDataFactory( rtrim( $services->getMainConfig()->get( 'StatsdMetricPrefix' ), '.' ) diff --git a/includes/interwiki/ClassicInterwikiLookup.php b/includes/interwiki/ClassicInterwikiLookup.php new file mode 100644 index 0000000000..6ac165ab82 --- /dev/null +++ b/includes/interwiki/ClassicInterwikiLookup.php @@ -0,0 +1,453 @@ +fetch( $prefix ). + * All work is done on slave, because this should *never* change (except during + * schema updates etc, which aren't wiki-related) + * + * @since 1.28 + */ +class ClassicInterwikiLookup implements InterwikiLookup { + + /** + * @var MapCacheLRU + */ + private $localCache; + + /** + * @var Language + */ + private $contentLanguage; + + /** + * @var WANObjectCache + */ + private $objectCache; + + /** + * @var int + */ + private $objectCacheExpiry; + + /** + * @var bool|array|string + */ + private $cdbData; + + /** + * @var int + */ + private $interwikiScopes; + + /** + * @var string + */ + private $fallbackSite; + + /** + * @var CdbReader|null + */ + private $cdbReader = null; + + /** + * @var string|null + */ + private $thisSite = null; + + /** + * @param Language $contentLanguage Language object used to convert prefixes to lower case + * @param WANObjectCache $objectCache Cache for interwiki info retrieved from the database + * @param int $objectCacheExpiry Expiry time for $objectCache, in seconds + * @param bool|array|string $cdbData The path of a CDB file, or + * an array resembling the contents of a CDB file, + * or false to use the database. + * @param int $interwikiScopes Specify number of domains to check for messages: + * - 1: Just local wiki level + * - 2: wiki and global levels + * - 3: site level as well as wiki and global levels + * @param string $fallbackSite The code to assume for the local site, + */ + function __construct( + Language $contentLanguage, + WANObjectCache $objectCache, + $objectCacheExpiry, + $cdbData, + $interwikiScopes, + $fallbackSite + ) { + $this->localCache = new MapCacheLRU( 100 ); + + $this->contentLanguage = $contentLanguage; + $this->objectCache = $objectCache; + $this->objectCacheExpiry = $objectCacheExpiry; + $this->cdbData = $cdbData; + $this->interwikiScopes = $interwikiScopes; + $this->fallbackSite = $fallbackSite; + } + + /** + * Check whether an interwiki prefix exists + * + * @param string $prefix Interwiki prefix to use + * @return bool Whether it exists + */ + public function isValidInterwiki( $prefix ) { + $result = $this->fetch( $prefix ); + + return (bool)$result; + } + + /** + * Fetch an Interwiki object + * + * @param string $prefix Interwiki prefix to use + * @return Interwiki|null|bool + */ + public function fetch( $prefix ) { + if ( $prefix == '' ) { + return null; + } + + $prefix = $this->contentLanguage->lc( $prefix ); + if ( $this->localCache->has( $prefix ) ) { + return $this->localCache->get( $prefix ); + } + + if ( $this->cdbData ) { + $iw = $this->getInterwikiCached( $prefix ); + } else { + $iw = $this->load( $prefix ); + if ( !$iw ) { + $iw = false; + } + } + $this->localCache->set( $prefix, $iw ); + + return $iw; + } + + /** + * Resets locally cached Interwiki objects. This is intended for use during testing only. + * This does not invalidate entries in the persistent cache, as invalidateCache() does. + * @since 1.27 + */ + public function resetLocalCache() { + $this->localCache->clear(); + } + + /** + * Purge the in-process and object cache for an interwiki prefix + * @param string $prefix + */ + public function invalidateCache( $prefix ) { + $this->localCache->clear( $prefix ); + + $key = $this->objectCache->makeKey( 'interwiki', $prefix ); + $this->objectCache->delete( $key ); + } + + /** + * Fetch interwiki prefix data from local cache in constant database. + * + * @note More logic is explained in DefaultSettings. + * + * @param string $prefix Interwiki prefix + * @return Interwiki + */ + private function getInterwikiCached( $prefix ) { + $value = $this->getInterwikiCacheEntry( $prefix ); + + if ( $value ) { + // Split values + list( $local, $url ) = explode( ' ', $value, 2 ); + return new Interwiki( $prefix, $url, '', '', (int)$local ); + } else { + return false; + } + } + + /** + * Get entry from interwiki cache + * + * @note More logic is explained in DefaultSettings. + * + * @param string $prefix Database key + * @return bool|string The interwiki entry or false if not found + */ + private function getInterwikiCacheEntry( $prefix ) { + wfDebug( __METHOD__ . "( $prefix )\n" ); + $value = false; + try { + // Resolve site name + if ( $this->interwikiScopes >= 3 && !$this->thisSite ) { + $this->thisSite = $this->getCacheValue( '__sites:' . wfWikiID() ); + if ( $this->thisSite == '' ) { + $this->thisSite = $this->fallbackSite; + } + } + + $value = $this->getCacheValue( wfMemcKey( $prefix ) ); + // Site level + if ( $value == '' && $this->interwikiScopes >= 3 ) { + $value = $this->getCacheValue( "_{$this->thisSite}:{$prefix}" ); + } + // Global Level + if ( $value == '' && $this->interwikiScopes >= 2 ) { + $value = $this->getCacheValue( "__global:{$prefix}" ); + } + if ( $value == 'undef' ) { + $value = ''; + } + } catch ( CdbException $e ) { + wfDebug( __METHOD__ . ": CdbException caught, error message was " + . $e->getMessage() ); + } + + return $value; + } + + private function getCacheValue( $key ) { + if ( $this->cdbReader === null ) { + if ( is_string( $this->cdbData ) ) { + $this->cdbReader = \Cdb\Reader::open( $this->cdbData ); + } elseif ( is_array( $this->cdbData ) ) { + $this->cdbReader = new \Cdb\Reader\Hash( $this->cdbData ); + } else { + $this->cdbReader = false; + } + } + + if ( $this->cdbReader ) { + return $this->cdbReader->get( $key ); + } else { + return false; + } + } + + /** + * Load the interwiki, trying first memcached then the DB + * + * @param string $prefix The interwiki prefix + * @return Interwiki|bool Interwiki if $prefix is valid, otherwise false + */ + private function load( $prefix ) { + $iwData = []; + if ( !Hooks::run( 'InterwikiLoadPrefix', [ $prefix, &$iwData ] ) ) { + return $this->loadFromArray( $iwData ); + } + + if ( is_array( $iwData ) ) { + $iw = $this->loadFromArray( $iwData ); + if ( $iw ) { + return $iw; // handled by hook + } + } + + $iwData = $this->objectCache->getWithSetCallback( + $this->objectCache->makeKey( 'interwiki', $prefix ), + $this->objectCacheExpiry, + function ( $oldValue, &$ttl, array &$setOpts ) use ( $prefix ) { + $dbr = wfGetDB( DB_SLAVE ); // TODO: inject LoadBalancer + + $setOpts += Database::getCacheSetOptions( $dbr ); + + $row = $dbr->selectRow( + 'interwiki', + ClassicInterwikiLookup::selectFields(), + [ 'iw_prefix' => $prefix ], + __METHOD__ + ); + + return $row ? (array)$row : '!NONEXISTENT'; + } + ); + + if ( is_array( $iwData ) ) { + return $this->loadFromArray( $iwData ) ?: false; + } + + return false; + } + + /** + * Fill in member variables from an array (e.g. memcached result, Database::fetchRow, etc) + * + * @param array $mc Associative array: row from the interwiki table + * @return Interwiki|bool Interwiki object or false if $mc['iw_url'] is not set + */ + private function loadFromArray( $mc ) { + if ( isset( $mc['iw_url'] ) ) { + $url = $mc['iw_url']; + $local = isset( $mc['iw_local'] ) ? $mc['iw_local'] : 0; + $trans = isset( $mc['iw_trans'] ) ? $mc['iw_trans'] : 0; + $api = isset( $mc['iw_api'] ) ? $mc['iw_api'] : ''; + $wikiId = isset( $mc['iw_wikiid'] ) ? $mc['iw_wikiid'] : ''; + + return new Interwiki( null, $url, $api, $wikiId, $local, $trans ); + } + + return false; + } + + /** + * Fetch all interwiki prefixes from interwiki cache + * + * @param null|string $local If not null, limits output to local/non-local interwikis + * @return array List of prefixes, where each row is an associative array + */ + private function getAllPrefixesCached( $local ) { + wfDebug( __METHOD__ . "()\n" ); + $data = []; + try { + /* Resolve site name */ + if ( $this->interwikiScopes >= 3 && !$this->thisSite ) { + $site = $this->getCacheValue( '__sites:' . wfWikiID() ); + + if ( $site == '' ) { + $this->thisSite = $this->fallbackSite; + } else { + $this->thisSite = $site; + } + } + + // List of interwiki sources + $sources = []; + // Global Level + if ( $this->interwikiScopes >= 2 ) { + $sources[] = '__global'; + } + // Site level + if ( $this->interwikiScopes >= 3 ) { + $sources[] = '_' . $this->thisSite; + } + $sources[] = wfWikiID(); + + foreach ( $sources as $source ) { + $list = $this->getCacheValue( '__list:' . $source ); + foreach ( explode( ' ', $list ) as $iw_prefix ) { + $row = $this->getCacheValue( "{$source}:{$iw_prefix}" ); + if ( !$row ) { + continue; + } + + list( $iw_local, $iw_url ) = explode( ' ', $row ); + + if ( $local !== null && $local != $iw_local ) { + continue; + } + + $data[$iw_prefix] = [ + 'iw_prefix' => $iw_prefix, + 'iw_url' => $iw_url, + 'iw_local' => $iw_local, + ]; + } + } + } catch ( CdbException $e ) { + wfDebug( __METHOD__ . ": CdbException caught, error message was " + . $e->getMessage() ); + } + + ksort( $data ); + + return array_values( $data ); + } + + /** + * Fetch all interwiki prefixes from DB + * + * @param string|null $local If not null, limits output to local/non-local interwikis + * @return array[] Interwiki rows + */ + private function getAllPrefixesDB( $local ) { + $db = wfGetDB( DB_SLAVE ); // TODO: inject DB LoadBalancer + + $where = []; + + if ( $local !== null ) { + if ( $local == 1 ) { + $where['iw_local'] = 1; + } elseif ( $local == 0 ) { + $where['iw_local'] = 0; + } + } + + $res = $db->select( 'interwiki', + $this->selectFields(), + $where, __METHOD__, [ 'ORDER BY' => 'iw_prefix' ] + ); + + $retval = []; + foreach ( $res as $row ) { + $retval[] = (array)$row; + } + + return $retval; + } + + /** + * Returns all interwiki prefixes + * + * @param string|null $local If set, limits output to local/non-local interwikis + * @return array[] Interwiki rows, where each row is an associative array + */ + public function getAllPrefixes( $local = null ) { + if ( $this->cdbData ) { + return $this->getAllPrefixesCached( $local ); + } + + return $this->getAllPrefixesDB( $local ); + } + + /** + * Return the list of interwiki fields that should be selected to create + * a new Interwiki object. + * @return string[] + */ + private static function selectFields() { + return [ + 'iw_prefix', + 'iw_url', + 'iw_api', + 'iw_wikiid', + 'iw_local', + 'iw_trans' + ]; + } + +} diff --git a/includes/interwiki/Interwiki.php b/includes/interwiki/Interwiki.php index 5a0dd36fa8..558e32c11e 100644 --- a/includes/interwiki/Interwiki.php +++ b/includes/interwiki/Interwiki.php @@ -19,19 +19,12 @@ * * @file */ -use \Cdb\Exception as CdbException; -use \Cdb\Reader as CdbReader; +use MediaWiki\MediaWikiServices; /** - * The interwiki class - * All information is loaded on creation when called by Interwiki::fetch( $prefix ). - * All work is done on slave, because this should *never* change (except during - * schema updates etc, which aren't wiki-related) + * Value object for representing interwiki records. */ class Interwiki { - // Cache - removes oldest entry when it hits limit - protected static $smCache = []; - const CACHE_LIMIT = 100; // 0 means unlimited, any other value is max number of entries. /** @var string The interwiki prefix, (e.g. "Meatball", or the language prefix "de") */ protected $mPrefix; @@ -67,335 +60,48 @@ class Interwiki { /** * Check whether an interwiki prefix exists * + * @deprecated since 1.28, use InterwikiLookup instead + * * @param string $prefix Interwiki prefix to use * @return bool Whether it exists */ public static function isValidInterwiki( $prefix ) { - $result = self::fetch( $prefix ); - - return (bool)$result; + return MediaWikiServices::getInstance()->getInterwikiLookup()->isValidInterwiki( $prefix ); } /** * Fetch an Interwiki object * + * @deprecated since 1.28, use InterwikiLookup instead + * * @param string $prefix Interwiki prefix to use * @return Interwiki|null|bool */ public static function fetch( $prefix ) { - global $wgContLang; - - if ( $prefix == '' ) { - return null; - } - - $prefix = $wgContLang->lc( $prefix ); - if ( isset( self::$smCache[$prefix] ) ) { - return self::$smCache[$prefix]; - } - - global $wgInterwikiCache; - if ( $wgInterwikiCache ) { - $iw = Interwiki::getInterwikiCached( $prefix ); - } else { - $iw = Interwiki::load( $prefix ); - if ( !$iw ) { - $iw = false; - } - } - - if ( self::CACHE_LIMIT && count( self::$smCache ) >= self::CACHE_LIMIT ) { - reset( self::$smCache ); - unset( self::$smCache[key( self::$smCache )] ); - } - - self::$smCache[$prefix] = $iw; - - return $iw; - } - - /** - * Resets locally cached Interwiki objects. This is intended for use during testing only. - * This does not invalidate entries in the persistent cache, as invalidateCache() does. - * @since 1.27 - */ - public static function resetLocalCache() { - static::$smCache = []; + return MediaWikiServices::getInstance()->getInterwikiLookup()->fetch( $prefix ); } /** * Purge the cache (local and persistent) for an interwiki prefix. + * * @param string $prefix * @since 1.26 */ public static function invalidateCache( $prefix ) { - $cache = ObjectCache::getMainWANInstance(); - $key = wfMemcKey( 'interwiki', $prefix ); - $cache->delete( $key ); - unset( static::$smCache[$prefix] ); - } - - /** - * Fetch interwiki prefix data from local cache in constant database. - * - * @note More logic is explained in DefaultSettings. - * - * @param string $prefix Interwiki prefix - * @return Interwiki - */ - protected static function getInterwikiCached( $prefix ) { - $value = self::getInterwikiCacheEntry( $prefix ); - - $s = new Interwiki( $prefix ); - if ( $value ) { - // Split values - list( $local, $url ) = explode( ' ', $value, 2 ); - $s->mURL = $url; - $s->mLocal = (bool)$local; - } else { - $s = false; - } - - return $s; - } - - /** - * Get entry from interwiki cache - * - * @note More logic is explained in DefaultSettings. - * - * @param string $prefix Database key - * @return bool|string The interwiki entry or false if not found - */ - protected static function getInterwikiCacheEntry( $prefix ) { - global $wgInterwikiScopes, $wgInterwikiFallbackSite; - static $site; - - $value = false; - try { - // Resolve site name - if ( $wgInterwikiScopes >= 3 && !$site ) { - $site = self::getCacheValue( '__sites:' . wfWikiID() ); - if ( $site == '' ) { - $site = $wgInterwikiFallbackSite; - } - } - - $value = self::getCacheValue( wfMemcKey( $prefix ) ); - // Site level - if ( $value == '' && $wgInterwikiScopes >= 3 ) { - $value = self::getCacheValue( "_{$site}:{$prefix}" ); - } - // Global Level - if ( $value == '' && $wgInterwikiScopes >= 2 ) { - $value = self::getCacheValue( "__global:{$prefix}" ); - } - if ( $value == 'undef' ) { - $value = ''; - } - } catch ( CdbException $e ) { - wfDebug( __METHOD__ . ": CdbException caught, error message was " - . $e->getMessage() ); - } - - return $value; - } - - private static function getCacheValue( $key ) { - global $wgInterwikiCache; - static $reader; - if ( $reader === null ) { - $reader = is_array( $wgInterwikiCache ) ? false : CdbReader::open( $wgInterwikiCache ); - } - if ( $reader ) { - return $reader->get( $key ); - } else { - return isset( $wgInterwikiCache[$key] ) ? $wgInterwikiCache[$key] : false; - } - } - - /** - * Load the interwiki, trying first memcached then the DB - * - * @param string $prefix The interwiki prefix - * @return Interwiki|bool Interwiki if $prefix is valid, otherwise false - */ - protected static function load( $prefix ) { - global $wgInterwikiExpiry; - - $iwData = []; - if ( !Hooks::run( 'InterwikiLoadPrefix', [ $prefix, &$iwData ] ) ) { - return Interwiki::loadFromArray( $iwData ); - } - - if ( is_array( $iwData ) ) { - $iw = Interwiki::loadFromArray( $iwData ); - if ( $iw ) { - return $iw; // handled by hook - } - } - - $iwData = ObjectCache::getMainWANInstance()->getWithSetCallback( - wfMemcKey( 'interwiki', $prefix ), - $wgInterwikiExpiry, - function ( $oldValue, &$ttl, array &$setOpts ) use ( $prefix ) { - $dbr = wfGetDB( DB_SLAVE ); - - $setOpts += Database::getCacheSetOptions( $dbr ); - - $row = $dbr->selectRow( - 'interwiki', - Interwiki::selectFields(), - [ 'iw_prefix' => $prefix ], - __METHOD__ - ); - - return $row ? (array)$row : '!NONEXISTENT'; - } - ); - - if ( is_array( $iwData ) ) { - return Interwiki::loadFromArray( $iwData ) ?: false; - } - - return false; - } - - /** - * Fill in member variables from an array (e.g. memcached result, Database::fetchRow, etc) - * - * @param array $mc Associative array: row from the interwiki table - * @return Interwiki|bool Interwiki object or false if $mc['iw_url'] is not set - */ - protected static function loadFromArray( $mc ) { - if ( isset( $mc['iw_url'] ) ) { - $iw = new Interwiki(); - $iw->mURL = $mc['iw_url']; - $iw->mLocal = isset( $mc['iw_local'] ) ? (bool)$mc['iw_local'] : false; - $iw->mTrans = isset( $mc['iw_trans'] ) ? (bool)$mc['iw_trans'] : false; - $iw->mAPI = isset( $mc['iw_api'] ) ? $mc['iw_api'] : ''; - $iw->mWikiID = isset( $mc['iw_wikiid'] ) ? $mc['iw_wikiid'] : ''; - - return $iw; - } - - return false; - } - - /** - * Fetch all interwiki prefixes from interwiki cache - * - * @param null|string $local If not null, limits output to local/non-local interwikis - * @return array List of prefixes - * @since 1.19 - */ - protected static function getAllPrefixesCached( $local ) { - global $wgInterwikiScopes, $wgInterwikiFallbackSite; - static $site; - - wfDebug( __METHOD__ . "()\n" ); - $data = []; - try { - /* Resolve site name */ - if ( $wgInterwikiScopes >= 3 && !$site ) { - $site = self::getCacheValue( '__sites:' . wfWikiID() ); - - if ( $site == '' ) { - $site = $wgInterwikiFallbackSite; - } - } - - // List of interwiki sources - $sources = []; - // Global Level - if ( $wgInterwikiScopes >= 2 ) { - $sources[] = '__global'; - } - // Site level - if ( $wgInterwikiScopes >= 3 ) { - $sources[] = '_' . $site; - } - $sources[] = wfWikiID(); - - foreach ( $sources as $source ) { - $list = self::getCacheValue( '__list:' . $source ); - foreach ( explode( ' ', $list ) as $iw_prefix ) { - $row = self::getCacheValue( "{$source}:{$iw_prefix}" ); - if ( !$row ) { - continue; - } - - list( $iw_local, $iw_url ) = explode( ' ', $row ); - - if ( $local !== null && $local != $iw_local ) { - continue; - } - - $data[$iw_prefix] = [ - 'iw_prefix' => $iw_prefix, - 'iw_url' => $iw_url, - 'iw_local' => $iw_local, - ]; - } - } - } catch ( CdbException $e ) { - wfDebug( __METHOD__ . ": CdbException caught, error message was " - . $e->getMessage() ); - } - - ksort( $data ); - - return array_values( $data ); - } - - /** - * Fetch all interwiki prefixes from DB - * - * @param string|null $local If not null, limits output to local/non-local interwikis - * @return array List of prefixes - * @since 1.19 - */ - protected static function getAllPrefixesDB( $local ) { - $db = wfGetDB( DB_SLAVE ); - - $where = []; - - if ( $local !== null ) { - if ( $local == 1 ) { - $where['iw_local'] = 1; - } elseif ( $local == 0 ) { - $where['iw_local'] = 0; - } - } - - $res = $db->select( 'interwiki', - self::selectFields(), - $where, __METHOD__, [ 'ORDER BY' => 'iw_prefix' ] - ); - - $retval = []; - foreach ( $res as $row ) { - $retval[] = (array)$row; - } - - return $retval; + return MediaWikiServices::getInstance()->getInterwikiLookup()->invalidateCache( $prefix ); } /** * Returns all interwiki prefixes * + * @deprecated since 1.28, unused. Use InterwikiLookup instead. + * * @param string|null $local If set, limits output to local/non-local interwikis * @return array List of prefixes * @since 1.19 */ public static function getAllPrefixes( $local = null ) { - global $wgInterwikiCache; - - if ( $wgInterwikiCache ) { - return self::getAllPrefixesCached( $local ); - } - - return self::getAllPrefixesDB( $local ); + return MediaWikiServices::getInstance()->getInterwikiLookup()->getAllPrefixes( $local ); } /** @@ -476,19 +182,4 @@ class Interwiki { return !$msg->exists() ? '' : $msg->text(); } - /** - * Return the list of interwiki fields that should be selected to create - * a new Interwiki object. - * @return string[] - */ - public static function selectFields() { - return [ - 'iw_prefix', - 'iw_url', - 'iw_api', - 'iw_wikiid', - 'iw_local', - 'iw_trans' - ]; - } } diff --git a/includes/interwiki/InterwikiLookup.php b/includes/interwiki/InterwikiLookup.php new file mode 100644 index 0000000000..459910a0a9 --- /dev/null +++ b/includes/interwiki/InterwikiLookup.php @@ -0,0 +1,63 @@ + [ 'SiteStore', SiteStore::class ], 'SiteLookup' => [ 'SiteLookup', SiteLookup::class ], 'StatsdDataFactory' => [ 'StatsdDataFactory', StatsdDataFactory::class ], + 'InterwikiLookup' => [ 'InterwikiLookup', InterwikiLookup::class ], 'EventRelayerGroup' => [ 'EventRelayerGroup', EventRelayerGroup::class ], 'SearchEngineFactory' => [ 'SearchEngineFactory', SearchEngineFactory::class ], 'SearchEngineConfig' => [ 'SearchEngineConfig', SearchEngineConfig::class ], diff --git a/tests/phpunit/includes/interwiki/ClassicInterwikiLookupTest.php b/tests/phpunit/includes/interwiki/ClassicInterwikiLookupTest.php new file mode 100644 index 0000000000..db6d00295c --- /dev/null +++ b/tests/phpunit/includes/interwiki/ClassicInterwikiLookupTest.php @@ -0,0 +1,236 @@ +delete( 'interwiki', '*', __METHOD__ ); + $dbw->insert( 'interwiki', array_values( $iwrows ), __METHOD__ ); + $this->tablesUsed[] = 'interwiki'; + } + + public function testDatabaseStorage() { + // NOTE: database setup is expensive, so we only do + // it once and run all the tests in one go. + $dewiki = [ + 'iw_prefix' => 'de', + 'iw_url' => 'http://de.wikipedia.org/wiki/', + 'iw_api' => 'http://de.wikipedia.org/w/api.php', + 'iw_wikiid' => 'dewiki', + 'iw_local' => 1, + 'iw_trans' => 0 + ]; + + $zzwiki = [ + 'iw_prefix' => 'zz', + 'iw_url' => 'http://zzwiki.org/wiki/', + 'iw_api' => 'http://zzwiki.org/w/api.php', + 'iw_wikiid' => 'zzwiki', + 'iw_local' => 0, + 'iw_trans' => 0 + ]; + + $this->populateDB( [ $dewiki, $zzwiki ] ); + $lookup = new \MediaWiki\Interwiki\ClassicInterwikiLookup( + Language::factory( 'en' ), + WANObjectCache::newEmpty(), + 60*60, + false, + 3, + 'en' + ); + + $this->assertEquals( + [ $dewiki, $zzwiki ], + $lookup->getAllPrefixes(), + 'getAllPrefixes()' + ); + $this->assertEquals( + [ $dewiki ], + $lookup->getAllPrefixes( true ), + 'getAllPrefixes()' + ); + $this->assertEquals( + [ $zzwiki ], + $lookup->getAllPrefixes( false ), + 'getAllPrefixes()' + ); + + $this->assertTrue( $lookup->isValidInterwiki( 'de' ), 'known prefix is valid' ); + $this->assertFalse( $lookup->isValidInterwiki( 'xyz' ), 'unknown prefix is valid' ); + + $this->assertNull( $lookup->fetch( null ), 'no prefix' ); + $this->assertFalse( $lookup->fetch( 'xyz' ), 'unknown prefix' ); + + $interwiki = $lookup->fetch( 'de' ); + $this->assertInstanceOf( 'Interwiki', $interwiki ); + $this->assertSame( $interwiki, $lookup->fetch( 'de' ), 'in-process caching' ); + + $this->assertSame( 'http://de.wikipedia.org/wiki/', $interwiki->getURL(), 'getURL' ); + $this->assertSame( 'http://de.wikipedia.org/w/api.php', $interwiki->getAPI(), 'getAPI' ); + $this->assertSame( 'dewiki', $interwiki->getWikiID(), 'getWikiID' ); + $this->assertSame( true, $interwiki->isLocal(), 'isLocal' ); + $this->assertSame( false, $interwiki->isTranscludable(), 'isTranscludable' ); + + $lookup->invalidateCache( 'de' ); + $this->assertNotSame( $interwiki, $lookup->fetch( 'de' ), 'invalidate cache' ); + } + + /** + * @param string $thisSite + * @param string[] $local + * @param string[] $global + * + * @return string[] + */ + private function populateHash( $thisSite, $local, $global ) { + $hash = []; + $hash[ '__sites:' . wfWikiID() ] = $thisSite; + + $globals = []; + $locals = []; + + foreach ( $local as $row ) { + $prefix = $row['iw_prefix']; + $data = $row['iw_local'] . ' ' . $row['iw_url']; + $locals[] = $prefix; + $hash[ "_{$thisSite}:{$prefix}" ] = $data; + } + + foreach ( $global as $row ) { + $prefix = $row['iw_prefix']; + $data = $row['iw_local'] . ' ' . $row['iw_url']; + $globals[] = $prefix; + $hash[ "__global:{$prefix}" ] = $data; + } + + $hash[ '__list:__global' ] = implode( ' ', $globals ); + $hash[ '__list:_' . $thisSite ] = implode( ' ', $locals ); + + return $hash; + } + + private function populateCDB( $thisSite, $local, $global ) { + $cdbFile = tempnam( wfTempDir(), 'MW-ClassicInterwikiLookupTest-' ) . '.cdb'; + $cdb = \Cdb\Writer::open( $cdbFile ); + + $hash = $this->populateHash( $thisSite, $local, $global ); + + foreach ( $hash as $key => $value ) { + $cdb->set( $key, $value ); + } + + $cdb->close(); + return $cdbFile; + } + + public function testCDBStorage() { + // NOTE: CDB setup is expensive, so we only do + // it once and run all the tests in one go. + + $dewiki = [ + 'iw_prefix' => 'de', + 'iw_url' => 'http://de.wikipedia.org/wiki/', + 'iw_local' => 1 + ]; + + $zzwiki = [ + 'iw_prefix' => 'zz', + 'iw_url' => 'http://zzwiki.org/wiki/', + 'iw_local' => 0 + ]; + + $cdbFile = $this->populateCDB( + 'en', + [ $dewiki ], + [ $zzwiki ] + ); + $lookup = new \MediaWiki\Interwiki\ClassicInterwikiLookup( + Language::factory( 'en' ), + WANObjectCache::newEmpty(), + 60*60, + $cdbFile, + 3, + 'en' + ); + + $this->assertEquals( + [ $dewiki, $zzwiki ], + $lookup->getAllPrefixes(), + 'getAllPrefixes()' + ); + + $this->assertTrue( $lookup->isValidInterwiki( 'de' ), 'known prefix is valid' ); + $this->assertTrue( $lookup->isValidInterwiki( 'zz' ), 'known prefix is valid' ); + + $interwiki = $lookup->fetch( 'de' ); + $this->assertInstanceOf( 'Interwiki', $interwiki ); + + $this->assertSame( 'http://de.wikipedia.org/wiki/', $interwiki->getURL(), 'getURL' ); + $this->assertSame( true, $interwiki->isLocal(), 'isLocal' ); + + $interwiki = $lookup->fetch( 'zz' ); + $this->assertInstanceOf( 'Interwiki', $interwiki ); + + $this->assertSame( 'http://zzwiki.org/wiki/', $interwiki->getURL(), 'getURL' ); + $this->assertSame( false, $interwiki->isLocal(), 'isLocal' ); + + // cleanup temp file + unlink( $cdbFile ); + } + + public function testArrayStorage() { + $dewiki = [ + 'iw_prefix' => 'de', + 'iw_url' => 'http://de.wikipedia.org/wiki/', + 'iw_local' => 1 + ]; + + $zzwiki = [ + 'iw_prefix' => 'zz', + 'iw_url' => 'http://zzwiki.org/wiki/', + 'iw_local' => 0 + ]; + + $hash = $this->populateHash( + 'en', + [ $dewiki ], + [ $zzwiki ] + ); + $lookup = new \MediaWiki\Interwiki\ClassicInterwikiLookup( + Language::factory( 'en' ), + WANObjectCache::newEmpty(), + 60*60, + $hash, + 3, + 'en' + ); + + $this->assertEquals( + [ $dewiki, $zzwiki ], + $lookup->getAllPrefixes(), + 'getAllPrefixes()' + ); + + $this->assertTrue( $lookup->isValidInterwiki( 'de' ), 'known prefix is valid' ); + $this->assertTrue( $lookup->isValidInterwiki( 'zz' ), 'known prefix is valid' ); + + $interwiki = $lookup->fetch( 'de' ); + $this->assertInstanceOf( 'Interwiki', $interwiki ); + + $this->assertSame( 'http://de.wikipedia.org/wiki/', $interwiki->getURL(), 'getURL' ); + $this->assertSame( true, $interwiki->isLocal(), 'isLocal' ); + + $interwiki = $lookup->fetch( 'zz' ); + $this->assertInstanceOf( 'Interwiki', $interwiki ); + + $this->assertSame( 'http://zzwiki.org/wiki/', $interwiki->getURL(), 'getURL' ); + $this->assertSame( false, $interwiki->isLocal(), 'isLocal' ); + } + +} diff --git a/tests/phpunit/includes/interwiki/InterwikiTest.php b/tests/phpunit/includes/interwiki/InterwikiTest.php index 411d6a3ff6..137dfb77ec 100644 --- a/tests/phpunit/includes/interwiki/InterwikiTest.php +++ b/tests/phpunit/includes/interwiki/InterwikiTest.php @@ -1,4 +1,6 @@ tablesUsed[] = 'interwiki'; } + private function setWgInterwikiCache( $interwikiCache ) { + $this->overrideMwServices(); + MediaWikiServices::getInstance()->resetServiceForTesting( 'InterwikiLookup' ); + $this->setMwGlobals( 'wgInterwikiCache', $interwikiCache ); + } + public function testDatabaseStorage() { + $this->markTestSkipped( 'Needs I37b8e8018b3 ' ); + // NOTE: database setup is expensive, so we only do // it once and run all the tests in one go. $dewiki = [ @@ -70,8 +80,7 @@ class InterwikiTest extends MediaWikiTestCase { $this->populateDB( [ $dewiki, $zzwiki ] ); - Interwiki::resetLocalCache(); - $this->setMwGlobals( 'wgInterwikiCache', false ); + $this->setWgInterwikiCache( false ); $this->assertEquals( [ $dewiki, $zzwiki ], @@ -179,8 +188,7 @@ class InterwikiTest extends MediaWikiTestCase { [ $zzwiki ] ); - Interwiki::resetLocalCache(); - $this->setMwGlobals( 'wgInterwikiCache', $cdbFile ); + $this->setWgInterwikiCache( $cdbFile ); $this->assertEquals( [ $dewiki, $zzwiki ], @@ -226,8 +234,7 @@ class InterwikiTest extends MediaWikiTestCase { [ $zzwiki ] ); - Interwiki::resetLocalCache(); - $this->setMwGlobals( 'wgInterwikiCache', $cdbData ); + $this->setWgInterwikiCache( $cdbData ); $this->assertEquals( [ $dewiki, $zzwiki ], -- 2.20.1