' );
+
+ // Find the test node in the middle.
+ while ( 'TEXTAREA' !== $processor->get_token_name() && $processor->next_token() ) {
+ continue;
+ }
+
+ $this->assertSame(
+ 'TEXTAREA',
+ $processor->get_token_name(),
+ 'Failed to find the test TEXTAREA node; check the test setup.'
+ );
+
+ $processor->set_modifiable_text( 'short' );
+ $processor->get_updated_html();
+ $this->assertSame(
+ 'short',
+ $processor->get_modifiable_text(),
+ 'Should have updated modifiable text to something shorter than the original.'
+ );
+
+ $this->assertTrue(
+ $processor->next_token(),
+ 'Should have advanced to the last token in the input.'
+ );
+
+ $this->assertSame(
+ 'DIV',
+ $processor->get_token_name(),
+ 'Should have recognized the final DIV in the input.'
+ );
+
+ $this->assertSame(
+ 'not a
',
+ $processor->get_attribute( 'id' ),
+ 'Should have read in the id from the last DIV as "not a "'
+ );
+ }
+
+ /**
+ * Ensures that reads to modifiable text after setting it reads the updated
+ * enqueued values, and not the original value.
+ *
+ * @ticket 61617
+ */
+ public function test_modifiable_text_reads_updates_after_setting() {
+ $processor = new WP_HTML_Tag_Processor( 'This is text' );
+
+ $processor->next_token();
+ $this->assertSame(
+ '#text',
+ $processor->get_token_name(),
+ 'Failed to find first text node: check test setup.'
+ );
+
+ $update = 'This is new text';
+ $processor->set_modifiable_text( $update );
+ $this->assertSame(
+ $update,
+ $processor->get_modifiable_text(),
+ 'Failed to read updated enqueued value of text node.'
+ );
+
+ $processor->next_token();
+ $this->assertSame(
+ '#comment',
+ $processor->get_token_name(),
+ 'Failed to advance to comment: check test setup.'
+ );
+
+ $this->assertSame(
+ ' this is not ',
+ $processor->get_modifiable_text(),
+ 'Failed to read modifiable text for next token; did it read the old enqueued value from the previous token?'
+ );
+ }
+
/**
* Ensures that when ignoring a newline after LISTING and PRE tags, that this
* happens appropriately after seeking.
@@ -108,4 +192,155 @@ public function test_get_modifiable_text_ignores_newlines_after_seeking() {
'Should not have removed the leading newline from the last DIV on its second traversal.'
);
}
+
+ /**
+ * Ensures that modifiable text updates are not applied where they aren't supported.
+ *
+ * @ticket 61617
+ *
+ * @dataProvider data_tokens_not_supporting_modifiable_text_updates
+ *
+ * @param string $html Contains HTML with a token not supporting modifiable text updates.
+ * @param int $advance_n_tokens Count of times to run `next_token()` before reaching target node.
+ */
+ public function test_rejects_updates_on_unsupported_match_locations( string $html, int $advance_n_tokens ) {
+ $processor = new WP_HTML_Tag_Processor( $html );
+ while ( --$advance_n_tokens >= 0 ) {
+ $processor->next_token();
+ }
+
+ $this->assertFalse(
+ $processor->set_modifiable_text( 'Bazinga!' ),
+ 'Should have prevented modifying the text at the target node.'
+ );
+
+ $this->assertSame(
+ $html,
+ $processor->get_updated_html(),
+ 'Should not have modified the input document in any way.'
+ );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public static function data_tokens_not_supporting_modifiable_text_updates() {
+ return array(
+ 'Before parsing' => array( 'nothing to see here', 0 ),
+ 'After parsing' => array( 'nothing here either', 2 ),
+ 'Incomplete document' => array( ' array( 'Text', 1, 'Blubber', 'Blubber' ),
+ 'Text node (middle)' => array( 'Bold move', 2, 'yo', 'yo' ),
+ 'Text node (end)' => array( 'of a dog', 2, 'of a cat', 'of a cat' ),
+ 'Encoded text node' => array( 'birds and dogs', 2, ' & ', '<birds> & <dogs>' ),
+ 'SCRIPT tag' => array( 'beforeafter', 2, 'const img = " &
";', 'beforeafter' ),
+ 'STYLE tag' => array( '', 1, 'p::before { content: " & "; }', '' ),
+ 'TEXTAREA tag' => array( 'ab', 2, "so it ", "ab" ),
+ 'TEXTAREA (escape)' => array( 'ab', 2, 'but it does for ', 'ab' ),
+ 'TEXTAREA (escape+attrs)' => array( 'ab', 2, 'but it does for ', 'ab' ),
+ 'TITLE tag' => array( 'ahas no need to escapeb', 2, "so it ", "aso it b" ),
+ 'TITLE (escape)' => array( 'ahas no need to escapeb', 2, 'but it does for ', 'abut it does for </title>b' ),
+ 'TITLE (escape+attrs)' => array( 'ahas no need to escapeb', 2, 'but it does for ', 'abut it does for </title not an="attribute">b' ),
+ );
+ }
+
+ /**
+ * 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.
+ *
+ * @ticket 61617
+ *
+ * @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 ) {
+ $processor = new WP_HTML_Tag_Processor( $html_with_nonempty_modifiable_text );
+
+ while ( '' === $processor->get_modifiable_text() && $processor->next_token() ) {
+ continue;
+ }
+
+ $original_text = $processor->get_modifiable_text();
+ $this->assertNotEmpty( $original_text, 'Should have found non-empty text: check test setup.' );
+
+ $this->assertFalse(
+ $processor->set_modifiable_text( $invalid_update ),
+ 'Should have reject possibly-compromising modifiable text update.'
+ );
+
+ // Flush updates.
+ $processor->get_updated_html();
+
+ $this->assertSame(
+ $original_text,
+ $processor->get_modifiable_text(),
+ 'Should have preserved the original modifiable text before the rejected update.'
+ );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public static function data_unallowed_modifiable_text_updates() {
+ return array(
+ 'Comment with -->' => array( '', 'Comments end in -->' ),
+ 'Comment with --!>' => array( '', 'Invalid but legitimate comments end in --!>' ),
+ 'SCRIPT with ' => array( '', 'Just a ' ),
+ 'SCRIPT with ' => array( '', 'beforeafter' ),
+ );
+ }
}