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' .
'',
esc_attr( $classes ),
esc_attr( $id ),
- esc_attr( $args['label'] ),
+ esc_attr( $label ),
esc_attr( $icon ),
esc_html( $content ),
esc_attr( $args['close_label'] ),
+ $button,
);
}
diff --git a/tests/phpunit/tests/general/wpGetTooltip.php b/tests/phpunit/tests/general/wpGetTooltip.php
index e545902e6ed6d..3687f878d238e 100644
--- a/tests/phpunit/tests/general/wpGetTooltip.php
+++ b/tests/phpunit/tests/general/wpGetTooltip.php
@@ -206,6 +206,90 @@ public function test_wp_get_toggletip_bubble_uses_dialog_role_and_autofocus() {
$this->assertStringContainsString( 'tabindex="-1" autofocus>', $html );
}
+ /**
+ * Tests that a percent sign in the button markup is not treated as a
+ * `sprintf()` conversion specification.
+ *
+ * @ticket 65914
+ *
+ * @dataProvider data_button_markup_containing_percent_signs
+ *
+ * @param string $button Button markup containing a percent sign.
+ * @param string $expected Substring that must be preserved in the output.
+ */
+ public function test_wp_get_tooltip_preserves_percent_signs_in_button_markup( $button, $expected ) {
+ $this->assertStringContainsString(
+ $expected,
+ wp_get_tooltip( 'Helpful text.', array( 'button' => $button ) ),
+ 'The tooltip did not preserve the percent sign.'
+ );
+ $this->assertStringContainsString(
+ $expected,
+ wp_get_toggletip( 'Helpful text.', array( 'button' => $button ) ),
+ 'The toggletip did not preserve the percent sign.'
+ );
+ }
+
+ /**
+ * Tests that a percent-encoded URL in anchor markup is not rewritten.
+ *
+ * @ticket 65914
+ *
+ * @dataProvider data_percent_encoded_hrefs
+ *
+ * @param string $href A percent-encoded URL.
+ */
+ public function test_wp_get_tooltip_preserves_percent_encoding_in_anchor_href( $href ) {
+ $html = wp_get_tooltip(
+ 'Helpful text.',
+ array( 'button' => 'Search' )
+ );
+
+ $this->assertStringContainsString( 'href="' . $href . '"', $html );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_percent_encoded_hrefs() {
+ return array(
+ 'an encoded space' => array( '/wp-admin/edit.php?s=hello%20world' ),
+ 'an encoded slash' => array( '/x?p=a%2Fb' ),
+ );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_button_markup_containing_percent_signs() {
+ return array(
+ 'a literal percent sign' => array(
+ '',
+ '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.
*