diff --git a/includes/parser/Sanitizer.php b/includes/parser/Sanitizer.php index fbcd316c1ba..a54bf5959b3 100644 --- a/includes/parser/Sanitizer.php +++ b/includes/parser/Sanitizer.php @@ -1291,7 +1291,7 @@ class Sanitizer { $mode = $wgFragmentMode[self::ID_PRIMARY]; - $id = self::escapeIdInternal( $id, $mode ); + $id = self::escapeIdInternalUrl( $id, $mode ); return $id; } @@ -1308,11 +1308,28 @@ class Sanitizer { public static function escapeIdForExternalInterwiki( $id ) { global $wgExternalInterwikiFragmentMode; - $id = self::escapeIdInternal( $id, $wgExternalInterwikiFragmentMode ); + $id = self::escapeIdInternalUrl( $id, $wgExternalInterwikiFragmentMode ); return $id; } + /** + * Do percent encoding of percent signs for href (but not id) attributes + * + * @since 1.35 + * @see https://phabricator.wikimedia.org/T238385 + * @param string $id String to escape + * @param string $mode One of modes from $wgFragmentMode + * @return string + */ + private static function escapeIdInternalUrl( $id, $mode ) { + $id = self::escapeIdInternal( $id, $mode ); + if ( $mode === 'html5' ) { + $id = preg_replace( '/%([a-fA-F0-9]{2})/', '%25$1', $id ); + } + return $id; + } + /** * Helper for escapeIdFor*() functions. Performs most of the actual escaping. * diff --git a/tests/parser/parserTests.txt b/tests/parser/parserTests.txt index 092cd3a75cb..5fb666f0ee1 100644 --- a/tests/parser/parserTests.txt +++ b/tests/parser/parserTests.txt @@ -22858,7 +22858,7 @@ wgFragmentMode=[ 'html5', 'legacy' ] __NOEDITSECTION__ !! html/php

_ +:.3A%3A _ &&]] x

-

+:.3A%3A_&&]]_x +

+:.3A%253A_&&]]_x

!! html/parsoid

_ +:.3A%3A _ &&]] x

diff --git a/tests/phpunit/includes/parser/SanitizerTest.php b/tests/phpunit/includes/parser/SanitizerTest.php index cff3d6d4347..176d02d7498 100644 --- a/tests/phpunit/includes/parser/SanitizerTest.php +++ b/tests/phpunit/includes/parser/SanitizerTest.php @@ -173,6 +173,7 @@ class SanitizerTest extends MediaWikiTestCase { * @covers Sanitizer::escapeIdForLink() * @covers Sanitizer::escapeIdForExternalInterwiki() * @covers Sanitizer::escapeIdInternal() + * @covers Sanitizer::escapeIdInternalUrl() * * @param string $stuff * @param string[] $config @@ -193,10 +194,11 @@ class SanitizerTest extends MediaWikiTestCase { public function provideEscapeIdForStuff() { // Test inputs and outputs - $text = 'foo тест_#%!\'()[]:<>&&&amp;'; + $text = 'foo тест_#%!\'()[]:<>&&&amp;%F0'; $legacyEncoded = 'foo_.D1.82.D0.B5.D1.81.D1.82_.23.25.21.27.28.29.5B.5D:.3C.3E' . - '.26.26amp.3B.26amp.3Bamp.3B'; - $html5Encoded = 'foo_тест_#%!\'()[]:<>&&&amp;'; + '.26.26amp.3B.26amp.3Bamp.3B.25F0'; + $html5EncodedId = 'foo_тест_#%!\'()[]:<>&&&amp;%F0'; + $html5EncodedHref = 'foo_тест_#%!\'()[]:<>&&&amp;%25F0'; // Settings: last element is $wgExternalInterwikiFragmentMode, the rest is $wgFragmentMode $legacy = [ 'legacy', 'legacy' ]; @@ -214,27 +216,27 @@ class SanitizerTest extends MediaWikiTestCase { // Transition to a new world: legacy links with HTML5 fallback [ 'Attribute', $legacyNew, $text, $legacyEncoded, Sanitizer::ID_PRIMARY ], - [ 'Attribute', $legacyNew, $text, $html5Encoded, Sanitizer::ID_FALLBACK ], + [ 'Attribute', $legacyNew, $text, $html5EncodedId, Sanitizer::ID_FALLBACK ], [ 'Link', $legacyNew, $text, $legacyEncoded ], [ 'ExternalInterwiki', $legacyNew, $text, $legacyEncoded ], // New world: HTML5 links, legacy fallbacks - [ 'Attribute', $newLegacy, $text, $html5Encoded, Sanitizer::ID_PRIMARY ], + [ 'Attribute', $newLegacy, $text, $html5EncodedId, Sanitizer::ID_PRIMARY ], [ 'Attribute', $newLegacy, $text, $legacyEncoded, Sanitizer::ID_FALLBACK ], - [ 'Link', $newLegacy, $text, $html5Encoded ], + [ 'Link', $newLegacy, $text, $html5EncodedHref ], [ 'ExternalInterwiki', $newLegacy, $text, $legacyEncoded ], // Distant future: no legacy fallbacks, but still linking to leagacy wikis - [ 'Attribute', $new, $text, $html5Encoded, Sanitizer::ID_PRIMARY ], + [ 'Attribute', $new, $text, $html5EncodedId, Sanitizer::ID_PRIMARY ], [ 'Attribute', $new, $text, false, Sanitizer::ID_FALLBACK ], - [ 'Link', $new, $text, $html5Encoded ], + [ 'Link', $new, $text, $html5EncodedHref ], [ 'ExternalInterwiki', $new, $text, $legacyEncoded ], // Just before the heat death of universe: external interwikis are also HTML5 \m/ - [ 'Attribute', $allNew, $text, $html5Encoded, Sanitizer::ID_PRIMARY ], + [ 'Attribute', $allNew, $text, $html5EncodedId, Sanitizer::ID_PRIMARY ], [ 'Attribute', $allNew, $text, false, Sanitizer::ID_FALLBACK ], - [ 'Link', $allNew, $text, $html5Encoded ], - [ 'ExternalInterwiki', $allNew, $text, $html5Encoded ], + [ 'Link', $allNew, $text, $html5EncodedHref ], + [ 'ExternalInterwiki', $allNew, $text, $html5EncodedHref ], ]; }