Icons API: Add wp_get_icon() to render registered icons as inline SVG#12010
Icons API: Add wp_get_icon() to render registered icons as inline SVG#12010t-hamano wants to merge 4 commits into
Conversation
Introduce a procedural wrapper around `WP_Icons_Registry` so themes and plugins can render any registered core icon as an inline SVG from PHP, mirroring the React `<Icon>` component. This bridges the gap for PHP contexts that otherwise depend on the deprecated dashicons font. `wp_get_icon()` looks up the icon content via the registry and uses `WP_HTML_Tag_Processor` to apply configurable `size` and `class` attributes, plus accessibility handling: a `label` produces `role="img"` and `aria-label`, while its absence marks the icon `aria-hidden="true"`. The function lives in a new `icons.php`, following the established registry-class-plus-procedural-API pairing used by connectors.php and abilities.php. Props ... Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
The test file only covers wp_get_icon(); the_wp_icon() does not exist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| * @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 ); |
There was a problem hiding this comment.
I think better to check if $name is not empty. WDYT?
if ( empty( $name ) ) {
return;
}
There was a problem hiding this comment.
Since get_registered_icon() returns null if $name is empty, adding an empty check feels redundant.
sirreal
left a comment
There was a problem hiding this comment.
Reproducing some feedback that I unintentionally left elsewhere (WordPress/gutenberg#78332 (review)).
| * @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 ); |
There was a problem hiding this comment.
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 size argument may be tricky. Are icons required to be square? Is that enforced?
And just to confirm, anything in the registry has passed through KSES, right?
| if ( ! empty( $args['class'] ) ) { | ||
| $processor->add_class( $args['class'] ); | ||
| } |
There was a problem hiding this comment.
Is this a single class, or could it be multiple classes? The ::add_class method expects a single class, and multiple classes should be applied individually.
Something like $processor->add_class( 'wp-icon class2 class3' ) is not how the method is intended to be used.
| if ( ! empty( $args['label'] ) ) { | ||
| $processor->set_attribute( 'role', 'img' ); | ||
| $processor->set_attribute( 'aria-label', $args['label'] ); | ||
| } else { | ||
| $processor->set_attribute( 'aria-hidden', 'true' ); | ||
| } |
There was a problem hiding this comment.
Consider that the svg content may already have these attributes, so if one branch should have role and aria-label but not aria-hidden, and vice-versa for the other branch, then it's a good idea to remove the attributes from the other branch so we don't wind up with:
<svg aria-hidden="true" role="img" aria-label="whoops?" />| if ( ! empty( $args['label'] ) ) { | |
| $processor->set_attribute( 'role', 'img' ); | |
| $processor->set_attribute( 'aria-label', $args['label'] ); | |
| } else { | |
| $processor->set_attribute( 'aria-hidden', 'true' ); | |
| } | |
| if ( ! empty( $args['label'] ) ) { | |
| $processor->set_attribute( 'role', 'img' ); | |
| $processor->set_attribute( 'aria-label', $args['label'] ); | |
| $processor->remove_attribute( 'aria-hidden' ) | |
| } else { | |
| $processor->set_attribute( 'aria-hidden', 'true' ); | |
| $processor->remove_attribute( 'role' ); | |
| $processor->remove_attribute( 'aria-label' ) | |
| } |
(Do check me and test this, consider the suggestion pseudo-code)
| $processor->set_attribute( 'width', (string) $args['size'] ); | ||
| $processor->set_attribute( 'height', (string) $args['size'] ); |
There was a problem hiding this comment.
What if we get something other than int, is that possible? The string "10%", or other less sensical values: [ '9', '9' ], "nonsense".
There was a problem hiding this comment.
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 absint() on it or something?
| $processor->set_attribute( 'role', 'img' ); | ||
| $processor->set_attribute( 'aria-label', $args['label'] ); | ||
| } else { | ||
| $processor->set_attribute( 'aria-hidden', 'true' ); |
There was a problem hiding this comment.
For decorative icons, should we also disable focusable to match what render_block_core_icon() is doing?
| $processor->set_attribute( 'aria-hidden', 'true' ); | |
| $processor->set_attribute( 'aria-hidden', 'true' ); | |
| $processor->set_attribute( 'focusable', 'false' ); |
|
|
||
| $processor->set_attribute( 'width', (string) $args['size'] ); | ||
| $processor->set_attribute( 'height', (string) $args['size'] ); | ||
| $processor->add_class( 'wp-icon' ); |
There was a problem hiding this comment.
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 render_block_core_icon() for example.
If we want to adopt it, we should likely consider adding it to render_block_core_icon() and any other React paths that render icons.
| * } | ||
| * @return string SVG markup for the icon, or empty string if not found. | ||
| */ | ||
| function wp_get_icon( $name, $args = array() ) { |
There was a problem hiding this comment.
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.
| $processor->set_attribute( 'aria-hidden', 'true' ); | ||
| } | ||
|
|
||
| return $processor->get_updated_html(); |
There was a problem hiding this comment.
Just to clarify, are we intentional about not adding a filter here?
| $processor->set_attribute( 'width', (string) $args['size'] ); | ||
| $processor->set_attribute( 'height', (string) $args['size'] ); |
There was a problem hiding this comment.
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 absint() on it or something?
| */ | ||
| public function test_wp_get_icon_escapes_attributes() { | ||
| $output = wp_get_icon( 'core/plus', array( 'class' => '"><script>alert(1)</script>' ) ); | ||
| $this->assertStringNotContainsString( '<script>', $output ); |
There was a problem hiding this comment.
What's the expected form here? <script>? Might make sense to add some more assertions or just change to a positive assertion that matches what exactly we expect.
There was a problem hiding this comment.
We could probably add some more assertion like assertEqualHTML for things like assertHTMLContainsNoTags( [ 'script' ], $html ) or similar.
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';| * @ticket 64847 | ||
| */ | ||
| public function test_wp_get_icon_escapes_attributes() { | ||
| $output = wp_get_icon( 'core/plus', array( 'class' => '"><script>alert(1)</script>' ) ); |
There was a problem hiding this comment.
Should we also test escaping in label and aria-label and any other user-controller attribute?
| $processor->set_attribute( 'height', (string) $args['size'] ); | ||
| $processor->add_class( 'wp-icon' ); | ||
|
|
||
| if ( ! empty( $args['class'] ) ) { |
There was a problem hiding this comment.
Is it fine if we pass multiple classes there (class-1 class-2)? To be precise and use the API correctly, should we explode() those and call add_class() for each of them? If you agree, let's also add a test for that.
This PR adds
wp_get_icon(), a procedural wrapper aroundWP_Icons_Registrythat returns a registered icon as an inline SVG string.Based on WordPress/gutenberg#78332.
Trac ticket: https://core.trac.wordpress.org/ticket/64847
Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 4.8
Used for: Porting the function and PHPUnit tests from the referenced Gutenberg PR to core conventions. I reviewed and tested the result; all 9 tests pass.
This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.