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(); } }