diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php
index 98367363942..a7193d11542 100644
--- a/includes/ServiceWiring.php
+++ b/includes/ServiceWiring.php
@@ -1,750 +1,751 @@
function ( MediaWikiServices $services ) : ActorMigration {
return new ActorMigration(
$services->getMainConfig()->get( 'ActorTableSchemaMigrationStage' )
);
},
'BlobStore' => function ( MediaWikiServices $services ) : BlobStore {
return $services->getService( '_SqlBlobStore' );
},
'BlobStoreFactory' => function ( MediaWikiServices $services ) : BlobStoreFactory {
return new BlobStoreFactory(
$services->getDBLoadBalancerFactory(),
$services->getMainWANObjectCache(),
new ServiceOptions( BlobStoreFactory::$constructorOptions,
$services->getMainConfig() ),
$services->getContentLanguage()
);
},
'BlockManager' => function ( MediaWikiServices $services ) : BlockManager {
$config = $services->getMainConfig();
$context = RequestContext::getMain();
return new BlockManager(
$context->getUser(),
$context->getRequest(),
$config->get( 'ApplyIpBlocksToXff' ),
$config->get( 'CookieSetOnAutoblock' ),
$config->get( 'CookieSetOnIpBlock' ),
$config->get( 'DnsBlacklistUrls' ),
$config->get( 'EnableDnsBlacklist' ),
$config->get( 'ProxyList' ),
$config->get( 'ProxyWhitelist' ),
$config->get( 'SoftBlockRanges' )
);
},
'BlockRestrictionStore' => function ( MediaWikiServices $services ) : BlockRestrictionStore {
return new BlockRestrictionStore(
$services->getDBLoadBalancer()
);
},
'CommentStore' => function ( MediaWikiServices $services ) : CommentStore {
return new CommentStore(
$services->getContentLanguage(),
MIGRATION_NEW
);
},
'ConfigFactory' => function ( MediaWikiServices $services ) : ConfigFactory {
// Use the bootstrap config to initialize the ConfigFactory.
$registry = $services->getBootstrapConfig()->get( 'ConfigRegistry' );
$factory = new ConfigFactory();
foreach ( $registry as $name => $callback ) {
$factory->register( $name, $callback );
}
return $factory;
},
'ConfigRepository' => function ( MediaWikiServices $services ) : ConfigRepository {
return new ConfigRepository( $services->getConfigFactory() );
},
'ConfiguredReadOnlyMode' => function ( MediaWikiServices $services ) : ConfiguredReadOnlyMode {
$config = $services->getMainConfig();
return new ConfiguredReadOnlyMode(
$config->get( 'ReadOnly' ),
$config->get( 'ReadOnlyFile' )
);
},
'ContentLanguage' => function ( MediaWikiServices $services ) : Language {
return Language::factory( $services->getMainConfig()->get( 'LanguageCode' ) );
},
'CryptHKDF' => function ( MediaWikiServices $services ) : CryptHKDF {
$config = $services->getMainConfig();
$secret = $config->get( 'HKDFSecret' ) ?: $config->get( 'SecretKey' );
if ( !$secret ) {
throw new RuntimeException( "Cannot use MWCryptHKDF without a secret." );
}
// In HKDF, the context can be known to the attacker, but this will
// keep simultaneous runs from producing the same output.
$context = [ microtime(), getmypid(), gethostname() ];
// Setup salt cache. Use APC, or fallback to the main cache if it isn't setup
$cache = $services->getLocalServerObjectCache();
if ( $cache instanceof EmptyBagOStuff ) {
$cache = ObjectCache::getLocalClusterInstance();
}
return new CryptHKDF( $secret, $config->get( 'HKDFAlgorithm' ), $cache, $context );
},
'DateFormatterFactory' => function () : DateFormatterFactory {
return new DateFormatterFactory;
},
'DBLoadBalancer' => function ( MediaWikiServices $services ) : Wikimedia\Rdbms\LoadBalancer {
// just return the default LB from the DBLoadBalancerFactory service
return $services->getDBLoadBalancerFactory()->getMainLB();
},
'DBLoadBalancerFactory' =>
function ( MediaWikiServices $services ) : Wikimedia\Rdbms\LBFactory {
$mainConfig = $services->getMainConfig();
$lbConf = MWLBFactory::applyDefaultConfig(
$mainConfig->get( 'LBFactoryConf' ),
new ServiceOptions( MWLBFactory::$applyDefaultConfigOptions, $mainConfig ),
$services->getConfiguredReadOnlyMode(),
$services->getLocalServerObjectCache(),
$services->getMainObjectStash(),
$services->getMainWANObjectCache()
);
$class = MWLBFactory::getLBFactoryClass( $lbConf );
$instance = new $class( $lbConf );
MWLBFactory::setSchemaAliases( $instance, $mainConfig->get( 'DBtype' ) );
return $instance;
},
'EventRelayerGroup' => function ( MediaWikiServices $services ) : EventRelayerGroup {
return new EventRelayerGroup( $services->getMainConfig()->get( 'EventRelayerConfig' ) );
},
'ExternalStoreFactory' => function ( MediaWikiServices $services ) : ExternalStoreFactory {
$config = $services->getMainConfig();
return new ExternalStoreFactory(
$config->get( 'ExternalStores' )
);
},
'GenderCache' => function ( MediaWikiServices $services ) : GenderCache {
return new GenderCache( $services->getNamespaceInfo() );
},
'HttpRequestFactory' =>
function ( MediaWikiServices $services ) : \MediaWiki\Http\HttpRequestFactory {
return new \MediaWiki\Http\HttpRequestFactory();
},
'InterwikiLookup' => function ( MediaWikiServices $services ) : InterwikiLookup {
$config = $services->getMainConfig();
return new ClassicInterwikiLookup(
$services->getContentLanguage(),
$services->getMainWANObjectCache(),
$config->get( 'InterwikiExpiry' ),
$config->get( 'InterwikiCache' ),
$config->get( 'InterwikiScopes' ),
$config->get( 'InterwikiFallbackSite' )
);
},
'LinkCache' => function ( MediaWikiServices $services ) : LinkCache {
return new LinkCache(
$services->getTitleFormatter(),
- $services->getMainWANObjectCache()
+ $services->getMainWANObjectCache(),
+ $services->getNamespaceInfo()
);
},
'LinkRenderer' => function ( MediaWikiServices $services ) : LinkRenderer {
if ( defined( 'MW_NO_SESSION' ) ) {
return $services->getLinkRendererFactory()->create();
} else {
return $services->getLinkRendererFactory()->createForUser(
RequestContext::getMain()->getUser()
);
}
},
'LinkRendererFactory' => function ( MediaWikiServices $services ) : LinkRendererFactory {
return new LinkRendererFactory(
$services->getTitleFormatter(),
$services->getLinkCache()
);
},
'LocalServerObjectCache' => function ( MediaWikiServices $services ) : BagOStuff {
$cacheId = \ObjectCache::detectLocalServerCache();
return \ObjectCache::newFromId( $cacheId );
},
'MagicWordFactory' => function ( MediaWikiServices $services ) : MagicWordFactory {
return new MagicWordFactory( $services->getContentLanguage() );
},
'MainConfig' => function ( MediaWikiServices $services ) : Config {
// Use the 'main' config from the ConfigFactory service.
return $services->getConfigFactory()->makeConfig( 'main' );
},
'MainObjectStash' => function ( MediaWikiServices $services ) : BagOStuff {
$mainConfig = $services->getMainConfig();
$id = $mainConfig->get( 'MainStash' );
if ( !isset( $mainConfig->get( 'ObjectCaches' )[$id] ) ) {
throw new UnexpectedValueException(
"Cache type \"$id\" is not present in \$wgObjectCaches." );
}
return \ObjectCache::newFromParams( $mainConfig->get( 'ObjectCaches' )[$id] );
},
'MainWANObjectCache' => function ( MediaWikiServices $services ) : WANObjectCache {
$mainConfig = $services->getMainConfig();
$id = $mainConfig->get( 'MainWANCache' );
if ( !isset( $mainConfig->get( 'WANObjectCaches' )[$id] ) ) {
throw new UnexpectedValueException(
"WAN cache type \"$id\" is not present in \$wgWANObjectCaches." );
}
$params = $mainConfig->get( 'WANObjectCaches' )[$id];
$objectCacheId = $params['cacheId'];
if ( !isset( $mainConfig->get( 'ObjectCaches' )[$objectCacheId] ) ) {
throw new UnexpectedValueException(
"Cache type \"$objectCacheId\" is not present in \$wgObjectCaches." );
}
$params['store'] = $mainConfig->get( 'ObjectCaches' )[$objectCacheId];
return \ObjectCache::newWANCacheFromParams( $params );
},
'MediaHandlerFactory' => function ( MediaWikiServices $services ) : MediaHandlerFactory {
return new MediaHandlerFactory(
$services->getMainConfig()->get( 'MediaHandlers' )
);
},
'MimeAnalyzer' => function ( MediaWikiServices $services ) : MimeAnalyzer {
$logger = LoggerFactory::getInstance( 'Mime' );
$mainConfig = $services->getMainConfig();
$params = [
'typeFile' => $mainConfig->get( 'MimeTypeFile' ),
'infoFile' => $mainConfig->get( 'MimeInfoFile' ),
'xmlTypes' => $mainConfig->get( 'XMLMimeTypes' ),
'guessCallback' =>
function ( $mimeAnalyzer, &$head, &$tail, $file, &$mime ) use ( $logger ) {
// Also test DjVu
$deja = new DjVuImage( $file );
if ( $deja->isValid() ) {
$logger->info( "Detected $file as image/vnd.djvu\n" );
$mime = 'image/vnd.djvu';
return;
}
// Some strings by reference for performance - assuming well-behaved hooks
Hooks::run(
'MimeMagicGuessFromContent',
[ $mimeAnalyzer, &$head, &$tail, $file, &$mime ]
);
},
'extCallback' => function ( $mimeAnalyzer, $ext, &$mime ) {
// Media handling extensions can improve the MIME detected
Hooks::run( 'MimeMagicImproveFromExtension', [ $mimeAnalyzer, $ext, &$mime ] );
},
'initCallback' => function ( $mimeAnalyzer ) {
// Allow media handling extensions adding MIME-types and MIME-info
Hooks::run( 'MimeMagicInit', [ $mimeAnalyzer ] );
},
'logger' => $logger
];
if ( $params['infoFile'] === 'includes/mime.info' ) {
$params['infoFile'] = __DIR__ . "/libs/mime/mime.info";
}
if ( $params['typeFile'] === 'includes/mime.types' ) {
$params['typeFile'] = __DIR__ . "/libs/mime/mime.types";
}
$detectorCmd = $mainConfig->get( 'MimeDetectorCommand' );
if ( $detectorCmd ) {
$factory = $services->getShellCommandFactory();
$params['detectCallback'] = function ( $file ) use ( $detectorCmd, $factory ) {
$result = $factory->create()
// $wgMimeDetectorCommand can contain commands with parameters
->unsafeParams( $detectorCmd )
->params( $file )
->execute();
return $result->getStdout();
};
}
return new MimeAnalyzer( $params );
},
'NamespaceInfo' => function ( MediaWikiServices $services ) : NamespaceInfo {
return new NamespaceInfo( new ServiceOptions( NamespaceInfo::$constructorOptions,
$services->getMainConfig() ) );
},
'NameTableStoreFactory' => function ( MediaWikiServices $services ) : NameTableStoreFactory {
return new NameTableStoreFactory(
$services->getDBLoadBalancerFactory(),
$services->getMainWANObjectCache(),
LoggerFactory::getInstance( 'NameTableSqlStore' )
);
},
'OldRevisionImporter' => function ( MediaWikiServices $services ) : OldRevisionImporter {
return new ImportableOldRevisionImporter(
true,
LoggerFactory::getInstance( 'OldRevisionImporter' ),
$services->getDBLoadBalancer()
);
},
'PageEditStash' => function ( MediaWikiServices $services ) : PageEditStash {
$config = $services->getMainConfig();
return new PageEditStash(
ObjectCache::getLocalClusterInstance(),
$services->getDBLoadBalancer(),
LoggerFactory::getInstance( 'StashEdit' ),
$services->getStatsdDataFactory(),
defined( 'MEDIAWIKI_JOB_RUNNER' ) || $config->get( 'CommandLineMode' )
? PageEditStash::INITIATOR_JOB_OR_CLI
: PageEditStash::INITIATOR_USER
);
},
'Parser' => function ( MediaWikiServices $services ) : Parser {
return $services->getParserFactory()->create();
},
'ParserCache' => function ( MediaWikiServices $services ) : ParserCache {
$config = $services->getMainConfig();
$cache = ObjectCache::getInstance( $config->get( 'ParserCacheType' ) );
wfDebugLog( 'caches', 'parser: ' . get_class( $cache ) );
return new ParserCache(
$cache,
$config->get( 'CacheEpoch' )
);
},
'ParserFactory' => function ( MediaWikiServices $services ) : ParserFactory {
return new ParserFactory(
$services->getMainConfig()->get( 'ParserConf' ),
$services->getMagicWordFactory(),
$services->getContentLanguage(),
wfUrlProtocols(),
$services->getSpecialPageFactory(),
$services->getMainConfig(),
$services->getLinkRendererFactory(),
$services->getNamespaceInfo()
);
},
'PasswordFactory' => function ( MediaWikiServices $services ) : PasswordFactory {
$config = $services->getMainConfig();
return new PasswordFactory(
$config->get( 'PasswordConfig' ),
$config->get( 'PasswordDefault' )
);
},
'PerDbNameStatsdDataFactory' =>
function ( MediaWikiServices $services ) : StatsdDataFactoryInterface {
$config = $services->getMainConfig();
$wiki = $config->get( 'DBname' );
return new PrefixingStatsdDataFactoryProxy(
$services->getStatsdDataFactory(),
$wiki
);
},
'PermissionManager' => function ( MediaWikiServices $services ) : PermissionManager {
$config = $services->getMainConfig();
return new PermissionManager(
$services->getSpecialPageFactory(),
$config->get( 'WhitelistRead' ),
$config->get( 'WhitelistReadRegexp' ),
$config->get( 'EmailConfirmToEdit' ),
$config->get( 'BlockDisablesLogin' ),
$services->getNamespaceInfo()
);
},
'PreferencesFactory' => function ( MediaWikiServices $services ) : PreferencesFactory {
$factory = new DefaultPreferencesFactory(
new ServiceOptions(
DefaultPreferencesFactory::$constructorOptions, $services->getMainConfig() ),
$services->getContentLanguage(),
AuthManager::singleton(),
$services->getLinkRendererFactory()->create(),
$services->getNamespaceInfo()
);
$factory->setLogger( LoggerFactory::getInstance( 'preferences' ) );
return $factory;
},
'ProxyLookup' => function ( MediaWikiServices $services ) : ProxyLookup {
$mainConfig = $services->getMainConfig();
return new ProxyLookup(
$mainConfig->get( 'SquidServers' ),
$mainConfig->get( 'SquidServersNoPurge' )
);
},
'ReadOnlyMode' => function ( MediaWikiServices $services ) : ReadOnlyMode {
return new ReadOnlyMode(
$services->getConfiguredReadOnlyMode(),
$services->getDBLoadBalancer()
);
},
'RepoGroup' => function ( MediaWikiServices $services ) : RepoGroup {
$config = $services->getMainConfig();
return new RepoGroup(
$config->get( 'LocalFileRepo' ),
$config->get( 'ForeignFileRepos' ),
$services->getMainWANObjectCache()
);
},
'ResourceLoader' => function ( MediaWikiServices $services ) : ResourceLoader {
// @todo This should not take a Config object, but it's not so easy to remove because it
// exposes it in a getter, which is actually used.
global $IP;
$config = $services->getMainConfig();
$rl = new ResourceLoader(
$config,
LoggerFactory::getInstance( 'resourceloader' )
);
$rl->addSource( $config->get( 'ResourceLoaderSources' ) );
// Core modules, then extension/skin modules
$rl->register( include "$IP/resources/Resources.php" );
$rl->register( $config->get( 'ResourceModules' ) );
Hooks::run( 'ResourceLoaderRegisterModules', [ &$rl ] );
if ( $config->get( 'EnableJavaScriptTest' ) === true ) {
$rl->registerTestModules();
}
return $rl;
},
'RevisionFactory' => function ( MediaWikiServices $services ) : RevisionFactory {
return $services->getRevisionStore();
},
'RevisionLookup' => function ( MediaWikiServices $services ) : RevisionLookup {
return $services->getRevisionStore();
},
'RevisionRenderer' => function ( MediaWikiServices $services ) : RevisionRenderer {
$renderer = new RevisionRenderer(
$services->getDBLoadBalancer(),
$services->getSlotRoleRegistry()
);
$renderer->setLogger( LoggerFactory::getInstance( 'SaveParse' ) );
return $renderer;
},
'RevisionStore' => function ( MediaWikiServices $services ) : RevisionStore {
return $services->getRevisionStoreFactory()->getRevisionStore();
},
'RevisionStoreFactory' => function ( MediaWikiServices $services ) : RevisionStoreFactory {
$config = $services->getMainConfig();
$store = new RevisionStoreFactory(
$services->getDBLoadBalancerFactory(),
$services->getBlobStoreFactory(),
$services->getNameTableStoreFactory(),
$services->getSlotRoleRegistry(),
$services->getMainWANObjectCache(),
$services->getCommentStore(),
$services->getActorMigration(),
$config->get( 'MultiContentRevisionSchemaMigrationStage' ),
LoggerFactory::getProvider(),
$config->get( 'ContentHandlerUseDB' )
);
return $store;
},
'SearchEngineConfig' => function ( MediaWikiServices $services ) : SearchEngineConfig {
// @todo This should not take a Config object, but it's not so easy to remove because it
// exposes it in a getter, which is actually used.
return new SearchEngineConfig( $services->getMainConfig(),
$services->getContentLanguage() );
},
'SearchEngineFactory' => function ( MediaWikiServices $services ) : SearchEngineFactory {
return new SearchEngineFactory( $services->getSearchEngineConfig() );
},
'ShellCommandFactory' => function ( MediaWikiServices $services ) : CommandFactory {
$config = $services->getMainConfig();
$limits = [
'time' => $config->get( 'MaxShellTime' ),
'walltime' => $config->get( 'MaxShellWallClockTime' ),
'memory' => $config->get( 'MaxShellMemory' ),
'filesize' => $config->get( 'MaxShellFileSize' ),
];
$cgroup = $config->get( 'ShellCgroup' );
$restrictionMethod = $config->get( 'ShellRestrictionMethod' );
$factory = new CommandFactory( $limits, $cgroup, $restrictionMethod );
$factory->setLogger( LoggerFactory::getInstance( 'exec' ) );
$factory->logStderr();
return $factory;
},
'SiteLookup' => function ( MediaWikiServices $services ) : SiteLookup {
// Use SiteStore as the SiteLookup as well. This was originally separated
// to allow for a cacheable read-only interface (using FileBasedSiteLookup),
// but this was never used. SiteStore has caching (see below).
return $services->getSiteStore();
},
'SiteStore' => function ( MediaWikiServices $services ) : SiteStore {
$rawSiteStore = new DBSiteStore( $services->getDBLoadBalancer() );
// TODO: replace wfGetCache with a CacheFactory service.
// TODO: replace wfIsHHVM with a capabilities service.
$cache = wfGetCache( wfIsHHVM() ? CACHE_ACCEL : CACHE_ANYTHING );
return new CachingSiteStore( $rawSiteStore, $cache );
},
'SkinFactory' => function ( MediaWikiServices $services ) : SkinFactory {
$factory = new SkinFactory();
$names = $services->getMainConfig()->get( 'ValidSkinNames' );
foreach ( $names as $name => $skin ) {
$factory->register( $name, $skin, function () use ( $name, $skin ) {
$class = "Skin$skin";
return new $class( $name );
} );
}
// Register a hidden "fallback" skin
$factory->register( 'fallback', 'Fallback', function () {
return new SkinFallback;
} );
// Register a hidden skin for api output
$factory->register( 'apioutput', 'ApiOutput', function () {
return new SkinApi;
} );
return $factory;
},
'SlotRoleRegistry' => function ( MediaWikiServices $services ) : SlotRoleRegistry {
$config = $services->getMainConfig();
$registry = new SlotRoleRegistry(
$services->getNameTableStoreFactory()->getSlotRoles()
);
$registry->defineRole( 'main', function () use ( $config ) {
return new MainSlotRoleHandler(
$config->get( 'NamespaceContentModels' )
);
} );
return $registry;
},
'SpecialPageFactory' => function ( MediaWikiServices $services ) : SpecialPageFactory {
return new SpecialPageFactory(
new ServiceOptions(
SpecialPageFactory::$constructorOptions, $services->getMainConfig() ),
$services->getContentLanguage()
);
},
'StatsdDataFactory' => function ( MediaWikiServices $services ) : IBufferingStatsdDataFactory {
return new BufferingStatsdDataFactory(
rtrim( $services->getMainConfig()->get( 'StatsdMetricPrefix' ), '.' )
);
},
'TitleFormatter' => function ( MediaWikiServices $services ) : TitleFormatter {
return $services->getService( '_MediaWikiTitleCodec' );
},
'TitleParser' => function ( MediaWikiServices $services ) : TitleParser {
return $services->getService( '_MediaWikiTitleCodec' );
},
'UploadRevisionImporter' => function ( MediaWikiServices $services ) : UploadRevisionImporter {
return new ImportableUploadRevisionImporter(
$services->getMainConfig()->get( 'EnableUploads' ),
LoggerFactory::getInstance( 'UploadRevisionImporter' )
);
},
'VirtualRESTServiceClient' =>
function ( MediaWikiServices $services ) : VirtualRESTServiceClient {
$config = $services->getMainConfig()->get( 'VirtualRestConfig' );
$vrsClient = new VirtualRESTServiceClient( new MultiHttpClient( [] ) );
foreach ( $config['paths'] as $prefix => $serviceConfig ) {
$class = $serviceConfig['class'];
// Merge in the global defaults
$constructArg = $serviceConfig['options'] ?? [];
$constructArg += $config['global'];
// Make the VRS service available at the mount point
$vrsClient->mount( $prefix, [ 'class' => $class, 'config' => $constructArg ] );
}
return $vrsClient;
},
'WatchedItemQueryService' =>
function ( MediaWikiServices $services ) : WatchedItemQueryService {
return new WatchedItemQueryService(
$services->getDBLoadBalancer(),
$services->getCommentStore(),
$services->getActorMigration(),
$services->getWatchedItemStore()
);
},
'WatchedItemStore' => function ( MediaWikiServices $services ) : WatchedItemStore {
$store = new WatchedItemStore(
$services->getDBLoadBalancerFactory(),
JobQueueGroup::singleton(),
$services->getMainObjectStash(),
new HashBagOStuff( [ 'maxKeys' => 100 ] ),
$services->getReadOnlyMode(),
$services->getMainConfig()->get( 'UpdateRowsPerQuery' )
);
$store->setStatsdDataFactory( $services->getStatsdDataFactory() );
if ( $services->getMainConfig()->get( 'ReadOnlyWatchedItemStore' ) ) {
$store = new NoWriteWatchedItemStore( $store );
}
return $store;
},
'WikiRevisionOldRevisionImporterNoUpdates' =>
function ( MediaWikiServices $services ) : ImportableOldRevisionImporter {
return new ImportableOldRevisionImporter(
false,
LoggerFactory::getInstance( 'OldRevisionImporter' ),
$services->getDBLoadBalancer()
);
},
'_MediaWikiTitleCodec' => function ( MediaWikiServices $services ) : MediaWikiTitleCodec {
return new MediaWikiTitleCodec(
$services->getContentLanguage(),
$services->getGenderCache(),
$services->getMainConfig()->get( 'LocalInterwikis' ),
$services->getInterwikiLookup(),
$services->getNamespaceInfo()
);
},
'_SqlBlobStore' => function ( MediaWikiServices $services ) : SqlBlobStore {
return $services->getBlobStoreFactory()->newSqlBlobStore();
},
///////////////////////////////////////////////////////////////////////////
// NOTE: When adding a service here, don't forget to add a getter function
// in the MediaWikiServices class. The convenience getter should just call
// $this->getService( 'FooBarService' ).
///////////////////////////////////////////////////////////////////////////
];
diff --git a/includes/cache/LinkCache.php b/includes/cache/LinkCache.php
index 33feee29abd..1bcf948d2df 100644
--- a/includes/cache/LinkCache.php
+++ b/includes/cache/LinkCache.php
@@ -1,344 +1,356 @@
getNamespaceInfo();
+ }
$this->goodLinks = new MapCacheLRU( self::MAX_SIZE );
$this->badLinks = new MapCacheLRU( self::MAX_SIZE );
$this->wanCache = $cache;
$this->titleFormatter = $titleFormatter;
+ $this->nsInfo = $nsInfo;
}
/**
* Get an instance of this class.
*
* @return LinkCache
* @deprecated since 1.28, use MediaWikiServices instead
*/
public static function singleton() {
return MediaWikiServices::getInstance()->getLinkCache();
}
/**
* General accessor to get/set whether the master DB should be used
*
* This used to also set the FOR UPDATE option (locking the rows read
* in order to avoid link table inconsistency), which was later removed
* for performance on wikis with a high edit rate.
*
* @param bool|null $update
* @return bool
*/
public function forUpdate( $update = null ) {
return wfSetVar( $this->mForUpdate, $update );
}
/**
* @param string $title Prefixed DB key
* @return int Page ID or zero
*/
public function getGoodLinkID( $title ) {
$info = $this->goodLinks->get( $title );
if ( !$info ) {
return 0;
}
return $info['id'];
}
/**
* Get a field of a title object from cache.
* If this link is not a cached good title, it will return NULL.
* @param LinkTarget $target
* @param string $field ('length','redirect','revision','model')
* @return string|int|null
*/
public function getGoodLinkFieldObj( LinkTarget $target, $field ) {
$dbkey = $this->titleFormatter->getPrefixedDBkey( $target );
$info = $this->goodLinks->get( $dbkey );
if ( !$info ) {
return null;
}
return $info[$field];
}
/**
* @param string $title Prefixed DB key
* @return bool
*/
public function isBadLink( $title ) {
// Use get() to ensure it records as used for LRU.
return $this->badLinks->has( $title );
}
/**
* Add a link for the title to the link cache
*
* @param int $id Page's ID
* @param LinkTarget $target
* @param int $len Text's length
* @param int|null $redir Whether the page is a redirect
* @param int $revision Latest revision's ID
* @param string|null $model Latest revision's content model ID
* @param string|null $lang Language code of the page, if not the content language
*/
public function addGoodLinkObj( $id, LinkTarget $target, $len = -1, $redir = null,
$revision = 0, $model = null, $lang = null
) {
$dbkey = $this->titleFormatter->getPrefixedDBkey( $target );
$this->goodLinks->set( $dbkey, [
'id' => (int)$id,
'length' => (int)$len,
'redirect' => (int)$redir,
'revision' => (int)$revision,
'model' => $model ? (string)$model : null,
'lang' => $lang ? (string)$lang : null,
'restrictions' => null
] );
}
/**
* Same as above with better interface.
* @since 1.19
* @param LinkTarget $target
* @param stdClass $row Object which has the fields page_id, page_is_redirect,
* page_latest and page_content_model
*/
public function addGoodLinkObjFromRow( LinkTarget $target, $row ) {
$dbkey = $this->titleFormatter->getPrefixedDBkey( $target );
$this->goodLinks->set( $dbkey, [
'id' => intval( $row->page_id ),
'length' => intval( $row->page_len ),
'redirect' => intval( $row->page_is_redirect ),
'revision' => intval( $row->page_latest ),
'model' => !empty( $row->page_content_model )
? strval( $row->page_content_model )
: null,
'lang' => !empty( $row->page_lang )
? strval( $row->page_lang )
: null,
'restrictions' => !empty( $row->page_restrictions )
? strval( $row->page_restrictions )
: null
] );
}
/**
* @param LinkTarget $target
*/
public function addBadLinkObj( LinkTarget $target ) {
$dbkey = $this->titleFormatter->getPrefixedDBkey( $target );
if ( !$this->isBadLink( $dbkey ) ) {
$this->badLinks->set( $dbkey, 1 );
}
}
/**
* @param string $title Prefixed DB key
*/
public function clearBadLink( $title ) {
$this->badLinks->clear( $title );
}
/**
* @param LinkTarget $target
*/
public function clearLink( LinkTarget $target ) {
$dbkey = $this->titleFormatter->getPrefixedDBkey( $target );
$this->badLinks->clear( $dbkey );
$this->goodLinks->clear( $dbkey );
}
/**
* Fields that LinkCache needs to select
*
* @since 1.28
* @return array
*/
public static function getSelectFields() {
global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
$fields = [
'page_id',
'page_len',
'page_is_redirect',
'page_latest',
'page_restrictions'
];
if ( $wgContentHandlerUseDB ) {
$fields[] = 'page_content_model';
}
if ( $wgPageLanguageUseDB ) {
$fields[] = 'page_lang';
}
return $fields;
}
/**
* Add a title to the link cache, return the page_id or zero if non-existent
*
* @param LinkTarget $nt LinkTarget object to add
* @return int Page ID or zero
*/
public function addLinkObj( LinkTarget $nt ) {
$key = $this->titleFormatter->getPrefixedDBkey( $nt );
if ( $this->isBadLink( $key ) || $nt->isExternal() || $nt->getNamespace() < 0 ) {
return 0;
}
$id = $this->getGoodLinkID( $key );
if ( $id != 0 ) {
return $id;
}
if ( $key === '' ) {
return 0;
}
// Cache template/file pages as they are less often viewed but heavily used
if ( $this->mForUpdate ) {
$row = $this->fetchPageRow( wfGetDB( DB_MASTER ), $nt );
} elseif ( $this->isCacheable( $nt ) ) {
// These pages are often transcluded heavily, so cache them
$cache = $this->wanCache;
$row = $cache->getWithSetCallback(
$cache->makeKey( 'page', $nt->getNamespace(), sha1( $nt->getDBkey() ) ),
$cache::TTL_DAY,
function ( $curValue, &$ttl, array &$setOpts ) use ( $cache, $nt ) {
$dbr = wfGetDB( DB_REPLICA );
$setOpts += Database::getCacheSetOptions( $dbr );
$row = $this->fetchPageRow( $dbr, $nt );
$mtime = $row ? wfTimestamp( TS_UNIX, $row->page_touched ) : false;
$ttl = $cache->adaptiveTTL( $mtime, $ttl );
return $row;
}
);
} else {
$row = $this->fetchPageRow( wfGetDB( DB_REPLICA ), $nt );
}
if ( $row ) {
$this->addGoodLinkObjFromRow( $nt, $row );
$id = intval( $row->page_id );
} else {
$this->addBadLinkObj( $nt );
$id = 0;
}
return $id;
}
/**
* @param WANObjectCache $cache
* @param LinkTarget $t
* @return string[]
* @since 1.28
*/
public function getMutableCacheKeys( WANObjectCache $cache, LinkTarget $t ) {
if ( $this->isCacheable( $t ) ) {
return [ $cache->makeKey( 'page', $t->getNamespace(), sha1( $t->getDBkey() ) ) ];
}
return [];
}
private function isCacheable( LinkTarget $title ) {
$ns = $title->getNamespace();
if ( in_array( $ns, [ NS_TEMPLATE, NS_FILE, NS_CATEGORY ] ) ) {
return true;
}
// Focus on transcluded pages more than the main content
- if ( MWNamespace::isContent( $ns ) ) {
+ if ( $this->nsInfo->isContent( $ns ) ) {
return false;
}
// Non-talk extension namespaces (e.g. NS_MODULE)
- return ( $ns >= 100 && MWNamespace::isSubject( $ns ) );
+ return ( $ns >= 100 && $this->nsInfo->isSubject( $ns ) );
}
private function fetchPageRow( IDatabase $db, LinkTarget $nt ) {
$fields = self::getSelectFields();
if ( $this->isCacheable( $nt ) ) {
$fields[] = 'page_touched';
}
return $db->selectRow(
'page',
$fields,
[ 'page_namespace' => $nt->getNamespace(), 'page_title' => $nt->getDBkey() ],
__METHOD__
);
}
/**
* Purge the link cache for a title
*
* @param LinkTarget $title
* @since 1.28
*/
public function invalidateTitle( LinkTarget $title ) {
if ( $this->isCacheable( $title ) ) {
$cache = $this->wanCache;
$cache->delete(
$cache->makeKey( 'page', $title->getNamespace(), sha1( $title->getDBkey() ) )
);
}
}
/**
* Clears cache
*/
public function clear() {
$this->goodLinks->clear();
$this->badLinks->clear();
}
}
diff --git a/tests/phpunit/includes/linker/LinkRendererTest.php b/tests/phpunit/includes/linker/LinkRendererTest.php
index 91ee2765507..e90577c907b 100644
--- a/tests/phpunit/includes/linker/LinkRendererTest.php
+++ b/tests/phpunit/includes/linker/LinkRendererTest.php
@@ -1,197 +1,198 @@
setMwGlobals( [
'wgArticlePath' => '/wiki/$1',
'wgServer' => '//example.org',
'wgCanonicalServer' => 'http://example.org',
'wgScriptPath' => '/w',
'wgScript' => '/w/index.php',
] );
$this->factory = MediaWikiServices::getInstance()->getLinkRendererFactory();
}
public function testMergeAttribs() {
$target = new TitleValue( NS_SPECIAL, 'Blankpage' );
$linkRenderer = $this->factory->create();
$link = $linkRenderer->makeBrokenLink( $target, null, [
// Appended to class
'class' => 'foobar',
// Suppresses href attribute
'href' => false,
// Extra attribute
'bar' => 'baz'
] );
$this->assertEquals(
''
. 'Special:BlankPage',
$link
);
}
public function testMakeKnownLink() {
$target = new TitleValue( NS_MAIN, 'Foobar' );
$linkRenderer = $this->factory->create();
// Query added
$this->assertEquals(
'Foobar',
$linkRenderer->makeKnownLink( $target, null, [], [ 'foo' => 'bar' ] )
);
$linkRenderer->setForceArticlePath( true );
$this->assertEquals(
'Foobar',
$linkRenderer->makeKnownLink( $target, null, [], [ 'foo' => 'bar' ] )
);
// expand = HTTPS
$linkRenderer->setForceArticlePath( false );
$linkRenderer->setExpandURLs( PROTO_HTTPS );
$this->assertEquals(
'Foobar',
$linkRenderer->makeKnownLink( $target )
);
}
public function testMakeBrokenLink() {
$target = new TitleValue( NS_MAIN, 'Foobar' );
$special = new TitleValue( NS_SPECIAL, 'Foobar' );
$linkRenderer = $this->factory->create();
// action=edit&redlink=1 added
$this->assertEquals(
'Foobar',
$linkRenderer->makeBrokenLink( $target )
);
// action=edit&redlink=1 not added due to action query parameter
$this->assertEquals(
'Foobar',
$linkRenderer->makeBrokenLink( $target, null, [], [ 'action' => 'foobar' ] )
);
// action=edit&redlink=1 not added due to NS_SPECIAL
$this->assertEquals(
'Special:Foobar',
$linkRenderer->makeBrokenLink( $special )
);
// fragment stripped
$this->assertEquals(
'Foobar',
$linkRenderer->makeBrokenLink( $target->createFragmentTarget( 'foobar' ) )
);
}
public function testMakeLink() {
$linkRenderer = $this->factory->create();
$foobar = new TitleValue( NS_SPECIAL, 'Foobar' );
$blankpage = new TitleValue( NS_SPECIAL, 'Blankpage' );
$this->assertEquals(
'foo',
$linkRenderer->makeLink( $foobar, 'foo' )
);
$this->assertEquals(
'blank',
$linkRenderer->makeLink( $blankpage, 'blank' )
);
$this->assertEquals(
'<script>evil()</script>',
$linkRenderer->makeLink( $foobar, '' )
);
$this->assertEquals(
'',
$linkRenderer->makeLink( $foobar, new HtmlArmor( '' ) )
);
$this->assertEquals(
'fragment',
$linkRenderer->makeLink( Title::newFromText( '#fragment' ) )
);
}
public function testGetLinkClasses() {
$wanCache = ObjectCache::getMainWANInstance();
$titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter();
- $linkCache = new LinkCache( $titleFormatter, $wanCache );
+ $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+ $linkCache = new LinkCache( $titleFormatter, $wanCache, $nsInfo );
$foobarTitle = new TitleValue( NS_MAIN, 'FooBar' );
$redirectTitle = new TitleValue( NS_MAIN, 'Redirect' );
$userTitle = new TitleValue( NS_USER, 'Someuser' );
$linkCache->addGoodLinkObj(
1, // id
$foobarTitle,
10, // len
0 // redir
);
$linkCache->addGoodLinkObj(
2, // id
$redirectTitle,
10, // len
1 // redir
);
$linkCache->addGoodLinkObj(
3, // id
$userTitle,
10, // len
0 // redir
);
$linkRenderer = new LinkRenderer( $titleFormatter, $linkCache );
$linkRenderer->setStubThreshold( 0 );
$this->assertEquals(
'',
$linkRenderer->getLinkClasses( $foobarTitle )
);
$linkRenderer->setStubThreshold( 20 );
$this->assertEquals(
'stub',
$linkRenderer->getLinkClasses( $foobarTitle )
);
$linkRenderer->setStubThreshold( 0 );
$this->assertEquals(
'mw-redirect',
$linkRenderer->getLinkClasses( $redirectTitle )
);
$linkRenderer->setStubThreshold( 20 );
$this->assertEquals(
'',
$linkRenderer->getLinkClasses( $userTitle )
);
}
function tearDown() {
Title::clearCaches();
parent::tearDown();
}
}