Skip to content
Draft
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
65 changes: 55 additions & 10 deletions src/wp-includes/html-api/class-wp-html-tag-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -3851,20 +3851,11 @@ public function set_modifiable_text( string $plaintext_content ): bool {
return true;

case 'STYLE':
$plaintext_content = preg_replace_callback(
'~</(?P<TAG_NAME>style)~i',
static function ( $tag_match ) {
return "\\3c\\2f{$tag_match['TAG_NAME']}";
},
$plaintext_content
);

$this->lexical_updates['modifiable text'] = new WP_HTML_Text_Replacement(
$this->text_starts_at,
$this->text_length,
$plaintext_content
self::escape_style_contents( $plaintext_content )
);

return true;

case 'TEXTAREA':
Expand Down Expand Up @@ -4240,6 +4231,60 @@ private static function escape_javascript_script_contents( string $sourcecode ):
return $escaped;
}

/**
* Escape style tag contents.
*
* Prevent CSS text from modifying the HTML structure of a document and
* ensure that it's contained within its enclosing STYLE tag as intended.
*
* @since 7.0.0
*
* @param string $text Raw contents intended to be serialized into an HTML STYLE element.
* @return string Escaped form of input contents which will not lead to premature closing of the containing STYLE element.
*/
private static function escape_style_contents( string $text ): string {
$at = 0;
$was_at = 0;
$end = strlen( $text );
$escaped = '';

/*
* Replace all instances of the ASCII case-insensitive match of "</style"
* when followed by whitespace or "/" or ">", by using a CSS Unicode
* escape sequence for the "s" (or the "S").
*
* CSS Unicode escape sequences will terminate at the first non-hexadecimal,
* so the `t` character in `style` ensures that a Unicode escape sequence
* like `\73t` is correctly interpreted as `st`.
*/
while ( $at < $end ) {
$tag_at = stripos( $text, '</style', $at );
if ( false === $tag_at ) {
break;
}

if ( 1 !== strspn( $text, " \t\f\r\n/>", $tag_at + 7, 1 ) ) {
$at = $tag_at + 7;
continue;
}

$escaped .= substr( $text, $was_at, $tag_at - $was_at + 2 );
$escaped .= 's' === $text[ $tag_at + 2 ] ? '\73' : '\53';
$was_at = $tag_at + 3;
$at = $tag_at + 8;
}
Comment on lines +4251 to +4275
Copy link
Member

Choose a reason for hiding this comment

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

Would there be an opportunity to factor this out into a common method to be reused by both escape_style_contents and escape_javascript_script_contents?


if ( '' === $escaped ) {
return $text;
}

if ( $was_at < $end ) {
$escaped .= substr( $text, $was_at );
}

return $escaped;
}

/**
* Updates or creates a new attribute on the currently matched tag with the passed value.
*
Expand Down
28 changes: 16 additions & 12 deletions tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php
Original file line number Diff line number Diff line change
Expand Up @@ -426,18 +426,22 @@ public function test_updates_basic_modifiable_text_on_supported_nodes( string $h
*/
public static function data_tokens_with_basic_modifiable_text_updates() {
return array(
'Text node (start)' => array( 'Text', 1, 'Blubber', 'Blubber' ),
'Text node (middle)' => array( '<em>Bold move</em>', 2, 'yo', '<em>yo</em>' ),
'Text node (end)' => array( '<img>of a dog', 2, 'of a cat', '<img>of a cat' ),
'Encoded text node' => array( '<figcaption>birds and dogs</figcaption>', 2, '<birds> & <dogs>', '<figcaption>&lt;birds&gt; &amp; &lt;dogs&gt;</figcaption>' ),
'SCRIPT tag' => array( 'before<script></script>after', 2, 'const img = "<img> & <br>";', 'before<script>const img = "<img> & <br>";</script>after' ),
'STYLE tag' => array( '<style></style>', 1, 'p::before { content: "<img> & </style>"; }', '<style>p::before { content: "<img> & \3c\2fstyle>"; }</style>' ),
'TEXTAREA tag' => array( 'a<textarea>has no need to escape</textarea>b', 2, "so it <doesn't>", "a<textarea>so it <doesn't></textarea>b" ),
'TEXTAREA (escape)' => array( 'a<textarea>has no need to escape</textarea>b', 2, 'but it does for </textarea>', 'a<textarea>but it does for &lt;/textarea></textarea>b' ),
'TEXTAREA (escape+attrs)' => array( 'a<textarea>has no need to escape</textarea>b', 2, 'but it does for </textarea not an="attribute">', 'a<textarea>but it does for &lt;/textarea not an="attribute"></textarea>b' ),
'TITLE tag' => array( 'a<title>has no need to escape</title>b', 2, "so it <doesn't>", "a<title>so it <doesn't></title>b" ),
'TITLE (escape)' => array( 'a<title>has no need to escape</title>b', 2, 'but it does for </title>', 'a<title>but it does for &lt;/title></title>b' ),
'TITLE (escape+attrs)' => array( 'a<title>has no need to escape</title>b', 2, 'but it does for </title not an="attribute">', 'a<title>but it does for &lt;/title not an="attribute"></title>b' ),
'Text node (start)' => array( 'Text', 1, 'Blubber', 'Blubber' ),
'Text node (middle)' => array( '<em>Bold move</em>', 2, 'yo', '<em>yo</em>' ),
'Text node (end)' => array( '<img>of a dog', 2, 'of a cat', '<img>of a cat' ),
'Encoded text node' => array( '<figcaption>birds and dogs</figcaption>', 2, '<birds> & <dogs>', '<figcaption>&lt;birds&gt; &amp; &lt;dogs&gt;</figcaption>' ),
'SCRIPT tag' => array( 'before<script></script>after', 2, 'const img = "<img> & <br>";', 'before<script>const img = "<img> & <br>";</script>after' ),
'STYLE tag' => array( '<style></style>', 1, 'p::before { content: "<img> & </style>"; }', '<style>p::before { content: "<img> & </\73tyle>"; }</style>' ),
'STYLE tag (mixed casing)' => array( '<style></style>', 1, 'p::before { content: "<img> & </StYlE>"; }', '<style>p::before { content: "<img> & </\53tYlE>"; }</style>' ),
'STYLE tag (trailing characters)' => array( '<style></style>', 1, "p::before { content: \"<img> & </style\t>\"; }", "<style>p::before { content: \"<img> & </\\73tyle\t>\"; }</style>" ),
'STYLE tag (non-closing tag)' => array( '<style></style>', 1, 'p::before { content: "<img> & </stylesheet>"; }', '<style>p::before { content: "<img> & </stylesheet>"; }</style>' ),
'STYLE tag (repeats)' => array( '<style></style>', 1, '*{ content: "</style></STYLE></style</style>" }', '<style>*{ content: "</\73tyle></\53TYLE></style</\73tyle>" }</style>' ),
'TEXTAREA tag' => array( 'a<textarea>has no need to escape</textarea>b', 2, "so it <doesn't>", "a<textarea>so it <doesn't></textarea>b" ),
'TEXTAREA (escape)' => array( 'a<textarea>has no need to escape</textarea>b', 2, 'but it does for </textarea>', 'a<textarea>but it does for &lt;/textarea></textarea>b' ),
'TEXTAREA (escape+attrs)' => array( 'a<textarea>has no need to escape</textarea>b', 2, 'but it does for </textarea not an="attribute">', 'a<textarea>but it does for &lt;/textarea not an="attribute"></textarea>b' ),
'TITLE tag' => array( 'a<title>has no need to escape</title>b', 2, "so it <doesn't>", "a<title>so it <doesn't></title>b" ),
'TITLE (escape)' => array( 'a<title>has no need to escape</title>b', 2, 'but it does for </title>', 'a<title>but it does for &lt;/title></title>b' ),
'TITLE (escape+attrs)' => array( 'a<title>has no need to escape</title>b', 2, 'but it does for </title not an="attribute">', 'a<title>but it does for &lt;/title not an="attribute"></title>b' ),
);
}

Expand Down
Loading