diff --git a/src/wp-includes/class-wp-icons-registry.php b/src/wp-includes/class-wp-icons-registry.php
index a10960da9a077..6d7ac0e0de689 100644
--- a/src/wp-includes/class-wp-icons-registry.php
+++ b/src/wp-includes/class-wp-icons-registry.php
@@ -237,29 +237,53 @@ public function unregister( $icon_name ) {
* @return string The sanitized icon SVG content.
*/
protected function sanitize_icon_content( $icon_content ) {
+ $stroke_attributes = array(
+ 'style' => true,
+ 'stroke' => true,
+ 'stroke-width' => true,
+ 'stroke-linecap' => true,
+ 'stroke-linejoin' => true,
+ 'stroke-miterlimit' => true,
+ 'vector-effect' => true,
+ );
+
$allowed_tags = array(
- 'svg' => array(
- 'class' => true,
- 'xmlns' => true,
- 'width' => true,
- 'height' => true,
- 'viewbox' => true,
- 'aria-hidden' => true,
- 'role' => true,
- 'focusable' => true,
+ 'svg' => array_merge(
+ array(
+ 'class' => true,
+ 'xmlns' => true,
+ 'width' => true,
+ 'height' => true,
+ 'viewbox' => true,
+ 'aria-hidden' => true,
+ 'role' => true,
+ 'focusable' => true,
+ 'fill' => true,
+ 'fill-rule' => true,
+ 'clip-rule' => true,
+ ),
+ $stroke_attributes
),
- 'path' => array(
- 'fill' => true,
- 'fill-rule' => true,
- 'd' => true,
- 'transform' => true,
+ 'path' => array_merge(
+ array(
+ 'fill' => true,
+ 'fill-rule' => true,
+ 'clip-rule' => true,
+ 'd' => true,
+ 'transform' => true,
+ ),
+ $stroke_attributes
),
- 'polygon' => array(
- 'fill' => true,
- 'fill-rule' => true,
- 'points' => true,
- 'transform' => true,
- 'focusable' => true,
+ 'polygon' => array_merge(
+ array(
+ 'fill' => true,
+ 'fill-rule' => true,
+ 'clip-rule' => true,
+ 'points' => true,
+ 'transform' => true,
+ 'focusable' => true,
+ ),
+ $stroke_attributes
),
);
return wp_kses( $icon_content, $allowed_tags );
diff --git a/tests/phpunit/tests/icons/wpIconsRegistry.php b/tests/phpunit/tests/icons/wpIconsRegistry.php
index 42853d2666a95..c6268666a2b7f 100644
--- a/tests/phpunit/tests/icons/wpIconsRegistry.php
+++ b/tests/phpunit/tests/icons/wpIconsRegistry.php
@@ -284,6 +284,70 @@ public function test_register_icon_sanitizes_content() {
$this->assertSame( '', $icon['content'] );
}
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_icon_content_sources() {
+ return array(
+ 'inline content' => array( false ),
+ 'file path' => array( true ),
+ );
+ }
+
+ /**
+ * Should preserve the attributes that stroke-based icons rely on.
+ *
+ * Only covers the attributes allowed for stroke-based icons. Those that were
+ * already allowed are covered by test_register_icon_sanitizes_content().
+ *
+ * @ticket 66101
+ *
+ * @dataProvider data_icon_content_sources
+ *
+ * @covers ::register
+ *
+ * @param bool $use_file_path Whether to register the icon from a file path.
+ */
+ public function test_register_icon_preserves_stroke_attributes( $use_file_path ) {
+ $stroke = 'style="fill: none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" vector-effect="non-scaling-stroke"';
+ $content = '';
+ $name = 'test-collection/stroke-icon';
+ $settings = array( 'label' => 'Stroke Icon' );
+
+ if ( $use_file_path ) {
+ $settings['file_path'] = $this->create_temp_icon_file( $content );
+ } else {
+ $settings['content'] = $content;
+ }
+
+ $this->assertTrue( $this->registry->register( $name, $settings ) );
+
+ $icon = $this->registry->get_registered_icon( $name );
+
+ $this->assertStringContainsString(
+ '