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
4 changes: 2 additions & 2 deletions src/wp-admin/comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,9 @@
$post_id = $comment->comment_post_ID;
if ( current_user_can( 'edit_post', $post_id ) ) {
$post_link = "<a href='" . esc_url( get_edit_post_link( $post_id ) ) . "'>";
$post_link .= esc_html( get_the_title( $post_id ) ) . '</a>';
$post_link .= wp_kses_post_title( get_the_title( $post_id ) ) . '</a>';
} else {
$post_link = esc_html( get_the_title( $post_id ) );
$post_link = wp_kses_post_title( get_the_title( $post_id ) );
}
echo $post_link;

Expand Down
4 changes: 2 additions & 2 deletions src/wp-admin/edit-form-comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,9 @@
$post_id = $comment->comment_post_ID;
if ( current_user_can( 'edit_post', $post_id ) ) {
$post_link = "<a href='" . esc_url( get_edit_post_link( $post_id ) ) . "'>";
$post_link .= esc_html( get_the_title( $post_id ) ) . '</a>';
$post_link .= wp_kses_post_title( get_the_title( $post_id ) ) . '</a>';
} else {
$post_link = esc_html( get_the_title( $post_id ) );
$post_link = wp_kses_post_title( get_the_title( $post_id ) );
}
?>

Expand Down
4 changes: 2 additions & 2 deletions src/wp-admin/includes/class-wp-comments-list-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -1123,9 +1123,9 @@ public function column_response( $comment ) {

if ( current_user_can( 'edit_post', $post->ID ) ) {
$post_link = "<a href='" . get_edit_post_link( $post->ID ) . "' class='comments-edit-item-link'>";
$post_link .= esc_html( get_the_title( $post->ID ) ) . '</a>';
$post_link .= wp_kses_post_title( get_the_title( $post->ID ) ) . '</a>';
} else {
$post_link = esc_html( get_the_title( $post->ID ) );
$post_link = wp_kses_post_title( get_the_title( $post->ID ) );
}

echo '<div class="response-links">';
Expand Down
15 changes: 9 additions & 6 deletions src/wp-admin/includes/class-wp-media-list-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ public function column_cb( $item ) {
<span class="screen-reader-text">
<?php
/* translators: Hidden accessibility text. %s: Attachment title. */
printf( __( 'Select %s' ), _draft_or_post_title() );
printf( __( 'Select %s' ), _draft_or_post_title( $post ) );
?>
</span>
</label>
Expand Down Expand Up @@ -483,7 +483,7 @@ public function column_title( $post ) {
}
}

$title = _draft_or_post_title();
$title = _draft_or_post_title( $post );
$thumb = wp_get_attachment_image( $attachment_id, array( 60, 60 ), true, array( 'alt' => '' ) );
$link_start = '';
$link_end = '';
Expand Down Expand Up @@ -653,7 +653,7 @@ public function column_parent( $post ) {
'<br /><a href="#the-list" onclick="findPosts.open( \'media[]\', \'%s\' ); return false;" class="hide-if-no-js aria-button-if-js" aria-label="%s">%s</a>',
$post->ID,
/* translators: %s: Attachment title. */
esc_attr( sprintf( __( 'Attach &#8220;%s&#8221; to existing content' ), $title ) ),
esc_attr( sprintf( __( 'Attach &#8220;%s&#8221; to existing content' ), wp_strip_all_tags( $title ) ) ),
__( 'Attach' )
);
}
Expand Down Expand Up @@ -759,7 +759,7 @@ public function display_rows() {

$this->comment_pending_count = get_pending_comments_num( $post_ids );

add_filter( 'the_title', 'esc_html' );
add_filter( 'the_title', 'wp_kses_post_title' );

while ( have_posts() ) :
the_post();
Expand All @@ -777,6 +777,8 @@ public function display_rows() {
</tr>
<?php
endwhile;

remove_filter( 'the_title', 'wp_kses_post_title' );
}

/**
Expand All @@ -800,7 +802,8 @@ protected function get_default_primary_column_name() {
* @return array<string, string> An array of row actions.
*/
private function _get_row_actions( $post, $att_title ) {
$actions = array();
$att_title = wp_strip_all_tags( $att_title );
$actions = array();

if ( ! $this->is_trash && current_user_can( 'edit_post', $post->ID ) ) {
$actions['edit'] = sprintf(
Expand Down Expand Up @@ -927,7 +930,7 @@ 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();
$att_title = _draft_or_post_title( $post );
$actions = $this->_get_row_actions( $post, $att_title );

return $this->row_actions( $actions );
Expand Down
23 changes: 17 additions & 6 deletions src/wp-admin/includes/class-wp-posts-list-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -798,13 +798,15 @@ public function display_rows( $posts = array(), $level = 0 ) {
$posts = $wp_query->posts;
}

add_filter( 'the_title', 'esc_html' );
add_filter( 'the_title', 'wp_kses_post_title' );

if ( $this->hierarchical_display ) {
$this->_display_rows_hierarchical( $posts, $this->get_pagenum(), $per_page );
} else {
$this->_display_rows( $posts, $level );
}

remove_filter( 'the_title', 'wp_kses_post_title' );
}

/**
Expand Down Expand Up @@ -1064,7 +1066,7 @@ public function column_cb( $item ) {
*/
if ( apply_filters( 'wp_list_table_show_post_checkbox', $show, $post ) ) :

$post_title = _draft_or_post_title();
$post_title = _draft_or_post_title( $post );

// If the post has no title, try adding part of the excerpt.
$no_title_excerpt = $this->get_no_title_excerpt( $post );
Expand Down Expand Up @@ -1127,7 +1129,16 @@ protected function _column_title( $post, $classes, $data, $primary ) {
* @return string The post title, or 'no title' if no title.
*/
protected function get_primary_column_aria_label( $item ) {
return ! empty( $item->post_title ) ? $item->post_title : __( 'no title' );
if ( empty( $item->post_title ) ) {
return __( 'no title' );
}

// Keep screen-reader labels as plain text, matching media list table behavior.
$title = wp_strip_all_tags( $item->post_title );
$title = html_entity_decode( $title, ENT_QUOTES, get_bloginfo( 'charset' ) );
$title = trim( $title );

return '' !== $title ? $title : __( 'no title' );
}

/**
Expand Down Expand Up @@ -1218,7 +1229,7 @@ public function column_title( $post ) {

echo '<strong>';

$title = _draft_or_post_title();
$title = _draft_or_post_title( $post );

// If the post has no title, try adding part of the excerpt.
$no_title_excerpt = $this->get_no_title_excerpt( $post );
Expand Down Expand Up @@ -1247,7 +1258,7 @@ public function column_title( $post ) {

if ( isset( $parent_name ) ) {
$post_type_object = get_post_type_object( $post->post_type );
echo ' | ' . $post_type_object->labels->parent_item_colon . ' ' . esc_html( $parent_name );
echo ' | ' . $post_type_object->labels->parent_item_colon . ' ' . wp_kses_post_title( $parent_name );
}

echo "</strong>\n";
Expand Down Expand Up @@ -1571,7 +1582,7 @@ 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();
$title = wp_strip_all_tags( _draft_or_post_title( $post ) );

if ( $can_edit_post && 'trash' !== $post->post_status ) {
$actions['edit'] = sprintf(
Expand Down
6 changes: 3 additions & 3 deletions src/wp-admin/includes/dashboard.php
Original file line number Diff line number Diff line change
Expand Up @@ -721,8 +721,8 @@ function wp_dashboard_recent_drafts( $drafts = false ) {
'<div class="draft-title"><a href="%s" aria-label="%s">%s</a><time datetime="%s">%s</time></div>',
esc_url( $url ),
/* translators: %s: Post title. */
esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;' ), $title ) ),
esc_html( $title ),
esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;' ), wp_strip_all_tags( $title ) ) ),
$title,
get_the_time( 'c', $draft ),
get_the_time( __( 'F j, Y' ), $draft )
);
Expand Down Expand Up @@ -1267,7 +1267,7 @@ function wp_dashboard_recent_posts( $args ) {
sprintf( _x( '%1$s, %2$s', 'dashboard' ), $date, get_the_time() ),
$recent_post_link,
/* translators: %s: Post title. */
esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;' ), $draft_or_post_title ) ),
esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;' ), wp_strip_all_tags( $draft_or_post_title ) ) ),
$draft_or_post_title
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/wp-admin/includes/template.php
Original file line number Diff line number Diff line change
Expand Up @@ -2107,6 +2107,7 @@ function the_post_password() {
* returned.
*
* @since 2.7.0
* @since 7.2.0 Permits a subset of formatting tags instead of encoding all HTML.
*
* @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post.
* @return string The post title if set.
Expand All @@ -2116,7 +2117,7 @@ function _draft_or_post_title( $post = 0 ) {
if ( empty( $title ) ) {
$title = __( '(no title)' );
}
return esc_html( $title );
return wp_kses_post_title( $title );
}

/**
Expand Down
25 changes: 25 additions & 0 deletions src/wp-includes/post-template.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,31 @@ function get_the_title( $post = 0 ) {
return apply_filters( 'the_title', $post_title, $post_id );
}

/**
* Sanitizes a post title for HTML display, allowing limited formatting tags.
*
* WordPress permits HTML in post titles. `esc_html()` encodes those tags, so a
* title such as `The <em>page</em> title` is shown with the tags as text.
* This keeps a small set of inline formatting tags and escapes everything else.
*
* @since 7.2.0
*
* @param string $title Post title to sanitize.
* @return string Sanitized title safe for HTML display.
*/
function wp_kses_post_title( $title ) {
return wp_kses(
$title,
array(
'strong' => array( 'class' => true ),
'em' => array( 'class' => true ),
'b' => array( 'class' => true ),
'i' => array( 'class' => true ),
'span' => array( 'class' => true ),
)
);
}

/**
* Displays the Post Global Unique Identifier (guid).
*
Expand Down
44 changes: 44 additions & 0 deletions tests/phpunit/tests/admin/wpPostsListTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -720,4 +720,48 @@ public function test_checkbox_label_includes_no_title_excerpt() {

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

/**
* Formatting tags in a post title are rendered in the list table title column.
*
* @ticket 66244
*
* @covers ::wp_kses_post_title
* @covers ::_draft_or_post_title
* @covers WP_Posts_List_Table::column_title
* @covers WP_Posts_List_Table::get_primary_column_aria_label
*/
public function test_post_title_formatting_tags_are_rendered_in_list_table() {
$user_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
if ( is_multisite() ) {
grant_super_admin( $user_id );
}
wp_set_current_user( $user_id );

$post = self::factory()->post->create_and_get(
array(
'post_type' => 'post',
'post_title' => 'The <em class="title">page</em> title',
)
);

$output = $this->render_column_title( $post, 'list' );

$this->assertStringContainsString(
sprintf( '<a class="row-title" href="%s">The <em class="title">page</em> title</a>', get_edit_post_link( $post->ID ) ),
$output
);
$this->assertStringNotContainsString(
sprintf( '<a class="row-title" href="%s">The &lt;em', get_edit_post_link( $post->ID ) ),
$output
);

$table = _get_list_table( 'WP_Posts_List_Table', array( 'screen' => 'edit-post' ) );
$method = new ReflectionMethod( $table, 'get_primary_column_aria_label' );
if ( PHP_VERSION_ID < 80100 ) {
$method->setAccessible( true );
}

$this->assertSame( 'The page title', $method->invoke( $table, $post ) );
}
}
35 changes: 35 additions & 0 deletions tests/phpunit/tests/kses.php
Original file line number Diff line number Diff line change
Expand Up @@ -2995,4 +2995,39 @@ public function data_allowed_attributes_in_descriptions() {
),
);
}

/**
* @ticket 66244
* @covers ::wp_kses_post_title
*/
public function test_wp_kses_post_title_allows_formatting_tags() {
$title = 'The <em class="title">page</em> <strong>title</strong>';

$this->assertSame( $title, wp_kses_post_title( $title ) );
}

/**
* @ticket 66244
* @covers ::wp_kses_post_title
*/
public function test_wp_kses_post_title_strips_disallowed_tags_and_encodes_ampersands() {
$result = wp_kses_post_title( 'The <a href="https://example.com">page</a> & title <script>alert(1)</script>' );

$this->assertStringNotContainsString( '<a ', $result );
$this->assertStringNotContainsString( '<script', $result );
$this->assertStringContainsString( 'page', $result );
$this->assertStringContainsString( '&amp; title', $result );
$this->assertStringContainsString( 'alert(1)', $result );
}

/**
* @ticket 66244
* @covers ::wp_kses_post_title
*/
public function test_wp_kses_post_title_strips_disallowed_attributes() {
$this->assertSame(
'<em class="title">page</em>',
wp_kses_post_title( '<em class="title" onclick="alert(1)">page</em>' )
);
}
}
Loading