From 337311662d344c90590ca5cee34b8a87da933430 Mon Sep 17 00:00:00 2001 From: daniel Date: Wed, 23 Jan 2019 11:17:21 +0100 Subject: [PATCH] Fix Title::getFragmentForURL for bad interwiki prefix. Calling Title::getLinkURL or any other method that relies on getFragmentForURL on a title with an unknown interwiki prefix was triggering a fatal error. With this patch, that situation is handled more gracefully. Bug: T204800 Change-Id: I665cd5e983a80c15c68c89541d9c856082c460bb --- includes/Title.php | 12 ++- tests/phpunit/includes/TitleMethodsTest.php | 108 ++++++++++++++++++++ 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/includes/Title.php b/includes/Title.php index 34966686b9..1bb54df6f9 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -1609,11 +1609,15 @@ class Title implements LinkTarget, IDBAccessObject { public function getFragmentForURL() { if ( !$this->hasFragment() ) { return ''; - } elseif ( $this->isExternal() - && !self::getInterwikiLookup()->fetch( $this->mInterwiki )->isLocal() - ) { - return '#' . Sanitizer::escapeIdForExternalInterwiki( $this->mFragment ); + } elseif ( $this->isExternal() ) { + // Note: If the interwiki is unknown, it's treated as a namespace on the local wiki, + // so we treat it like a local interwiki. + $interwiki = self::getInterwikiLookup()->fetch( $this->mInterwiki ); + if ( $interwiki && !$interwiki->isLocal() ) { + return '#' . Sanitizer::escapeIdForExternalInterwiki( $this->mFragment ); + } } + return '#' . Sanitizer::escapeIdForLink( $this->mFragment ); } diff --git a/tests/phpunit/includes/TitleMethodsTest.php b/tests/phpunit/includes/TitleMethodsTest.php index 44d440f3ec..25dc9b3e30 100644 --- a/tests/phpunit/includes/TitleMethodsTest.php +++ b/tests/phpunit/includes/TitleMethodsTest.php @@ -1,5 +1,6 @@ assertEquals( 0, $linkCache->getGoodLinkID( 'Foo' ), 'link cache should be empty' ); } + public function provideGetLinkURL() { + yield 'Simple' => [ + '/wiki/Goats', + NS_MAIN, + 'Goats' + ]; + + yield 'Fragment' => [ + '/wiki/Goats#Goatificatiön', + NS_MAIN, + 'Goats', + 'Goatificatiön' + ]; + + yield 'Unknown interwiki with fragment' => [ + 'https://xx.wiki.test/wiki/xyzzy:Goats#Goatificatiön', + NS_MAIN, + 'Goats', + 'Goatificatiön', + 'xyzzy' + ]; + + yield 'Interwiki with fragment' => [ + 'https://acme.test/Goats#Goatificati.C3.B6n', + NS_MAIN, + 'Goats', + 'Goatificatiön', + 'acme' + ]; + + yield 'Interwiki with query' => [ + 'https://acme.test/Goats?a=1&b=blank+blank#Goatificati.C3.B6n', + NS_MAIN, + 'Goats', + 'Goatificatiön', + 'acme', + [ + 'a' => 1, + 'b' => 'blank blank' + ] + ]; + + yield 'Local interwiki with fragment' => [ + 'https://yy.wiki.test/wiki/Goats#Goatificatiön', + NS_MAIN, + 'Goats', + 'Goatificatiön', + 'yy' + ]; + } + + /** + * @dataProvider provideGetLinkURL + * + * @covers Title::getLinkURL + * @covers Title::getFullURL + * @covers Title::getLocalURL + * @covers Title::getFragmentForURL + */ + public function testGetLinkURL( + $expected, + $ns, + $title, + $fragment = '', + $interwiki = '', + $query = '', + $query2 = false, + $proto = false + ) { + $this->setMwGlobals( [ + 'wgServer' => 'https://xx.wiki.test', + 'wgArticlePath' => '/wiki/$1', + 'wgExternalInterwikiFragmentMode' => 'legacy', + 'wgFragmentMode' => [ 'html5', 'legacy' ] + ] ); + + $interwikiLookup = $this->getMock( InterwikiLookup::class ); + + $interwikiLookup->method( 'fetch' ) + ->willReturnCallback( function ( $interwiki ) { + switch ( $interwiki ) { + case '': + return null; + case 'acme': + return new Interwiki( + 'acme', + 'https://acme.test/$1' + ); + case 'yy': + return new Interwiki( + 'yy', + 'https://yy.wiki.test/wiki/$1', + '/w/api.php', + 'yywiki', + true + ); + default: + return false; + } + } ); + + $this->setService( 'InterwikiLookup', $interwikiLookup ); + + $title = Title::makeTitle( $ns, $title, $fragment, $interwiki ); + $this->assertSame( $expected, $title->getLinkURL( $query, $query2, $proto ) ); + } + function tearDown() { Title::clearCaches(); parent::tearDown(); -- 2.20.1