Skip to content

Commit 17c99e5

Browse files
committed
HTML API: Preserve raw text contents in serialization.
The contents of `iframe`, `noembed`, and `noframes` elements are parsed as raw text and must be appended literally when serializing rather than escaped or omitted. Setting the modifiable text of these elements, as well as `xmp`, is now supported. Developed in WordPress#12289. Follow-up to [62542]. Props jonsurrell, dmsnell. See #65372. git-svn-id: https://develop.svn.wordpress.org/trunk@62715 602fd350-edb4-49c9-b593-d223f7449a82
1 parent c2b126a commit 17c99e5

4 files changed

Lines changed: 72 additions & 23 deletions

File tree

src/wp-includes/html-api/class-wp-html-processor.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,9 +1511,6 @@ public function serialize_token(): string {
15111511
case 'IFRAME':
15121512
case 'NOEMBED':
15131513
case 'NOFRAMES':
1514-
$text = '';
1515-
break;
1516-
15171514
case 'SCRIPT':
15181515
case 'STYLE':
15191516
case 'XMP':

src/wp-includes/html-api/class-wp-html-tag-processor.php

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@
298298
* closing the SCRIPT from inside a JavaScript string. E.g. `console.log( '</script>' )`.
299299
* - `TITLE` and `TEXTAREA` whose contents are treated as plaintext and then any
300300
* character references are decoded. E.g. `1 &lt; 2 < 3` becomes `1 < 2 < 3`.
301-
* - `IFRAME`, `NOEMBED`, `NOFRAMES`, `STYLE` whose contents are treated as
301+
* - `IFRAME`, `NOEMBED`, `NOFRAMES`, `STYLE`, `XMP` whose contents are treated as
302302
* raw plaintext and left as-is. E.g. `1 &lt; 2 < 3` remains `1 &lt; 2 < 3`.
303303
*
304304
* #### Other tokens with modifiable text.
@@ -4075,6 +4075,32 @@ public function set_modifiable_text( string $plaintext_content ): bool {
40754075
);
40764076
return true;
40774077

4078+
case 'IFRAME':
4079+
case 'NOEMBED':
4080+
case 'NOFRAMES':
4081+
case 'XMP':
4082+
$tag_name = $this->get_tag();
4083+
if ( false !== stripos( $plaintext_content, "</{$tag_name}" ) ) {
4084+
_doing_it_wrong(
4085+
__METHOD__,
4086+
sprintf(
4087+
/* translators: %s: HTML tag name. */
4088+
__( '%s text cannot contain its own closing tag.' ),
4089+
$tag_name
4090+
),
4091+
'7.1.0'
4092+
);
4093+
return false;
4094+
}
4095+
4096+
$this->lexical_updates['modifiable text'] = new WP_HTML_Text_Replacement(
4097+
$this->text_starts_at,
4098+
$this->text_length,
4099+
$plaintext_content
4100+
);
4101+
4102+
return true;
4103+
40784104
case 'STYLE':
40794105
$plaintext_content = preg_replace_callback(
40804106
'~</(?P<TAG_NAME>style)~i',
@@ -4132,7 +4158,7 @@ static function ( $tag_match ) {
41324158

41334159
_doing_it_wrong(
41344160
__METHOD__,
4135-
__( 'Only the SCRIPT, STYLE, TEXTAREA, and TITLE tags support setting modifiable text.' ),
4161+
__( 'This tag does not support setting modifiable text.' ),
41364162
'7.1.0'
41374163
);
41384164
return false;

tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -270,22 +270,6 @@ public function test_style_contents_are_not_escaped() {
270270
);
271271
}
272272

273-
/**
274-
* XMP contents are parsed using the generic raw text element parsing algorithm.
275-
* Their contents should not be escaped with HTML character references on normalization.
276-
*
277-
* @ticket 65372
278-
*/
279-
public function test_xmp_contents_are_not_escaped() {
280-
$normalized = WP_HTML_Processor::normalize( "<xmp> < > & \" ' \x00 </xmp>" );
281-
282-
$this->assertSame(
283-
"<xmp> < > & \" ' \u{FFFD} </xmp>",
284-
$normalized,
285-
'Should have preserved text inside an XMP element, except for replacing NULL bytes.'
286-
);
287-
}
288-
289273
public function test_unexpected_closing_tags_are_removed() {
290274
$this->assertSame(
291275
WP_HTML_Processor::normalize( 'one</div>two</span>three' ),
@@ -481,9 +465,9 @@ public function test_replaces_null_bytes_appropriately( string $html_with_nulls,
481465
/**
482466
* Data provider.
483467
*
484-
* @return array[]
468+
* @return array<string, array{string, string}>
485469
*/
486-
public static function data_tokens_with_null_bytes() {
470+
public static function data_tokens_with_null_bytes(): array {
487471
return array(
488472
'Tag name' => array( "<img\x00id=5>", "<img\u{FFFD}id=5></img\u{FFFD}id=5>" ),
489473
'Attribute name' => array( "<img/\x00id=5>", "<img \u{FFFD}id=\"5\">" ),
@@ -492,11 +476,45 @@ public static function data_tokens_with_null_bytes() {
492476
'Foreign content text' => array( "<svg>one\x00two</svg>", "<svg>one\u{FFFD}two</svg>" ),
493477
'SCRIPT content' => array( "<script>alert(\x00)</script>", "<script>alert(\u{FFFD})</script>" ),
494478
'STYLE content' => array( "<style>\x00 {}</style>", "<style>\u{FFFD} {}</style>" ),
479+
'IFRAME content' => array( "<iframe>a\x00b</iframe>", "<iframe>a\u{FFFD}b</iframe>" ),
480+
'NOEMBED content' => array( "<noembed>a\x00b</noembed>", "<noembed>a\u{FFFD}b</noembed>" ),
481+
'NOFRAMES content' => array( "<noframes>a\x00b</noframes>", "<noframes>a\u{FFFD}b</noframes>" ),
495482
'XMP content' => array( "<xmp>a\x00b</xmp>", "<xmp>a\u{FFFD}b</xmp>" ),
496483
'Comment text' => array( "<!-- \x00 -->", "<!-- \u{FFFD} -->" ),
497484
);
498485
}
499486

487+
/**
488+
* Ensures that contents of rawtext elements are preserved when serializing.
489+
*
490+
* @ticket 65372
491+
*
492+
* @dataProvider data_rawtext_elements_with_html_syntax_character_contents
493+
*
494+
* @param string $html Normalized HTML containing a rawtext element with contents.
495+
*/
496+
public function test_rawtext_element_contents_are_preserved_when_normalizing( string $html ): void {
497+
$this->assertSame(
498+
$html,
499+
WP_HTML_Processor::normalize( $html ),
500+
'Should have preserved the rawtext element contents.'
501+
);
502+
}
503+
504+
/**
505+
* Data provider.
506+
*
507+
* @return array<string, array{string}>
508+
*/
509+
public static function data_rawtext_elements_with_html_syntax_character_contents(): array {
510+
return array(
511+
'IFRAME' => array( 'before<iframe> < > &amp; " \' </iframe>after' ),
512+
'NOEMBED' => array( 'before<noembed> < > &amp; " \' </noembed>after' ),
513+
'NOFRAMES' => array( 'before<noframes> < > &amp; " \' </noframes>after' ),
514+
'XMP' => array( 'before<xmp> < > &amp; " \' </xmp>after' ),
515+
);
516+
}
517+
500518
/**
501519
* @ticket 62396
502520
*

tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,10 @@ public static function data_tokens_with_basic_modifiable_text_updates() {
434434
'PI node (no separator)' => array( '<?wp-bit?>', 1, '{"just": "kidding"}', '<?wp-bit {"just": "kidding"}?>' ),
435435
'Text node (end)' => array( '<img>of a dog', 2, 'of a cat', '<img>of a cat' ),
436436
'Encoded text node' => array( '<figcaption>birds and dogs</figcaption>', 2, '<birds> & <dogs>', '<figcaption>&lt;birds&gt; &amp; &lt;dogs&gt;</figcaption>' ),
437+
'IFRAME tag' => array( 'before<iframe>old content</iframe>after', 2, '<p>raw & text</p>', 'before<iframe><p>raw & text</p></iframe>after' ),
438+
'NOEMBED tag' => array( 'before<noembed>old content</noembed>after', 2, '<p>raw & text</p>', 'before<noembed><p>raw & text</p></noembed>after' ),
439+
'NOFRAMES tag' => array( 'before<noframes>old content</noframes>after', 2, '<p>raw & text</p>', 'before<noframes><p>raw & text</p></noframes>after' ),
440+
'XMP tag' => array( 'before<xmp>old content</xmp>after', 2, '<p>raw & text</p>', 'before<xmp><p>raw & text</p></xmp>after' ),
437441
'SCRIPT tag' => array( 'before<script></script>after', 2, 'const img = "<img> & <br>";', 'before<script>const img = "<img> & <br>";</script>after' ),
438442
'STYLE tag' => array( '<style></style>', 1, 'p::before { content: "<img> & </style>"; }', '<style>p::before { content: "<img> & \3c\2fstyle>"; }</style>' ),
439443
'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" ),
@@ -624,6 +628,10 @@ public static function data_unallowed_modifiable_text_updates() {
624628
return array(
625629
'Comment with -->' => array( '<!-- this is a comment -->', 'Comments end in -->' ),
626630
'Comment with --!>' => array( '<!-- this is a comment -->', 'Invalid but legitimate comments end in --!>' ),
631+
'IFRAME with </iframe>' => array( '<iframe>Replace me</iframe>', 'Just a </iframe>' ),
632+
'NOEMBED with </NOEMBED>' => array( '<noembed>Replace me</noembed>', 'Just a </NOEMBED>' ),
633+
'NOFRAMES with </noframes attributes>' => array( '<noframes>Replace me</noframes>', 'before</noframes sneaky>after' ),
634+
'XMP with </xmp>' => array( '<xmp>Replace me</xmp>', 'Just a </xmp>' ),
627635
'PI with >' => array( '<?wp-bit some data?>', 'Processing instructions end at the first >' ),
628636
'PI with leading space' => array( '<?wp-bit some data?>', ' leading whitespace is skipped after the target' ),
629637
'PI with leading tab' => array( '<?wp-bit some data?>', "\tleading whitespace is skipped after the target" ),

0 commit comments

Comments
 (0)