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 тест_#%!\'()[]:<>&&&';
+ $text = 'foo тест_#%!\'()[]:<>&&&%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_тест_#%!\'()[]:<>&&&';
+ '.26.26amp.3B.26amp.3Bamp.3B.25F0';
+ $html5EncodedId = 'foo_тест_#%!\'()[]:<>&&&%F0';
+ $html5EncodedHref = 'foo_тест_#%!\'()[]:<>&&&%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 ],
];
}