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
11 changes: 10 additions & 1 deletion src/wp-admin/includes/class-wp-links-list-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,20 @@ public function column_cb( $item ) {
*/
public function column_name( $link ) {
$edit_link = get_edit_bookmark_link( $link );

/*
* `display_rows()` escapes the name, so entities are decoded first to let
* `wp_strip_all_tags()` detect the HTML. Otherwise it survives the
* `esc_attr()` call below and is announced as literal text.
*/
$aria_label_name = html_entity_decode( $link->link_name, ENT_QUOTES, get_bloginfo( 'charset' ) );
$aria_label_name = wp_strip_all_tags( $aria_label_name );

printf(
'<strong><a class="row-title" href="%s" aria-label="%s">%s</a></strong>',
$edit_link,
/* translators: %s: Link name. */
esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;' ), $link->link_name ) ),
esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;' ), $aria_label_name ) ),
$link->link_name
);
}
Expand Down
8 changes: 7 additions & 1 deletion src/wp-admin/includes/class-wp-media-list-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -927,7 +927,13 @@ protected function handle_row_actions( $item, $column_name, $primary ) {
// Restores the more descriptive, specific name for use within this method.
$post = $item;

$att_title = _draft_or_post_title();
/*
* `_draft_or_post_title()` escapes the title, so entities are decoded first
* to let `wp_strip_all_tags()` detect the HTML. Otherwise it survives the
* `esc_attr()` calls on the `aria-label` values and is announced as literal text.
*/
$att_title = html_entity_decode( _draft_or_post_title(), ENT_QUOTES, get_bloginfo( 'charset' ) );
$att_title = wp_strip_all_tags( $att_title );
$actions = $this->_get_row_actions( $post, $att_title );

return $this->row_actions( $actions );
Expand Down
9 changes: 8 additions & 1 deletion src/wp-admin/includes/class-wp-posts-list-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -1579,7 +1579,14 @@ protected function handle_row_actions( $item, $column_name, $primary ) {
$post_type_object = get_post_type_object( $post->post_type );
$can_edit_post = current_user_can( 'edit_post', $post->ID );
$actions = array();
$title = _draft_or_post_title();

/*
* `_draft_or_post_title()` escapes the title, so entities are decoded first
* to let `wp_strip_all_tags()` detect the HTML. Otherwise it survives the
* `esc_attr()` calls below and is announced as literal text.
*/
$title = html_entity_decode( _draft_or_post_title(), ENT_QUOTES, get_bloginfo( 'charset' ) );
$title = wp_strip_all_tags( $title );

if ( $can_edit_post && 'trash' !== $post->post_status ) {
$actions['edit'] = sprintf(
Expand Down
56 changes: 56 additions & 0 deletions tests/phpunit/tests/admin/wpLinksListTable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

/**
* @group admin
*/
class Tests_Admin_wpLinksListTable extends WP_UnitTestCase {
/**
* A list table for testing.
*
* @var WP_Links_List_Table
*/
protected $table;

public function set_up() {
parent::set_up();

$this->table = _get_list_table( 'WP_Links_List_Table', array( 'screen' => 'link-manager' ) );
}

/**
* Tests that `WP_Links_List_Table::column_name()` strips HTML from the link
* name used within the `aria-label` attribute.
*
* The name is escaped by `WP_Links_List_Table::display_rows()`, so any HTML
* it contains survives `esc_attr()` and is announced as literal text by
* screen readers.
*
* @ticket 65729
*
* @covers WP_Links_List_Table::column_name
*/
public function test_column_name_should_strip_html_from_the_aria_label() {
wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );

// A link name is saved to the database with any HTML it contains already escaped.
$link = self::factory()->bookmark->create_and_get( array( 'link_name' => 'my&lt;div&gt;link' ) );

// Replicate the escaping applied by `WP_Links_List_Table::display_rows()`.
$link->link_name = esc_attr( $link->link_name );

ob_start();
$this->table->column_name( $link );
$output = ob_get_clean();

$this->assertStringContainsString(
'aria-label="Edit &#8220;mylink&#8221;"',
$output,
'The aria-label did not contain the stripped name.'
);
$this->assertStringContainsString(
'>my&lt;div&gt;link</a>',
$output,
'The displayed link name should be left unchanged.'
);
}
}
40 changes: 40 additions & 0 deletions tests/phpunit/tests/admin/wpMediaListTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,46 @@ public function test_get_row_actions_should_not_include_download_without_an_atta
$this->assertArrayNotHasKey( 'download', $actions, '"download" was included in the actions.' );
}

/**
* Tests that `WP_Media_List_Table::handle_row_actions()` strips HTML from the
* attachment title used within the row action `aria-label` attributes.
*
* The title is escaped by `_draft_or_post_title()`, so any HTML it contains
* survives `esc_attr()` and is announced as literal text by screen readers.
*
* @ticket 65729
*
* @covers WP_Media_List_Table::handle_row_actions
*/
public function test_handle_row_actions_should_strip_html_from_aria_labels() {
wp_set_current_user( self::$admin );
self::set_is_trash( false );

$attachment = self::factory()->attachment->create_and_get(
array(
'post_title' => 'my<div>image',
'file' => 'image.jpg',
'post_mime_type' => 'image/jpeg',
)
);

$GLOBALS['post'] = $attachment;

$handle_row_actions = new ReflectionMethod( self::$list_table, 'handle_row_actions' );
if ( PHP_VERSION_ID < 80100 ) {
$handle_row_actions->setAccessible( true );
}
$output = $handle_row_actions->invoke( self::$list_table, $attachment, 'title', 'title' );
if ( PHP_VERSION_ID < 80100 ) {
$handle_row_actions->setAccessible( false );
}

unset( $GLOBALS['post'] );

$this->assertStringNotContainsString( '&lt;div&gt;', $output, 'The escaped HTML was not stripped from the title.' );
$this->assertStringContainsString( 'aria-label="Edit &#8220;myimage&#8221;"', $output, 'The "edit" aria-label did not contain the stripped title.' );
}

/**
* Sets the `$is_trash` property.
*
Expand Down
40 changes: 40 additions & 0 deletions tests/phpunit/tests/admin/wpPostsListTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -596,4 +596,44 @@ public function test_checkbox_label_includes_no_title_excerpt() {

$this->assertStringContainsString( 'Select (no title) Hello world example excerpt.', $output );
}

/**
* Tests that `WP_Posts_List_Table::handle_row_actions()` strips HTML from the
* post title used within the row action `aria-label` attributes.
*
* The title is escaped by `_draft_or_post_title()`, so any HTML it contains
* survives `esc_attr()` and is announced as literal text by screen readers.
*
* @ticket 65729
*
* @covers WP_Posts_List_Table::handle_row_actions
*/
public function test_handle_row_actions_should_strip_html_from_aria_labels() {
wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );

$post = self::factory()->post->create_and_get(
array(
'post_title' => 'my<div>post',
'post_status' => 'publish',
)
);

$GLOBALS['post'] = $post;

$table = _get_list_table( 'WP_Posts_List_Table', array( 'screen' => 'edit-post' ) );

$handle_row_actions = new ReflectionMethod( $table, 'handle_row_actions' );
if ( PHP_VERSION_ID < 80100 ) {
$handle_row_actions->setAccessible( true );
}
$output = $handle_row_actions->invoke( $table, $post, 'title', 'title' );
if ( PHP_VERSION_ID < 80100 ) {
$handle_row_actions->setAccessible( false );
}

unset( $GLOBALS['post'] );

$this->assertStringNotContainsString( '&lt;div&gt;', $output, 'The escaped HTML was not stripped from the title.' );
$this->assertStringContainsString( 'aria-label="Edit &#8220;mypost&#8221;"', $output, 'The "edit" aria-label did not contain the stripped title.' );
}
}
Loading