Skip to content

Commit 27b2d08

Browse files
t-hamanoclaude
andcommitted
Icons: Allow stroke attributes and rect/circle elements in the icon SVG sanitizer.
Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f83d5cc commit 27b2d08

3 files changed

Lines changed: 143 additions & 26 deletions

File tree

‎src/wp-includes/class-wp-icons-registry.php‎

Lines changed: 72 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -242,38 +242,87 @@ public function unregister( $icon_name ) {
242242
/**
243243
* Sanitizes the icon SVG content.
244244
*
245-
* Logic borrowed from twentytwenty.
246-
* @see twentytwenty_get_theme_svg
247-
*
248245
* @since 7.0.0
249246
*
250247
* @param string $icon_content The icon SVG content to sanitize.
251248
* @return string The sanitized icon SVG content.
252249
*/
253250
protected function sanitize_icon_content( $icon_content ) {
251+
$stroke_attributes = array(
252+
'style' => true,
253+
'stroke' => true,
254+
'stroke-width' => true,
255+
'stroke-linecap' => true,
256+
'stroke-linejoin' => true,
257+
'stroke-miterlimit' => true,
258+
'vector-effect' => true,
259+
);
260+
254261
$allowed_tags = array(
255-
'svg' => array(
256-
'class' => true,
257-
'xmlns' => true,
258-
'width' => true,
259-
'height' => true,
260-
'viewbox' => true,
261-
'aria-hidden' => true,
262-
'role' => true,
263-
'focusable' => true,
262+
'svg' => array_merge(
263+
array(
264+
'class' => true,
265+
'xmlns' => true,
266+
'width' => true,
267+
'height' => true,
268+
'viewbox' => true,
269+
'aria-hidden' => true,
270+
'role' => true,
271+
'focusable' => true,
272+
'fill' => true,
273+
'fill-rule' => true,
274+
'clip-rule' => true,
275+
),
276+
$stroke_attributes
277+
),
278+
'path' => array_merge(
279+
array(
280+
'fill' => true,
281+
'fill-rule' => true,
282+
'clip-rule' => true,
283+
'd' => true,
284+
'opacity' => true,
285+
'transform' => true,
286+
),
287+
$stroke_attributes
264288
),
265-
'path' => array(
266-
'fill' => true,
267-
'fill-rule' => true,
268-
'd' => true,
269-
'transform' => true,
289+
'polygon' => array_merge(
290+
array(
291+
'fill' => true,
292+
'fill-rule' => true,
293+
'clip-rule' => true,
294+
'points' => true,
295+
'transform' => true,
296+
'focusable' => true,
297+
),
298+
$stroke_attributes
270299
),
271-
'polygon' => array(
272-
'fill' => true,
273-
'fill-rule' => true,
274-
'points' => true,
275-
'transform' => true,
276-
'focusable' => true,
300+
'rect' => array_merge(
301+
array(
302+
'fill' => true,
303+
'fill-rule' => true,
304+
'clip-rule' => true,
305+
'x' => true,
306+
'y' => true,
307+
'width' => true,
308+
'height' => true,
309+
'rx' => true,
310+
'ry' => true,
311+
'transform' => true,
312+
),
313+
$stroke_attributes
314+
),
315+
'circle' => array_merge(
316+
array(
317+
'fill' => true,
318+
'fill-rule' => true,
319+
'clip-rule' => true,
320+
'cx' => true,
321+
'cy' => true,
322+
'r' => true,
323+
'transform' => true,
324+
),
325+
$stroke_attributes
277326
),
278327
);
279328
return wp_kses( $icon_content, $allowed_tags );

‎tests/phpunit/tests/icons/wpGetIcon.php‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,11 @@ public function test_wp_get_icon_custom_size() {
5555
* @ticket 64847
5656
*/
5757
public function test_wp_get_icon_size_null_leaves_dimensions_untouched() {
58-
$output = wp_get_icon( 'core/plus', array( 'size' => null ) );
59-
$this->assertStringNotContainsString( 'width=', $output );
60-
$this->assertStringNotContainsString( 'height=', $output );
58+
$output = wp_get_icon( 'core/plus', array( 'size' => null ) );
59+
$processor = new WP_HTML_Tag_Processor( $output );
60+
$this->assertTrue( $processor->next_tag( 'svg' ) );
61+
$this->assertNull( $processor->get_attribute( 'width' ) );
62+
$this->assertNull( $processor->get_attribute( 'height' ) );
6163
}
6264

6365
/**

‎tests/phpunit/tests/icons/wpIconsRegistry.php‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,20 @@ private function create_temp_icon_file( $contents, $extension = 'svg' ) {
7474
return $this->temp_file;
7575
}
7676

77+
/**
78+
* Invokes the WP_Icons_Registry::sanitize_icon_content method on the registry instance.
79+
*
80+
* @param string $icon_content The icon SVG content to sanitize.
81+
* @return string The sanitized icon SVG content.
82+
*/
83+
private function sanitize_icon_content( $icon_content ) {
84+
$method = new ReflectionMethod( $this->registry, 'sanitize_icon_content' );
85+
if ( PHP_VERSION_ID < 80100 ) {
86+
$method->setAccessible( true );
87+
}
88+
return $method->invoke( $this->registry, $icon_content );
89+
}
90+
7791
/**
7892
* Provides valid namespaced icon names, including names that contain,
7993
* start or end with digits, as well as underscores and hyphens.
@@ -284,6 +298,58 @@ public function test_register_icon_sanitizes_content() {
284298
$this->assertSame( '<svg viewbox="0 0 24 24"><path d="M0 0" /></svg>', $icon['content'] );
285299
}
286300

301+
/**
302+
* @ticket 65795
303+
*
304+
* @covers ::sanitize_icon_content
305+
*
306+
* @dataProvider data_sanitize_icon_content
307+
*
308+
* @param string $input The icon content to sanitize.
309+
* @param string $expected The expected sanitized output.
310+
*/
311+
public function test_sanitize_icon_content( $input, $expected ) {
312+
$this->assertSame( $expected, $this->sanitize_icon_content( $input ) );
313+
}
314+
315+
/**
316+
* Data provider for test_sanitize_icon_content.
317+
*
318+
* @return array<string, array{0: string, 1: string}> Data sets of [ $input, $expected ].
319+
*/
320+
public function data_sanitize_icon_content() {
321+
return array(
322+
'allows fill and clip rules on svg' => array(
323+
'<svg fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"><path d="M0 0" /></svg>',
324+
'<svg fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"><path d="M0 0" /></svg>',
325+
),
326+
'allows stroke attributes and style on svg' => array(
327+
'<svg style="fill: none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" vector-effect="non-scaling-stroke"><path d="M0 0" /></svg>',
328+
'<svg style="fill: none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" vector-effect="non-scaling-stroke"><path d="M0 0" /></svg>',
329+
),
330+
'allows clip rule, opacity and stroke on path' => array(
331+
'<svg><path d="M0 0" fill-rule="evenodd" clip-rule="evenodd" opacity="0.4" style="fill: none" stroke="currentColor" vector-effect="non-scaling-stroke" /></svg>',
332+
'<svg><path d="M0 0" fill-rule="evenodd" clip-rule="evenodd" opacity="0.4" style="fill: none" stroke="currentColor" vector-effect="non-scaling-stroke" /></svg>',
333+
),
334+
'allows clip rule and stroke on polygon' => array(
335+
'<svg><polygon points="0,0 1,1" clip-rule="evenodd" stroke="currentColor" vector-effect="non-scaling-stroke" /></svg>',
336+
'<svg><polygon points="0,0 1,1" clip-rule="evenodd" stroke="currentColor" vector-effect="non-scaling-stroke" /></svg>',
337+
),
338+
'allows rect' => array(
339+
'<svg><rect x="4" y="5" width="16" height="14" rx="2" ry="2" fill="currentColor" stroke="currentColor" transform="rotate(45)" vector-effect="non-scaling-stroke" /></svg>',
340+
'<svg><rect x="4" y="5" width="16" height="14" rx="2" ry="2" fill="currentColor" stroke="currentColor" transform="rotate(45)" vector-effect="non-scaling-stroke" /></svg>',
341+
),
342+
'allows circle' => array(
343+
'<svg><circle cx="12" cy="12" r="3" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" transform="rotate(45)" vector-effect="non-scaling-stroke" /></svg>',
344+
'<svg><circle cx="12" cy="12" r="3" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" transform="rotate(45)" vector-effect="non-scaling-stroke" /></svg>',
345+
),
346+
'strips opacity on elements other than path' => array(
347+
'<svg opacity="0.4"><rect width="1" height="1" opacity="0.4" /></svg>',
348+
'<svg><rect width="1" height="1" /></svg>',
349+
),
350+
);
351+
}
352+
287353
/**
288354
* Should fail to register an icon that provides both `content` and `file_path`.
289355
*

0 commit comments

Comments
 (0)