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
6 changes: 4 additions & 2 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,11 @@
add_filter( 'wp_get_custom_css', 'wp_replace_insecure_home_url' );

// RSS filters.
add_filter( 'the_title_rss', 'strip_tags' );

add_filter( 'the_title_rss', 'ent2ncr', 8 );
add_filter( 'the_title_rss', 'esc_html' );
add_filter( 'the_title_rss', 'wp_encode_bare_lt', 9 );
add_filter( 'the_title_rss', 'strip_tags', 10 );
add_filter( 'the_title_rss', 'esc_html', 11 );
Comment on lines +259 to +261
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is reason behind adding the priorities here?

Suggested change
add_filter( 'the_title_rss', 'wp_encode_bare_lt', 9 );
add_filter( 'the_title_rss', 'strip_tags', 10 );
add_filter( 'the_title_rss', 'esc_html', 11 );
add_filter( 'the_title_rss', 'wp_encode_bare_lt' );
add_filter( 'the_title_rss', 'strip_tags' );
add_filter( 'the_title_rss', 'esc_html' );

Does this will not works?

add_filter( 'the_content_rss', 'ent2ncr', 8 );
add_filter( 'the_content_feed', 'wp_staticize_emoji' );
add_filter( 'the_content_feed', '_oembed_filter_feed_content' );
Expand Down
23 changes: 23 additions & 0 deletions src/wp-includes/formatting.php
Original file line number Diff line number Diff line change
Expand Up @@ -5509,6 +5509,29 @@ function normalize_whitespace( $str ) {
return $str;
}

/**
* Encodes bare `<` characters (those not starting an HTML tag) as `&lt;`.
*
* @since x.x.x
*
* @param string $text The text to encode.
* @return string Text with bare `<` characters encoded.
*/
function wp_encode_bare_lt( $text ) {
/*
* A valid HTML tag begins with `<` followed by:
* - a letter (opening tag, e.g. `<strong>`)
* - `/` (closing tag, e.g. `</strong>`)
* - `!` (comment or doctype, e.g. `<!-- … -->`)
* - `?` (processing instruction, e.g. `<?xml … ?>`)
*
* Any `<` not followed by one of these is a bare less-than sign and is
* encoded as `&lt;` so that strip_tags() does not silently discard it.
*/
$encoded = preg_replace( '#<(?![a-zA-Z/!?])#', '&lt;', $text );
return null !== $encoded ? $encoded : $text;
}

/**
* Properly strips all HTML tags including 'script' and 'style'.
*
Expand Down
26 changes: 26 additions & 0 deletions tests/phpunit/tests/feed/atom.php
Original file line number Diff line number Diff line change
Expand Up @@ -294,4 +294,30 @@ public function test_atom_enclosure_with_extended_url_length_type_parsing() {
}
}
}

/**
* Tests that a bare `<` in a post title is encoded inside the Atom feed CDATA block, not stripped.
*
* Atom wraps `the_title_rss()` output in `<![CDATA[…]]>`. This test verifies
* the encoded `<` character survives into that block by writing the raw title
* directly to the database, bypassing kses sanitization on insert.
*
* @ticket 9993
*
* @covers ::get_the_title_rss
*/
public function test_atom_title_encodes_bare_lt_in_cdata() {
global $wpdb;

$post_id = self::factory()->post->create();

$wpdb->update( $wpdb->posts, array( 'post_title' => '& > test <' ), array( 'ID' => $post_id ) );
clean_post_cache( $post_id );

$this->go_to( '/?feed=atom&p=' . $post_id );
$feed = $this->do_atom();

$this->assertStringContainsString( 'test &lt;', $feed, 'Bare `<` was stripped from the Atom feed title CDATA block.' );
$this->assertStringNotContainsString( 'test ]]>', $feed, 'Bare `<` must not be stripped (leaving title truncated before `]]>`).' );
}
}
38 changes: 38 additions & 0 deletions tests/phpunit/tests/feed/rss2.php
Original file line number Diff line number Diff line change
Expand Up @@ -629,4 +629,42 @@ function ( $headers ) use ( $today ) {

$this->go_to( '/?feed=rss2&withcomments=1' );
}

/**
* Tests that the `the_title_rss` filter encodes bare `<` characters rather than stripping them.
*
* @ticket 9993
* @dataProvider data_title_rss_encodes_special_characters
*
* @covers ::get_the_title_rss
*/
public function test_title_rss_encodes_special_characters( $title, $expected ) {
$this->assertSame( $expected, apply_filters( 'the_title_rss', $title ) );
}

/**
* Data provider for test_title_rss_encodes_special_characters().
*
* @return array[]
*/
public static function data_title_rss_encodes_special_characters() {
return array(
'bare less-than at end is encoded not stripped' => array(
'& > test <',
'&amp; &gt; test &lt;',
),
'bare less-than in middle is encoded' => array(
'a < b',
'a &lt; b',
),
'html tags are still stripped by strip_tags' => array(
'<strong>bold</strong>',
'bold',
),
'plain text passes through unchanged' => array(
'Hello World',
'Hello World',
),
);
}
}
Loading