-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Icons API: Add wp_get_icon() to render registered icons as inline SVG #12010
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from all commits
d9dc326
54df3d4
7bc1bd8
961087d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,68 @@ | ||||||||||||||||||||||||||||||||
| <?php | ||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Icons API: Icon rendering helper functions. | ||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||
| * @package WordPress | ||||||||||||||||||||||||||||||||
| * @subpackage Icons | ||||||||||||||||||||||||||||||||
| * @since 7.1.0 | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Returns the SVG markup for a registered icon. | ||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||
| * @since 7.1.0 | ||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||
| * @param string $name The namespaced icon name (e.g. 'core/plus'). | ||||||||||||||||||||||||||||||||
| * @param array $args { | ||||||||||||||||||||||||||||||||
| * Optional. Arguments for the icon. Default empty array. | ||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||
| * @type int $size Width and height in pixels. Default 24. | ||||||||||||||||||||||||||||||||
| * @type string $class Additional CSS class names. Default empty string. | ||||||||||||||||||||||||||||||||
| * @type string $label Accessible label. If provided, the SVG gets | ||||||||||||||||||||||||||||||||
| * role="img" and aria-label. If omitted, the SVG | ||||||||||||||||||||||||||||||||
| * gets aria-hidden="true". Default empty string. | ||||||||||||||||||||||||||||||||
| * } | ||||||||||||||||||||||||||||||||
| * @return string SVG markup for the icon, or empty string if not found. | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| function wp_get_icon( $name, $args = array() ) { | ||||||||||||||||||||||||||||||||
| $icon = WP_Icons_Registry::get_instance()->get_registered_icon( $name ); | ||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think better to check if
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do we know about the registered icons? If this is a public API where arbitrary SVG content (that passes KSES checks) may be in the icon, things can get very complicated. One that comes to mind is if the SVG has a viewBox, then a single And just to confirm, anything in the registry has passed through |
||||||||||||||||||||||||||||||||
| if ( is_null( $icon ) ) { | ||||||||||||||||||||||||||||||||
| return ''; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| $svg = $icon['content']; | ||||||||||||||||||||||||||||||||
| if ( empty( $svg ) ) { | ||||||||||||||||||||||||||||||||
| return ''; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| $args = wp_parse_args( | ||||||||||||||||||||||||||||||||
| $args, | ||||||||||||||||||||||||||||||||
| array( | ||||||||||||||||||||||||||||||||
| 'size' => 24, | ||||||||||||||||||||||||||||||||
| 'class' => '', | ||||||||||||||||||||||||||||||||
| 'label' => '', | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| $processor = new WP_HTML_Tag_Processor( $svg ); | ||||||||||||||||||||||||||||||||
| if ( ! $processor->next_tag( 'svg' ) ) { | ||||||||||||||||||||||||||||||||
| return ''; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| $processor->set_attribute( 'width', (string) $args['size'] ); | ||||||||||||||||||||||||||||||||
| $processor->set_attribute( 'height', (string) $args['size'] ); | ||||||||||||||||||||||||||||||||
|
Comment on lines
+52
to
+53
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if we get something other than
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, came here to ask some of the same. Also, what about negative integers - should we make sure it's not the case by calling |
||||||||||||||||||||||||||||||||
| $processor->add_class( 'wp-icon' ); | ||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How necessary is this class? Are we adding it anywhere else? I don't think we are, and this may mean we're actually producing non-identical output with If we want to adopt it, we should likely consider adding it to |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if ( ! empty( $args['class'] ) ) { | ||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it fine if we pass multiple classes there ( |
||||||||||||||||||||||||||||||||
| $processor->add_class( $args['class'] ); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+56
to
+58
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a single class, or could it be multiple classes? The Something like |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if ( ! empty( $args['label'] ) ) { | ||||||||||||||||||||||||||||||||
| $processor->set_attribute( 'role', 'img' ); | ||||||||||||||||||||||||||||||||
| $processor->set_attribute( 'aria-label', $args['label'] ); | ||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||
| $processor->set_attribute( 'aria-hidden', 'true' ); | ||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For decorative icons, should we also disable focusable to match what
Suggested change
|
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+60
to
+65
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider that the <svg aria-hidden="true" role="img" aria-label="whoops?" />
Suggested change
(Do check me and test this, consider the suggestion pseudo-code) |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return $processor->get_updated_html(); | ||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to clarify, are we intentional about not adding a filter here? |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| <?php | ||
| /** | ||
| * Unit tests covering the wp_get_icon() function. | ||
| * | ||
| * @package WordPress | ||
| * @subpackage Icons | ||
| * @since 7.1.0 | ||
| * | ||
| * @group icons | ||
| * | ||
| * @covers ::wp_get_icon | ||
| */ | ||
| class Tests_Icons_WpGetIcon extends WP_UnitTestCase { | ||
|
|
||
| /** | ||
| * @ticket 64847 | ||
| */ | ||
| public function test_wp_get_icon_returns_svg_for_known_icon() { | ||
| $output = wp_get_icon( 'core/plus' ); | ||
| $this->assertStringStartsWith( '<svg ', $output ); | ||
| $this->assertStringContainsString( '</svg>', $output ); | ||
| } | ||
|
|
||
| /** | ||
| * @ticket 64847 | ||
| */ | ||
| public function test_wp_get_icon_returns_empty_string_for_unknown_icon() { | ||
| $output = wp_get_icon( 'this-icon-does-not-exist' ); | ||
| $this->assertSame( '', $output ); | ||
| } | ||
|
|
||
| /** | ||
| * @ticket 64847 | ||
| */ | ||
| public function test_wp_get_icon_default_attributes() { | ||
| $output = wp_get_icon( 'core/plus' ); | ||
| // WP_HTML_Tag_Processor lowercases attribute names. | ||
| $this->assertStringContainsString( 'viewbox="0 0 24 24"', $output ); | ||
| $this->assertStringContainsString( 'width="24"', $output ); | ||
| $this->assertStringContainsString( 'height="24"', $output ); | ||
| $this->assertStringContainsString( 'class="wp-icon"', $output ); | ||
| $this->assertStringContainsString( 'aria-hidden="true"', $output ); | ||
| } | ||
|
|
||
| /** | ||
| * @ticket 64847 | ||
| */ | ||
| public function test_wp_get_icon_custom_size() { | ||
| $output = wp_get_icon( 'core/plus', array( 'size' => 32 ) ); | ||
| $this->assertStringContainsString( 'width="32"', $output ); | ||
| $this->assertStringContainsString( 'height="32"', $output ); | ||
| } | ||
|
|
||
| /** | ||
| * @ticket 64847 | ||
| */ | ||
| public function test_wp_get_icon_custom_class() { | ||
| $output = wp_get_icon( 'core/plus', array( 'class' => 'my-button-icon' ) ); | ||
| $this->assertStringContainsString( 'class="wp-icon my-button-icon"', $output ); | ||
| } | ||
|
|
||
| /** | ||
| * @ticket 64847 | ||
| */ | ||
| public function test_wp_get_icon_with_label() { | ||
| $output = wp_get_icon( 'core/plus', array( 'label' => 'Add item' ) ); | ||
| $this->assertStringContainsString( 'role="img"', $output ); | ||
| $this->assertStringContainsString( 'aria-label="Add item"', $output ); | ||
| $this->assertStringNotContainsString( 'aria-hidden', $output ); | ||
| } | ||
|
|
||
| /** | ||
| * @ticket 64847 | ||
| */ | ||
| public function test_wp_get_icon_without_label_is_hidden() { | ||
| $output = wp_get_icon( 'core/plus' ); | ||
| $this->assertStringContainsString( 'aria-hidden="true"', $output ); | ||
| $this->assertStringNotContainsString( 'role="img"', $output ); | ||
| $this->assertStringNotContainsString( 'aria-label', $output ); | ||
| } | ||
|
|
||
| /** | ||
| * @ticket 64847 | ||
| */ | ||
| public function test_wp_get_icon_contains_svg_content() { | ||
| $output = wp_get_icon( 'core/plus' ); | ||
| $this->assertStringContainsString( '<path ', $output ); | ||
| } | ||
|
|
||
| /** | ||
| * @ticket 64847 | ||
| */ | ||
| public function test_wp_get_icon_escapes_attributes() { | ||
| $output = wp_get_icon( 'core/plus', array( 'class' => '"><script>alert(1)</script>' ) ); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we also test escaping in |
||
| $this->assertStringNotContainsString( '<script>', $output ); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the expected form here?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could probably add some more assertion like assertEqualHTML for things like For this case, it may be a good idea to create a tag processor with something like this, what we care about is not having a SCRIPT tag: $p = new WP_HTML_Tag_Processor('<svg><script></script>');
while ( $p->next_token() ) {
var_dump( $p->get_token_name() );
if ( 'SCRIPT' === $p->get_token_name() ) {
echo 'TEST FAILURE';
throw new Error('bad bad bad');
}
}
// assert $p->paused_at_incomplete_token() === false;Or it may be better to just assert that that class was set: $p = new WP_HTML_Tag_Processor('<hax0r>');
$p->next_tag();
$p->add_class('"><script>alert(1)</script>');
$malicious = $p->get_updated_html();
$p = new WP_HTML_Tag_Processor($malicious);
$p->next_tag();
assert( $p->has_class( '"><script>alert(1)</script>' ) );
echo 'test passed'; |
||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a blocker, but we may want to refactor
render_block_core_icon()to use this function instead of reinventing some of the same logic.