Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
4a8ca98
Auto-escape JavaScript and JSON script tags when necessary
sirreal Dec 15, 2025
4a28ef2
Update ticket number
sirreal Dec 15, 2025
0bef687
Fix those lints
sirreal Dec 15, 2025
cdba027
No trailing function commas
sirreal Dec 15, 2025
ba54ae4
Remove JSON_THROW_ON_ERROR constant
sirreal Dec 15, 2025
a697e9e
fixup! Remove JSON_THROW_ON_ERROR constant
sirreal Dec 15, 2025
2b3d0d0
Merge branch 'trunk' into html-api/auto-escape-javascript-json
sirreal Dec 16, 2025
8246439
Remove JSON +json subtype handling
sirreal Dec 16, 2025
1b6b4fd
Merge branch 'html-api/auto-escape-javascript-json' into scripts/use-…
sirreal Dec 16, 2025
b3e88e8
Add test for scripts containing script tags
sirreal Dec 16, 2025
27c6371
Update wp_add_inline_script to use HTML API
sirreal Dec 16, 2025
deebd54
Use the tag processor for all the inline script tags
sirreal Dec 16, 2025
d78cd35
Remove assertions
sirreal Dec 16, 2025
c29c3d9
PICKME: Update Script Modules tests to use assertEqualHTML
sirreal Dec 16, 2025
abeebd6
Revert "Remove assertions"
sirreal Dec 16, 2025
aaacd6f
Merge branch 'trunk' into scripts/use-html-api-for-script-tags
sirreal Dec 19, 2025
3f7f227
Merge branch 'trunk' into html-api/auto-escape-javascript-json
sirreal Dec 19, 2025
1362451
Add JS and JSON script tag tests
Copilot Dec 19, 2025
8d30680
Fix typo in comment
sirreal Dec 19, 2025
3b5ef4e
Improve comment
sirreal Dec 19, 2025
f8cfdf9
Clean up and fix tests
sirreal Dec 19, 2025
501d201
Clean up and improve type/language logic
sirreal Dec 19, 2025
253b971
Merge branch 'html-api/auto-escape-javascript-json' into scripts/use-…
sirreal Dec 19, 2025
bcc02ae
Fix svg SCRIPT tag tests
sirreal Dec 19, 2025
ea03441
Add todo on MIME type parsing
sirreal Dec 19, 2025
c7d1827
Update since tags 🤞
sirreal Dec 19, 2025
a134e82
Make is_{javascript,json}_script_tag methods private
sirreal Dec 19, 2025
d4bd4b3
Add language whitespace test
sirreal Dec 19, 2025
edae8d5
How'd that extra space get there, remove it!
sirreal Dec 19, 2025
02ca3c0
Name search parts
sirreal Dec 19, 2025
d058a78
Clean up escaping tests
sirreal Dec 19, 2025
dfb63af
Add ignore and todo tags to new private is_*_script_tag functions
sirreal Dec 19, 2025
11f51c9
Improve example comment
sirreal Dec 19, 2025
a3e0e27
Update regex comment with named groups
sirreal Dec 19, 2025
288b952
Add details to "other" failure to escape comment
sirreal Dec 19, 2025
a7495dd
Improve consistency of comment quoting and differentiate types of dou…
sirreal Dec 19, 2025
614916d
Describe JavaScript escaping strategy in detail
sirreal Dec 19, 2025
5828382
Merge branch 'html-api/auto-escape-javascript-json' into scripts/use-…
sirreal Dec 19, 2025
d8c320c
Describe JavaScript escaping strategy in detail
sirreal Dec 19, 2025
016a29f
Merge branch 'html-api/auto-escape-javascript-json' into scripts/use-…
sirreal Dec 19, 2025
369eefc
Use the updated_html value in round-trip test
sirreal Dec 19, 2025
d67749d
Merge branch 'html-api/auto-escape-javascript-json' into scripts/use-…
sirreal Dec 19, 2025
2869880
Revert changes to wp_add_inline_script
sirreal Dec 19, 2025
d6bfdca
Remove debugging assertions
sirreal Dec 19, 2025
b7099e4
fixup! Revert changes to wp_add_inline_script
sirreal Dec 19, 2025
010a2a2
Switch to assertEqualHTML assertions
sirreal Dec 19, 2025
ab53486
Update defer/async boolean attributes
sirreal Dec 19, 2025
3ba2267
Use Tag Processor in wp_get_script_Tag
sirreal Dec 19, 2025
64a23b7
Add utility for semantically comparing a SCRIPT tag within HTML
sirreal Dec 19, 2025
c920821
Use script tag comparison in tests
sirreal Dec 19, 2025
dbd9f55
Remove more async=async and defer=defer
sirreal Dec 19, 2025
7bef55a
fixup! Use script tag comparison in tests
sirreal Dec 19, 2025
4333300
fixup! Add utility for semantically comparing a SCRIPT tag within HTML
sirreal Dec 19, 2025
504a928
fix lint
sirreal Dec 19, 2025
55d4e47
Merge branch 'trunk' into html-api/auto-escape-javascript-json
sirreal Dec 22, 2025
1c84037
Merge branch 'html-api/auto-escape-javascript-json' into scripts/use-…
sirreal Dec 22, 2025
2ef0bf0
Fix demonstration comment
sirreal Dec 22, 2025
cb6b990
Extract and document escaping functions
sirreal Dec 22, 2025
da28eec
Merge branch 'html-api/auto-escape-javascript-json' into scripts/use-…
sirreal Dec 22, 2025
e399bf6
Tweak workaround documentation
sirreal Dec 22, 2025
83c1fab
Add ASCII chart about HTML script tags with original source
sirreal Dec 22, 2025
1872681
Improve linking between escapes
sirreal Dec 22, 2025
83ff62f
Fix comments, typos, lints
sirreal Dec 22, 2025
5979782
fixup! Fix comments, typos, lints
sirreal Dec 22, 2025
d469ae4
fixup! fixup! Fix comments, typos, lints
sirreal Dec 22, 2025
402ae9f
Fix \c -> \r (carriage return) typo
sirreal Dec 22, 2025
6b2c9ba
Add note about not parsing MIME types for JS script tags
sirreal Dec 22, 2025
eef0ccb
Add todo comment to is_json_script_tag
sirreal Dec 22, 2025
71d2686
Re-order tag name termination chars to match elsewhere
sirreal Dec 22, 2025
d4693a2
Fix typo
sirreal Dec 22, 2025
eb4e091
Merge branch 'trunk' into html-api/auto-escape-javascript-json
sirreal Dec 29, 2025
4c3b0b2
Update comments on tag prefixes matching search pattern
sirreal Dec 29, 2025
40015c9
Merge branch 'html-api/auto-escape-javascript-json' into scripts/use-…
sirreal Dec 29, 2025
bfa60cf
Use content-type identification and escape without PCRE
dmsnell Dec 30, 2025
3252160
fixup! Use content-type identification and escape without PCRE
dmsnell Dec 30, 2025
0b9b2bd
fixup! Use content-type identification and escape without PCRE
dmsnell Dec 30, 2025
b2533e1
fixup! Use content-type identification and escape without PCRE
dmsnell Dec 31, 2025
ddeb301
fixup! Use content-type identification and escape without PCRE
dmsnell Dec 31, 2025
b36fc32
fixup! Use content-type identification and escape without PCRE
dmsnell Dec 31, 2025
1249af0
fixup! Use content-type identification and escape without PCRE
dmsnell Dec 31, 2025
645cb45
Update escaping comment
sirreal Jan 8, 2026
f23b35b
Fix off-by-one repeat processing after script
sirreal Jan 8, 2026
31cefb7
Restructure escaping logic
sirreal Jan 8, 2026
25279e3
Merge and remove duplicate script tag content type tests
sirreal Jan 8, 2026
6660196
Merge branch 'trunk' into html-api/auto-escape-javascript-json
sirreal Jan 8, 2026
b436e71
Editing review of the escaping method docblock.
dmsnell Jan 9, 2026
95259d2
Fixup: Missed a couple of box drawing connections.
dmsnell Jan 9, 2026
91c019d
Fixup: Missed a curved corner
dmsnell Jan 9, 2026
09e3aa2
Tweak language in JS escaping comments
sirreal Jan 9, 2026
30d7747
Merge branch 'trunk' into html-api/auto-escape-javascript-json
sirreal Jan 12, 2026
c1c6067
Merge branch 'trunk' into html-api/auto-escape-javascript-json
sirreal Jan 12, 2026
292614d
Merge branch 'html-api/auto-escape-javascript-json' into scripts/use-…
sirreal Jan 12, 2026
6a6e1cc
Append trailing newline to output HTML not input
sirreal Jan 12, 2026
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
379 changes: 358 additions & 21 deletions src/wp-includes/html-api/class-wp-html-tag-processor.php

Large diffs are not rendered by default.

19 changes: 17 additions & 2 deletions src/wp-includes/script-loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -2872,7 +2872,14 @@ function wp_get_script_tag( $attributes ) {
*/
$attributes = apply_filters( 'wp_script_attributes', $attributes );

return sprintf( "<script%s></script>\n", wp_sanitize_script_attributes( $attributes ) );
$processor = new WP_HTML_Tag_Processor( '<script></script>' );
$processor->next_tag();
foreach ( $attributes as $name => $value ) {
if ( is_string( $value ) || true === $value ) {
$processor->set_attribute( $name, $value );
Copy link
Member

Choose a reason for hiding this comment

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

I appreciate this type guarding, but it feels a bit redundant. are we attempting to type-check the filter results or are we trying to guard against sending invalid types to set_attribute()?

it doesn’t currently type-check against a string, but we could add _doing_it_wrong() if we want to incur the runtime cost of type-checking there, which would seem okay.

I just want to question the intent here and the fact that this hides type errors and mistakes in the calling code by skipping over attributes which presumably were supposed to be set.

}
}
return "{$processor->get_updated_html()}\n";
}

/**
Expand Down Expand Up @@ -2916,7 +2923,15 @@ function wp_get_inline_script_tag( $data, $attributes = array() ) {
*/
$attributes = apply_filters( 'wp_inline_script_attributes', $attributes, $data );

return sprintf( "<script%s>%s</script>\n", wp_sanitize_script_attributes( $attributes ), $data );
$processor = new WP_HTML_Tag_Processor( '<script></script>' );
$processor->next_tag();
foreach ( $attributes as $name => $value ) {
if ( is_string( $value ) || true === $value ) {
$processor->set_attribute( $name, $value );
}
}
$processor->set_modifiable_text( $data );
return "{$processor->get_updated_html()}\n";
Copy link
Member

Choose a reason for hiding this comment

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

same question as above

}

/**
Expand Down
30 changes: 30 additions & 0 deletions tests/phpunit/data/html-api/script-element-escaping-diagram.dot
Copy link
Member

Choose a reason for hiding this comment

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

I'm curious why you chose DOT as opposed to Mermaid? I know that GitHub supports Mermaid.

In any case, I'm very impressed that you went to the effort of making a diagram to document this.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
digraph {
rankdir=TB;

// Entry point
entry [shape=plaintext label="Open script"];
entry -> script_data;

// Double-circle states arranged more compactly
data [shape=doublecircle label="Close script"];
script_data [shape=doublecircle color=blue label="script\ndata"];
script_data_escaped [shape=circle color=orange label="escaped"];
script_data_double_escaped [shape=circle color=red label="double\nescaped"];

// Group related nodes on same ranks where possible
{rank=same; script_data script_data_escaped script_data_double_escaped}

script_data -> script_data [label="<!--(…)>\n(all dashes)"];
script_data -> script_data_escaped [label="<!--"];
script_data -> data [label="</script†"];

script_data_escaped -> script_data [label="-->"];
script_data_escaped -> script_data_double_escaped [label="<script†"];
script_data_escaped -> data [label="</script†"];

script_data_double_escaped -> script_data [label="-->"];
script_data_double_escaped -> script_data_escaped [label="</script†"];

label="† = Case insensitive 'script' followed by one of ' \\t\\f\\r\\n/>'";
labelloc=b;
}
13 changes: 13 additions & 0 deletions tests/phpunit/data/html-api/script-element-escaping-diagram.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

/**
* This is the original Graphviz source for the SCRIPT tag
* parsing behavior, used in the documentation for the HTML API.
*
* @see WP_HTML_Tag_Processor::escape_javascript_script_contents()
*
* @return string
*/
function wp_html_api_script_element_escaping_diagram_source() {
return file_get_contents( __DIR__ . '/script-element-escaping-diagram.dot' );
}
17 changes: 9 additions & 8 deletions tests/phpunit/tests/dependencies/scripts.php
Original file line number Diff line number Diff line change
Expand Up @@ -333,8 +333,8 @@ public function test_delayed_dependent_with_blocking_dependency_not_enqueued( $s
// This dependent is registered but not enqueued, so it should not factor into the eligible loading strategy.
wp_register_script( 'dependent-script-a4', '/dependent-script-a4.js', array( 'main-script-a4' ), null );
$output = get_echo( 'wp_print_scripts' );
$expected = str_replace( "'", '"', "<script src='/main-script-a4.js' id='main-script-a4-js' {$strategy} data-wp-strategy='{$strategy}'></script>" );
$this->assertStringContainsString( $expected, $output, 'Only enqueued dependents should affect the eligible strategy.' );
$expected = "<script src='/main-script-a4.js' id='main-script-a4-js' {$strategy} data-wp-strategy='{$strategy}'></script>";
$this->assertEqualHTMLScriptTagById( $expected, $output, 'Only enqueued dependents should affect the eligible strategy.' );
Copy link
Member

Choose a reason for hiding this comment

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

I missed this in #10649! but this is big

Copy link
Member Author

Choose a reason for hiding this comment

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

These test changes should be extracted to their own PR.

}

/**
Expand Down Expand Up @@ -1076,8 +1076,8 @@ public function test_various_strategy_dependency_chains( $set_up, $expected_mark
public function test_loading_strategy_with_defer_having_no_dependents_nor_dependencies() {
wp_enqueue_script( 'main-script-d1', 'http://example.com/main-script-d1.js', array(), null, array( 'strategy' => 'defer' ) );
$output = get_echo( 'wp_print_scripts' );
$expected = str_replace( "'", '"', "<script src='http://example.com/main-script-d1.js' id='main-script-d1-js' defer data-wp-strategy='defer'></script>\n" );
$this->assertStringContainsString( $expected, $output, 'Expected defer, as there is no dependent or dependency' );
$expected = "<script src='http://example.com/main-script-d1.js' id='main-script-d1-js' defer data-wp-strategy='defer'></script>\n";
$this->assertEqualHTMLScriptTagById( $expected, $output, 'Expected defer, as there is no dependent or dependency' );
}

/**
Expand All @@ -1096,7 +1096,7 @@ public function test_loading_strategy_with_defer_dependent_and_varied_dependenci
wp_enqueue_script( 'main-script-d2', 'http://example.com/main-script-d2.js', array( 'dependency-script-d2-1', 'dependency-script-d2-3' ), null, array( 'strategy' => 'defer' ) );
$output = get_echo( 'wp_print_scripts' );
$expected = '<script src="http://example.com/main-script-d2.js" id="main-script-d2-js" defer data-wp-strategy="defer"></script>';
$this->assertStringContainsString( $expected, $output, 'Expected defer, as all dependencies are either deferred or blocking' );
$this->assertEqualHTMLScriptTagById( $expected, $output, 'Expected defer, as all dependencies are either deferred or blocking' );
}

/**
Expand All @@ -1115,7 +1115,7 @@ public function test_loading_strategy_with_all_defer_dependencies() {
wp_enqueue_script( 'dependent-script-d3-3', 'http://example.com/dependent-script-d3-3.js', array( 'dependent-script-d3-2' ), null, array( 'strategy' => 'defer' ) );
$output = get_echo( 'wp_print_scripts' );
$expected = '<script src="http://example.com/main-script-d3.js" id="main-script-d3-js" defer data-wp-strategy="defer"></script>';
$this->assertStringContainsString( $expected, $output, 'Expected defer, as all dependents have defer loading strategy' );
$this->assertEqualHTMLScriptTagById( $expected, $output, 'Expected defer, as all dependents have defer loading strategy' );
}

/**
Expand Down Expand Up @@ -1495,9 +1495,10 @@ public function test_loading_strategy_with_invalid_defer_registration() {
wp_enqueue_script( 'dependent-script-d4-1', '/dependent-script-d4-1.js', array( 'main-script-d4' ), null, array( 'strategy' => 'defer' ) );
wp_enqueue_script( 'dependent-script-d4-2', '/dependent-script-d4-2.js', array( 'dependent-script-d4-1' ), null );
wp_enqueue_script( 'dependent-script-d4-3', '/dependent-script-d4-3.js', array( 'dependent-script-d4-2' ), null, array( 'strategy' => 'defer' ) );

$output = get_echo( 'wp_print_scripts' );
$expected = str_replace( "'", '"', "<script src='/main-script-d4.js' id='main-script-d4-js' data-wp-strategy='defer'></script>\n" );
$this->assertStringContainsString( $expected, $output, 'Scripts registered as defer but that have all dependents with no strategy, should become blocking (no strategy).' );
$expected = "<script src='/main-script-d4.js' id='main-script-d4-js' data-wp-strategy='defer'></script>\n";
$this->assertEqualHTMLScriptTagById( $expected, $output, 'Scripts registered as defer but that have all dependents with no strategy, should become blocking (no strategy).' );
}

/**
Expand Down
161 changes: 152 additions & 9 deletions tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php
Original file line number Diff line number Diff line change
Expand Up @@ -444,17 +444,19 @@ public static function data_tokens_with_basic_modifiable_text_updates() {
/**
* Ensures that updates with potentially-compromising values aren't accepted.
*
* For example, a modifiable text update should be allowed which would break
* the structure of the containing element, such as in a script or comment.
* For example, a modifiable text update that would change the structure of the HTML
* document is not allowed, like attempting to set `-->` within a comment or `</script>`
* within a text/plain SCRIPT tag.
*
* @ticket 61617
* @ticket 62797
*
* @dataProvider data_unallowed_modifiable_text_updates
*
* @param string $html_with_nonempty_modifiable_text Will be used to find the test element.
* @param string $invalid_update Update containing possibly-compromising text.
*/
public function test_rejects_updates_with_unallowed_substrings( string $html_with_nonempty_modifiable_text, string $invalid_update ) {
public function test_rejects_dangerous_updates( string $html_with_nonempty_modifiable_text, string $invalid_update ) {
$processor = new WP_HTML_Tag_Processor( $html_with_nonempty_modifiable_text );

while ( '' === $processor->get_modifiable_text() && $processor->next_token() ) {
Expand All @@ -466,7 +468,7 @@ public function test_rejects_updates_with_unallowed_substrings( string $html_wit

$this->assertFalse(
$processor->set_modifiable_text( $invalid_update ),
'Should have reject possibly-compromising modifiable text update.'
'Should have rejected possibly-compromising modifiable text update.'
);

// Flush updates.
Expand All @@ -486,11 +488,152 @@ public function test_rejects_updates_with_unallowed_substrings( string $html_wit
*/
public static function data_unallowed_modifiable_text_updates() {
return array(
'Comment with -->' => array( '<!-- this is a comment -->', 'Comments end in -->' ),
'Comment with --!>' => array( '<!-- this is a comment -->', 'Invalid but legitimate comments end in --!>' ),
'SCRIPT with </script>' => array( '<script>Replace me</script>', 'Just a </script>' ),
'SCRIPT with </script attributes>' => array( '<script>Replace me</script>', 'before</script id=sneak>after' ),
'SCRIPT with "<script " opener' => array( '<script>Replace me</script>', '<!--<script ' ),
'Comment with -->' => array( '<!-- this is a comment -->', 'Comments end in -->' ),
'Comment with --!>' => array( '<!-- this is a comment -->', 'Invalid but legitimate comments end in --!>' ),
'Non-JS SCRIPT with <script>' => array( '<script type="text/html">Replace me</script>', '<!-- Just a <script>' ),
'Non-JS SCRIPT with </script>' => array( '<script type="text/plain">Replace me</script>', 'Just a </script>' ),
'Non-JS SCRIPT with <script attributes>' => array( '<script language="text">Replace me</script>', '<!-- <script sneaky>after' ),
'Non-JS SCRIPT with </script attributes>' => array( '<script language="text">Replace me</script>', 'before</script sneaky>after' ),
);
}

/**
* Ensures that JavaScript script tag contents are safely updated.
*
* @ticket 62797
*
* @dataProvider data_script_tag_text_updates
*
* @param string $html HTML containing a SCRIPT tag to be modified.
* @param string $update Update containing possibly-compromising text.
* @param string $expected Expected result.
*/
public function test_safely_updates_script_tag_contents( string $html, string $update, string $expected ) {
$processor = new WP_HTML_Tag_Processor( $html );
$this->assertTrue( $processor->next_tag( 'SCRIPT' ) );
$this->assertTrue( $processor->set_modifiable_text( $update ) );
$this->assertSame( $expected, $processor->get_updated_html() );
}

/**
* Data provider.
*
* @return array[]
*/
public static function data_script_tag_text_updates(): array {
return array(
'Simple update' => array( '<script></script>', '{}', '<script>{}</script>' ),
'Needs no replacement' => array( '<script></script>', '<!--<scriptish>', '<script><!--<scriptish></script>' ),
'var script;1<script>0' => array( '<script></script>', 'var script;1<script>0', '<script>var script;1<\u0073cript>0</script>' ),
'1</script>/' => array( '<script></script>', '1</script>/', '<script>1</\u0073cript>/</script>' ),
'var SCRIPT;1<SCRIPT>0' => array( '<script></script>', 'var SCRIPT;1<SCRIPT>0', '<script>var SCRIPT;1<\u0053CRIPT>0</script>' ),
'1</SCRIPT>/' => array( '<script></script>', '1</SCRIPT>/', '<script>1</\u0053CRIPT>/</script>' ),
'"</script>"' => array( '<script></script>', '"</script>"', '<script>"</\u0073cript>"</script>' ),
'"</ScRiPt>"' => array( '<script></script>', '"</ScRiPt>"', '<script>"</\u0053cRiPt>"</script>' ),
'Tricky script open tag with \r' => array( '<script></script>', "<!-- <script\r>", "<script><!-- <\\u0073cript\r></script>" ),
'Tricky script open tag with \r\n' => array( '<script></script>', "<!-- <script\r\n>", "<script><!-- <\\u0073cript\r\n></script>" ),
'Tricky script close tag with \r' => array( '<script></script>', "// </script\r>", "<script>// </\\u0073cript\r></script>" ),
'Tricky script close tag with \r\n' => array( '<script></script>', "// </script\r\n>", "<script>// </\\u0073cript\r\n></script>" ),
'Module tag' => array( '<script type="module"></script>', '"<script>"', '<script type="module">"<\u0073cript>"</script>' ),
'Tag with type' => array( '<script type="text/javascript"></script>', '"<script>"', '<script type="text/javascript">"<\u0073cript>"</script>' ),
'Tag with language' => array( '<script language="javascript"></script>', '"<script>"', '<script language="javascript">"<\u0073cript>"</script>' ),
'Non-JS script, save HTML-like content' => array( '<script type="text/html"></script>', '<h1>This & that</h1>', '<script type="text/html"><h1>This & that</h1></script>' ),
);
}

/**
* @ticket 64419
*/
public function test_complex_javascript_and_json_auto_escaping() {
$processor = new WP_HTML_Tag_Processor( "<script></script>\n<script></script>\n<hr>" );
$processor->next_tag( 'SCRIPT' );
$processor->set_attribute( 'type', 'importmap' );
$importmap_data = array(
'imports' => array(
'</SCRIPT>\\<!--\\<script>' => './script',
),
);

$importmap = json_encode(
$importmap_data,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_LINE_TERMINATORS
);

$processor->set_modifiable_text( "\n{$importmap}\n" );
$decoded_importmap = json_decode( $processor->get_modifiable_text(), true );
$this->assertSame( JSON_ERROR_NONE, json_last_error(), 'JSON failed to decode correctly.' );
$this->assertEquals( $importmap_data, $decoded_importmap );
$processor->next_tag( 'SCRIPT' );
$processor->set_attribute( 'type', 'module' );
$javascript = <<<'JS'
import '</SCRIPT>\\<!--\\<script>';
JS;
$processor->set_modifiable_text( "\n{$javascript}\n" );

$expected = <<<'HTML'
<script type="importmap">
{"imports":{"</\u0053CRIPT>\\<!--\\<\u0073cript>":"./script"}}
</script>
<script type="module">
import '</\u0053CRIPT>\\<!--\\<\u0073cript>';
</script>
<hr>
HTML;

$updated_html = $processor->get_updated_html();
$this->assertEqualHTML( $expected, $updated_html );

// Reprocess to ensure JSON survives HTML round-trip:
$processor = new WP_HTML_Tag_Processor( $updated_html );
$processor->next_tag( 'SCRIPT' );
$this->assertSame( 'importmap', $processor->get_attribute( 'type' ) );
$importmap_json = $processor->get_modifiable_text();
$decoded_importmap = json_decode( $importmap_json, true );
$this->assertSame( JSON_ERROR_NONE, json_last_error(), 'Importmap JSON failed to decode.' );
$this->assertEquals(
$importmap_data,
$decoded_importmap,
'JSON was not equal after re-processing updated HTML.'
);
}

/**
* @ticket 64419
*/
public function test_json_auto_escaping() {
// This is not a typical JSON encoding or escaping, but it is valid.
$json_text = '"Escaped BS: \\\\; Escaped BS+LT: \\\\<; Unescaped LT: <; Script closer: </script>"';
$expected_decoded_json = 'Escaped BS: \\; Escaped BS+LT: \\<; Unescaped LT: <; Script closer: </script>';
$decoded_json = json_decode( $json_text );
$this->assertSame( JSON_ERROR_NONE, json_last_error(), 'JSON failed to decode.' );
$this->assertSame(
$expected_decoded_json,
$decoded_json,
'Decoded JSON did not match expected value.'
);

$processor = new WP_HTML_Tag_Processor( '<script type="application/json"></script>' );
$processor->next_tag( 'SCRIPT' );

$processor->set_modifiable_text( "\n{$json_text}\n" );

$expected = <<<'HTML'
<script type="application/json">
"Escaped BS: \\; Escaped BS+LT: \\<; Unescaped LT: <; Script closer: </\u0073cript>"
</script>
HTML;

$updated_html = $processor->get_updated_html();
$this->assertEqualHTML( $expected, $updated_html );

// Reprocess to ensure JSON value survives HTML round-trip:
$processor = new WP_HTML_Tag_Processor( $updated_html );
$processor->next_tag( 'SCRIPT' );
$decoded_json_from_html = json_decode( $processor->get_modifiable_text(), true );
$this->assertSame( JSON_ERROR_NONE, json_last_error(), 'JSON failed to decode.' );
$this->assertEquals(
$expected_decoded_json,
$decoded_json_from_html
);
}
}
Loading
Loading