From 7735a48368a0dc05d240477d9ec0fd98e5d5f845 Mon Sep 17 00:00:00 2001 From: Kamran Abdul Aziz Date: Thu, 20 Aug 2026 17:01:52 +0530 Subject: [PATCH] General: Preserve percent signs in tooltip buttons `wp_get_tooltip()` and `wp_get_toggletip()` concatenated caller-supplied button markup into a `sprintf()` format string, so literal percent signs in valid markup were read as conversion specifications. This caused fatal errors or silently corrupted output, including percent-encoded URLs. Build the generated button with its final label, icon, and popover target rather than with placeholders, then pass the button markup to `sprintf()` as a value. Caller-supplied button markup is no longer scanned or substituted. See #65914. --- src/wp-includes/general-template.php | 38 ++++++--- tests/phpunit/tests/general/wpGetTooltip.php | 84 ++++++++++++++++++++ 2 files changed, 110 insertions(+), 12 deletions(-) diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index 22a1d3e307d3e..346c14625a3ce 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -473,7 +473,7 @@ function wp_get_tooltip_helper( $content, $args = array() ) { $defaults = array( 'id' => wp_unique_id( 'wp-tooltip-' ), - 'button' => '', + 'button' => '', 'label' => __( 'Help' ), 'close_label' => __( 'Close' ), 'icon' => 'dashicons-editor-help', @@ -488,15 +488,29 @@ function wp_get_tooltip_helper( $content, $args = array() ) { $classes .= ' ' . $args['class']; } - $icon = ( $args['icon'] ) ? trim( $args['icon'] ) : $defaults['icon']; - $id = ( $args['id'] ) ? $args['id'] : $defaults['id']; - $button = ( $args['button'] ) ? $args['button'] : $defaults['button']; + $icon = ( $args['icon'] ) ? trim( $args['icon'] ) : $defaults['icon']; + $id = ( $args['id'] ) ? $args['id'] : $defaults['id']; + + // Tooltips use the content as the accessible name; toggletips use the label. + $label = ( 'tooltip' === $args['type'] ) ? wp_strip_all_tags( $content, true ) : $args['label']; + + /* + * The generated button is built with its final values rather than with + * placeholders, so that caller-supplied markup is never scanned or + * substituted. A percent sign in custom markup, such as a percent-encoded + * URL, is therefore never treated as a conversion specification. + */ + $default_button = ''; + + $button = ( $args['button'] ) ? $args['button'] : $default_button; $processed = false; $processor = new WP_HTML_Tag_Processor( $button ); if ( true === $processor->next_tag( 'button' ) ) { $processor->add_class( 'wp-tooltip__toggle' ); if ( 'tooltip' !== $args['type'] ) { - $processor->set_attribute( 'popovertarget', '%2$s' ); + $processor->set_attribute( 'popovertarget', $id ); $processor->set_attribute( 'aria-haspopup', 'dialog' ); } $button = $processor->get_updated_html(); @@ -512,10 +526,10 @@ function wp_get_tooltip_helper( $content, $args = array() ) { } if ( ! $processed ) { // Button HTML passed was not valid. - $processor = new WP_HTML_Tag_Processor( $defaults['button'] ); + $processor = new WP_HTML_Tag_Processor( $default_button ); $processor->add_class( 'wp-tooltip__toggle' ); if ( 'tooltip' !== $args['type'] ) { - $processor->set_attribute( 'popovertarget', '%2$s' ); + $processor->set_attribute( 'popovertarget', $id ); $processor->set_attribute( 'aria-haspopup', 'dialog' ); } $button = $processor->get_updated_html(); @@ -528,11 +542,9 @@ function wp_get_tooltip_helper( $content, $args = array() ) { * the layout. See #65660. */ if ( 'tooltip' === $args['type'] ) { - // Tooltips are only used to visually display labels. - $label = wp_strip_all_tags( $content, true ); $markup = sprintf( ' - ' . $button . ' + %6$s ' . '%5$s' . '' . @@ -542,6 +554,7 @@ function wp_get_tooltip_helper( $content, $args = array() ) { esc_attr( $label ), esc_attr( $icon ), esc_html( $content ), + $button, ); } else { /* @@ -551,7 +564,7 @@ function wp_get_tooltip_helper( $content, $args = array() ) { */ $markup = sprintf( ' - ' . $button . ' + %7$s ' . '%5$s' . '', + '100% done', + ), + 'a percent sign then a word' => array( + '', + 'Save 20%!', + ), + 'an unknown format specifier' => array( + '', + 'Buy %q now', + ), + 'a percent sign in an ID' => array( + '', + 'aria-describedby="box_100%_complete-title"', + ), + 'text resembling a argnum' => array( + '', + 'Use %2$s in your code', + ), + ); + } + /** * Data provider. *