configureLanguages(); $this->getServiceContainer()->getMessageCache()->enable(); } /** * Helper function -- setup site language for testing */ protected function configureLanguages() { // for the test, we need the content language to be anything but English, // let's choose e.g. German (de) $this->setUserLang( 'de' ); $this->setContentLang( 'de' ); } public function addDBDataOnce() { $this->configureLanguages(); // Set up messages and fallbacks ab -> ru -> de $this->makePage( 'FallbackLanguageTest-Full', 'ab' ); $this->makePage( 'FallbackLanguageTest-Full', 'ru' ); $this->makePage( 'FallbackLanguageTest-Full', 'de' ); // Fallbacks where ab does not exist $this->makePage( 'FallbackLanguageTest-Partial', 'ru' ); $this->makePage( 'FallbackLanguageTest-Partial', 'de' ); // Fallback to the content language $this->makePage( 'FallbackLanguageTest-ContLang', 'de' ); // Full key tests -- always want russian $this->makePage( 'MessageCacheTest-FullKeyTest', 'ab' ); $this->makePage( 'MessageCacheTest-FullKeyTest', 'ru' ); // In content language -- get base if no derivative $this->makePage( 'FallbackLanguageTest-NoDervContLang', 'de', 'de/none' ); } /** * Helper function for addDBData -- adds a simple page to the database * * @param string $title Title of page to be created * @param string $lang Language and content of the created page * @param string|null $content Content of the created page, or null for a generic string * * @return RevisionRecord */ private function makePage( $title, $lang, $content = null ) { $content ??= $lang; if ( $lang !== $this->getServiceContainer()->getContentLanguageCode()->toString() ) { $title = "$title/$lang"; } $title = Title::makeTitle( NS_MEDIAWIKI, $title ); $wikiPage = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title ); $content = ContentHandler::makeContent( $content, $title ); $summary = CommentStoreComment::newUnsavedComment( "$lang translation test case" ); $newRevision = $wikiPage->newPageUpdater( $this->getTestSysop()->getUser() ) ->setContent( SlotRecord::MAIN, $content ) ->saveRevision( $summary ); $this->assertNotNull( $newRevision, 'Create page ' . $title->getPrefixedDBkey() ); // Run the updates if no outer transaction is active DeferredUpdates::tryOpportunisticExecute(); return $newRevision; } /** * Test message fallbacks, T3495 * * @dataProvider provideMessagesForFallback */ public function testMessageFallbacks( $message, $langCode, $expectedContent ) { $lang = $this->getServiceContainer()->getLanguageFactory()->getLanguage( $langCode ); $result = $this->getServiceContainer()->getMessageCache()->get( $message, true, $lang ); $this->assertEquals( $expectedContent, $result, "Message fallback failed." ); } public static function provideMessagesForFallback() { return [ [ 'FallbackLanguageTest-Full', 'ab', 'ab' ], [ 'FallbackLanguageTest-Partial', 'ab', 'ru' ], [ 'FallbackLanguageTest-ContLang', 'ab', 'de' ], [ 'FallbackLanguageTest-None', 'ab', false ], // T48579 [ 'FallbackLanguageTest-NoDervContLang', 'de', 'de/none' ], // UI language different from content language should only use de/none as last option [ 'FallbackLanguageTest-NoDervContLang', 'fit', 'de/none' ], ]; } public function testReplaceMsg() { $messageCache = $this->getServiceContainer()->getMessageCache(); $message = 'go'; $uckey = $this->getServiceContainer()->getContentLanguage()->ucfirst( $message ); $oldText = $messageCache->get( $message ); // "Ausführen" $dbw = $this->getDb(); $dbw->startAtomic( __METHOD__ ); // simulate request and block deferred updates $messageCache->replace( $uckey, 'Allez!' ); $this->assertEquals( 'Allez!', $messageCache->getMsgFromNamespace( $uckey, 'de' ), 'Updates are reflected in-process immediately' ); $this->assertEquals( 'Allez!', $messageCache->get( $message ), 'Updates are reflected in-process immediately' ); $this->makePage( 'Go', 'de', 'Race!' ); $dbw->endAtomic( __METHOD__ ); $this->runDeferredUpdates(); $this->assertSame( 0, DeferredUpdates::pendingUpdatesCount(), 'Post-commit deferred update triggers a run of all updates' ); $this->assertEquals( 'Race!', $messageCache->get( $message ), 'Correct final contents' ); $this->makePage( 'Go', 'de', $oldText ); $messageCache->replace( $uckey, $oldText ); // deferred update runs immediately $this->assertEquals( $oldText, $messageCache->get( $message ), 'Content restored' ); } public function testReplaceCache() { $this->overrideConfigValues( [ MainConfigNames::MainCacheType => CACHE_HASH, ] ); $messageCache = $this->getServiceContainer()->getMessageCache(); $messageCache->enable(); // Populate one key $this->makePage( 'Key1', 'de', 'Value1' ); $this->assertSame( 0, DeferredUpdates::pendingUpdatesCount(), 'Post-commit deferred update triggers a run of all updates' ); $this->assertEquals( 'Value1', $messageCache->get( 'Key1' ), 'Key1 was successfully edited' ); // Screw up the database so MessageCache::loadFromDB() will // produce the wrong result for reloading Key1 $this->getDb()->newDeleteQueryBuilder() ->deleteFrom( 'page' ) ->where( [ 'page_namespace' => NS_MEDIAWIKI, 'page_title' => 'Key1' ] ) ->caller( __METHOD__ ) ->execute(); // Populate the second key $this->makePage( 'Key2', 'de', 'Value2' ); $this->assertSame( 0, DeferredUpdates::pendingUpdatesCount(), 'Post-commit deferred update triggers a run of all updates' ); $this->assertEquals( 'Value2', $messageCache->get( 'Key2' ), 'Key2 was successfully edited' ); // Now test that the second edit didn't reload Key1 $this->assertEquals( 'Value1', $messageCache->get( 'Key1' ), 'Key1 wasn\'t reloaded by edit of Key2' ); } /** * @dataProvider provideNormalizeKey */ public function testNormalizeKey( $key, $expected ) { $actual = MessageCache::normalizeKey( $key ); $this->assertEquals( $expected, $actual ); } public static function provideNormalizeKey() { return [ [ 'Foo', 'foo' ], [ 'foo', 'foo' ], [ 'fOo', 'fOo' ], [ 'FOO', 'fOO' ], [ 'Foo bar', 'foo_bar' ], [ 'Ćab', 'ćab' ], [ 'Ćab_e 3', 'ćab_e_3' ], [ 'ĆAB', 'ćAB' ], [ 'ćab', 'ćab' ], [ 'ćaB', 'ćaB' ], ]; } public function testNoDBAccessContentLanguage() { $languageCode = $this->getServiceContainer()->getMainConfig()->get( MainConfigNames::LanguageCode ); $dbr = $this->getDb(); $messageCache = $this->getServiceContainer()->getMessageCache(); $messageCache->getMsgFromNamespace( 'allpages', $languageCode ); $this->assertSame( 0, $dbr->trxLevel() ); $dbr->setFlag( DBO_TRX, $dbr::REMEMBER_PRIOR ); // make queries trigger TRX $messageCache->getMsgFromNamespace( 'go', $languageCode ); $dbr->restoreFlags(); $this->assertSame( 0, $dbr->trxLevel(), "No DB read queries (content language)" ); } public function testNoDBAccessNonContentLanguage() { $dbr = $this->getDb(); $messageCache = $this->getServiceContainer()->getMessageCache(); $messageCache->getMsgFromNamespace( 'allpages/nl', 'nl' ); $this->assertSame( 0, $dbr->trxLevel() ); $dbr->setFlag( DBO_TRX, $dbr::REMEMBER_PRIOR ); // make queries trigger TRX $messageCache->getMsgFromNamespace( 'go/nl', 'nl' ); $dbr->restoreFlags(); $this->assertSame( 0, $dbr->trxLevel(), "No DB read queries (non-content language)" ); } /** * Regression test for T218918 */ public function testLoadFromDB_fetchLatestRevision() { // Create three revisions of the same message page. // Must be an existing message key. $key = 'Log'; $this->makePage( $key, 'de', 'Test eins' ); $this->makePage( $key, 'de', 'Test zwei' ); $r3 = $this->makePage( $key, 'de', 'Test drei' ); // Create an out-of-sequence revision by importing a // revision with an old timestamp. Hacky. $importRevision = new WikiRevision(); $title = Title::newFromLinkTarget( $r3->getPageAsLinkTarget() ); $importRevision->setTitle( $title ); $importRevision->setComment( 'Imported edit' ); $importRevision->setTimestamp( '19991122001122' ); $content = ContentHandler::makeContent( 'IMPORTED OLD TEST', $title ); $importRevision->setContent( SlotRecord::MAIN, $content ); $importRevision->setUsername( 'ext>Alan Smithee' ); $importer = $this->getServiceContainer()->getWikiRevisionOldRevisionImporterNoUpdates(); $importer->import( $importRevision ); // Now, load the message from the wiki page $messageCache = $this->getServiceContainer()->getMessageCache(); $messageCache->enable(); $messageCache = TestingAccessWrapper::newFromObject( $messageCache ); $cache = $messageCache->loadFromDB( 'de' ); $this->assertArrayHasKey( $key, $cache ); // Text in the cache has an extra space in front! $this->assertSame( ' ' . 'Test drei', $cache[$key] ); } /** * @dataProvider provideIsMainCacheable * @param string|null $code The language code * @param string $message The message key * @param bool $expected */ public function testIsMainCacheable( $code, $message, $expected ) { $messageCache = TestingAccessWrapper::newFromObject( $this->getServiceContainer()->getMessageCache() ); $this->assertSame( $expected, $messageCache->isMainCacheable( $message, $code ) ); } public static function provideIsMainCacheable() { $cases = [ [ 'allpages', true ], [ 'Allpages', true ], [ 'Allpages/bat', true ], [ 'Conversiontable/zh-tw', true ], [ 'My_special_message', false ], ]; foreach ( [ null, 'en', 'fr' ] as $code ) { foreach ( $cases as $case ) { yield array_merge( [ $code ], $case ); } } } /** * @dataProvider provideLocalOverride * @param string $messageKey */ public function testLocalOverride( $messageKey ) { $messageCache = $this->getServiceContainer()->getMessageCache(); $languageFactory = $this->getServiceContainer()->getLanguageFactory(); $languageZh = $languageFactory->getLanguage( 'zh' ); $languageZh_tw = $languageFactory->getLanguage( 'zh-tw' ); $languageZh_hk = $languageFactory->getLanguage( 'zh-hk' ); $languageZh_mo = $languageFactory->getLanguage( 'zh-mo' ); $oldMessageZh = $messageCache->get( $messageKey, true, $languageZh ); $oldMessageZh_tw = $messageCache->get( $messageKey, true, $languageZh_tw ); $localOverrideHK = $messageKey . '_zh-hk'; $this->makePage( ucfirst( $messageKey ), 'zh-hk', $localOverrideHK ); $this->assertEquals( $oldMessageZh, $messageCache->get( $messageKey, true, $languageZh ), 'Local override overlapped (main code)' ); $this->assertEquals( $oldMessageZh_tw, $messageCache->get( $messageKey, true, $languageZh_tw ), 'Local override overlapped' ); $this->assertEquals( $localOverrideHK, $messageCache->get( $messageKey, true, $languageZh_hk ), 'Local override failed (self)' ); $this->assertEquals( $localOverrideHK, $messageCache->get( $messageKey, true, $languageZh_mo ), 'Local override failed (fallback)' ); } public static function provideLocalOverride() { return [ // Preloaded with preloadedMessages [ 'nstab-main' ], // Not preloaded [ 'nstab-help' ], ]; } public function testNestedMessageParse() { $msgOuter = ( new RawMessage( '[[Link|{{#language:}}]]' ) ) ->inLanguage( 'outer' ) ->page( new PageIdentityValue( 1, NS_MAIN, 'Link', PageIdentityValue::LOCAL ) ); // T372891: Allow nested message parsing // Any hook from Linker or LinkRenderer will do for this test, but this one is the simplest $this->setTemporaryHook( 'SelfLinkBegin', static function ( $nt, &$html, &$trail, &$prefix, &$ret ) { $msgInner = ( new RawMessage( '{{#language:}}' ) )->inLanguage( 'inner' ); $html .= $msgInner->escaped(); } ); $this->assertEquals( 'outerinner', $msgOuter->parse() ); } /** @dataProvider provideXssLanguage */ public function testXssLanguage( array $config, bool $expectXssMessage ): void { $this->overrideConfigValues( $config + [ MainConfigNames::UseXssLanguage => false, MainConfigNames::RawHtmlMessages => [], ] ); $xss = $this->getServiceContainer()->getLanguageFactory()->getLanguage( 'x-xss' ); $message = $this->getServiceContainer()->getMessageCache() ->get( 'key', true, $xss ); if ( $expectXssMessage ) { $this->assertSame( "\">assertFalse( $message ); } } public static function provideXssLanguage(): iterable { yield 'default' => [ 'config' => [], 'expectXssMessage' => false, ]; yield 'enabled' => [ 'config' => [ MainConfigNames::UseXssLanguage => true, ], 'expectXssMessage' => true, ]; yield 'enabled but message marked as raw' => [ 'config' => [ MainConfigNames::UseXssLanguage => true, MainConfigNames::RawHtmlMessages => [ 'key' ], ], 'expectXssMessage' => false, ]; } }