Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 44 additions & 13 deletions src/wp-includes/general-template.php
Original file line number Diff line number Diff line change
Expand Up @@ -473,14 +473,17 @@ function wp_get_tooltip_helper( $content, $args = array() ) {

$defaults = array(
'id' => wp_unique_id( 'wp-tooltip-' ),
'button' => '<button type="button" aria-label="%3$s"><span class="dashicons %4$s" aria-hidden="true"></span></button>',
'button' => '<button type="button" aria-label="{{wp-tooltip-label}}"><span class="dashicons {{wp-tooltip-icon}}" aria-hidden="true"></span></button>',
'label' => __( 'Help' ),
'close_label' => __( 'Close' ),
'icon' => 'dashicons-editor-help',
'class' => '',
'type' => 'tooltip',
);

// Captured before `wp_parse_args()` merges in the default button markup below.
$is_default_button = empty( $args['button'] );

$args = wp_parse_args( $args, $defaults );

$classes = ( 'tooltip' === $args['type'] ) ? 'wp-tooltip wp-is-tooltip' : 'wp-tooltip wp-is-toggletip';
Expand All @@ -492,11 +495,22 @@ function wp_get_tooltip_helper( $content, $args = array() ) {
$id = ( $args['id'] ) ? $args['id'] : $defaults['id'];
$button = ( $args['button'] ) ? $args['button'] : $defaults['button'];
$processed = false;

/*
* Placeholder tokens, rather than `sprintf()` conversion specifications,
* are used so the popover target, label, and icon can be filled in with
* `str_replace()` below. The button markup is later passed to `sprintf()`
* as an argument, not concatenated into the format string, so any literal
* `%` characters in caller-supplied markup (e.g. a percent-encoded URL)
* are never parsed as a conversion specification. See #65914.
*/
$popovertarget_token = '{{wp-tooltip-popovertarget}}';

$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', $popovertarget_token );
$processor->set_attribute( 'aria-haspopup', 'dialog' );
}
$button = $processor->get_updated_html();
Expand All @@ -512,15 +526,34 @@ function wp_get_tooltip_helper( $content, $args = array() ) {
}
if ( ! $processed ) {
// Button HTML passed was not valid.
$processor = new WP_HTML_Tag_Processor( $defaults['button'] );
$is_default_button = true;
$processor = new WP_HTML_Tag_Processor( $defaults['button'] );
$processor->add_class( 'wp-tooltip__toggle' );
if ( 'tooltip' !== $args['type'] ) {
$processor->set_attribute( 'popovertarget', '%2$s' );
$processor->set_attribute( 'popovertarget', $popovertarget_token );
$processor->set_attribute( 'aria-haspopup', 'dialog' );
}
$button = $processor->get_updated_html();
}

if ( false !== strpos( $button, $popovertarget_token ) ) {
$button = str_replace( $popovertarget_token, esc_attr( $id ), $button );
}

/*
* Only the default, core-generated button template contains the label
* and icon tokens. Caller-supplied button markup is never scanned for
* these tokens, so literal matching text in custom markup is left as-is.
*/
if ( $is_default_button ) {
$button_label = ( 'tooltip' === $args['type'] ) ? wp_strip_all_tags( $content, true ) : $args['label'];
$button = str_replace(
array( '{{wp-tooltip-label}}', '{{wp-tooltip-icon}}' ),
array( esc_attr( $button_label ), esc_attr( $icon ) ),
$button
);
}

/*
* The markup only uses phrasing content so it is valid when nested
* in a phrasing context. Sectioning content (e.g. `div`, `dialog`) will
Expand All @@ -529,19 +562,17 @@ function wp_get_tooltip_helper( $content, $args = array() ) {
*/
if ( 'tooltip' === $args['type'] ) {
// Tooltips are only used to visually display labels.
$label = wp_strip_all_tags( $content, true );
$markup = sprintf(
'<span class="%1$s">
' . $button . '
%4$s
<span popover="hint" id="%2$s" class="wp-tooltip__bubble" role="tooltip">' .
'<span id="%2$s-text" class="wp-tooltip__text">%5$s</span>' .
'<span id="%2$s-text" class="wp-tooltip__text">%3$s</span>' .
'</span>' .
'</span>',
esc_attr( $classes ),
esc_attr( $id ),
esc_attr( $label ),
esc_attr( $icon ),
esc_html( $content ),
$button,
);
} else {
/*
Expand All @@ -551,20 +582,20 @@ function wp_get_tooltip_helper( $content, $args = array() ) {
*/
$markup = sprintf(
'<span class="%1$s">
' . $button . '
%6$s
<span popover="auto" id="%2$s" class="wp-tooltip__bubble" role="dialog" aria-label="%3$s" tabindex="-1" autofocus>' .
'<span id="%2$s-text" class="wp-tooltip__text">%5$s</span>' .
'<button type="button" class="wp-tooltip__close" popovertarget="%2$s" popovertargetaction="hide" aria-label="%6$s">' .
'<span id="%2$s-text" class="wp-tooltip__text">%4$s</span>' .
'<button type="button" class="wp-tooltip__close" popovertarget="%2$s" popovertargetaction="hide" aria-label="%5$s">' .
'<span class="dashicons dashicons-no-alt" aria-hidden="true"></span>' .
'</button>' .
'</span>' .
'</span>',
esc_attr( $classes ),
esc_attr( $id ),
esc_attr( $args['label'] ),
esc_attr( $icon ),
esc_html( $content ),
esc_attr( $args['close_label'] ),
$button,
);
}

Expand Down
79 changes: 79 additions & 0 deletions tests/phpunit/tests/general/wpGetTooltip.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,85 @@ public function test_wp_get_toggletip_bubble_uses_dialog_role_and_autofocus() {
$this->assertStringContainsString( 'tabindex="-1" autofocus>', $html );
}

/**
* Tests that a percent sign in custom button markup is not parsed as a
* `sprintf()` conversion specification.
*
* @ticket 65914
*/
public function test_wp_get_tooltip_does_not_treat_percent_encoded_url_as_format_string() {
$html = wp_get_tooltip(
'Helpful text.',
array( 'button' => '<a href="/wp-admin/edit.php?s=hello%20world">Search</a>' )
);

$this->assertStringContainsString( 'href="/wp-admin/edit.php?s=hello%20world"', $html );
}

/**
* Tests that literal text containing a percent sign is preserved verbatim.
*
* @ticket 65914
*/
public function test_wp_get_tooltip_preserves_percent_sign_in_button_text() {
$html = wp_get_tooltip(
'Helpful text.',
array( 'button' => '<button type="button">100% done</button>' )
);

$this->assertStringContainsString( '100% done', $html );
}

/**
* Tests that a percent-encoded path is not silently rewritten.
*
* @ticket 65914
*/
public function test_wp_get_tooltip_does_not_rewrite_percent_encoded_path() {
$html = wp_get_tooltip(
'Helpful text.',
array( 'button' => '<a href="/x?p=a%2Fb">Link</a>' )
);

$this->assertStringContainsString( 'href="/x?p=a%2Fb"', $html );
}

/**
* Tests that a meta box style ID containing a percent sign, as used by
* `do_meta_boxes()`, does not cause a fatal error.
*
* @ticket 65914
*/
public function test_wp_get_tooltip_does_not_fatal_on_percent_in_id_attribute() {
$html = wp_get_tooltip(
'Helpful text.',
array( 'button' => '<button type="button" id="sale_100%_off">Toggle</button>' )
);

$this->assertStringContainsString( 'id="sale_100%_off"', $html );
}

/**
* Tests that the default, core-generated button still receives its label,
* icon, and popovertarget correctly after the button markup is no longer
* concatenated into the `sprintf()` format string.
*
* @ticket 65914
*/
public function test_wp_get_toggletip_default_button_still_receives_label_and_icon() {
$html = wp_get_toggletip(
'Some help content here.',
array(
'label' => 'Info',
'icon' => 'dashicons-info',
)
);

$this->assertStringContainsString( 'aria-label="Info"', $html );
$this->assertStringContainsString( 'class="dashicons dashicons-info"', $html );
$this->assertStringNotContainsString( '{{', $html, 'Placeholder tokens must not leak into output.' );
}

/**
* Data provider.
*
Expand Down
Loading