diff --git a/phpunit/data/wxr-with-note-comments.xml b/phpunit/data/wxr-with-note-comments.xml new file mode 100644 index 00000000..294c2db4 --- /dev/null +++ b/phpunit/data/wxr-with-note-comments.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + My WordPress Website + https://playground.wordpress.net/scope:creative-busy-garden + + Tue, 24 Mar 2026 10:39:41 +0000 + en-US + 1.2 + https://playground.wordpress.net/scope:creative-busy-garden + https://playground.wordpress.net/scope:creative-busy-garden + + 1 + https://wordpress.org/?v=6.9.4 + + + <![CDATA[Page with notes and comments]]> + https://playground.wordpress.net/scope:creative-busy-garden/page-with-notes-and-comments/ + Tue, 24 Mar 2026 10:38:09 +0000 + + https://playground.wordpress.net/scope:creative-busy-garden/?page_id=10 + + +

Note on heading 1

+ + + +

Paragraph with note 1

+]]>
+ + 10 + + + + + + + + + 0 + 0 + + + 0 + + 2 + + + http://127.0.0.1:34717 + + + + + + + 0 + 1 + + + 3 + + + http://127.0.0.1:34717 + + + + + + + 0 + 1 + + + 4 + + + http://127.0.0.1:34717 + + + + + + + 0 + 1 + +
+
+
diff --git a/phpunit/tests/import.php b/phpunit/tests/import.php index 3f037964..3b091d98 100644 --- a/phpunit/tests/import.php +++ b/phpunit/tests/import.php @@ -374,6 +374,43 @@ public static function data_flat_attachment_import_rewrites_attachment_url() { ); } + /** + * Test that note comment IDs are correctly remapped in block content. + * + * @covers WP_Import::update_block_note_ids + * @covers WP_Import::process_post_comments + */ + public function test_note_comment_ids_are_remapped_in_block_content() { + $authors = array( + 'admin' => 1, + ); + $this->_import_wp( DIR_TESTDATA_WP_IMPORTER . '/wxr-with-note-comments.xml', $authors ); + + $posts = get_posts( + array( + 'post_type' => 'page', + 'post_status' => 'publish', + 'title' => 'Page with notes and comments', + ) + ); + $post = $posts[0]; + $comments = get_comments( + array( + 'post_id' => $post->ID, + 'type' => 'note', + ) + ); + + $new_comment_ids = array_map( 'intval', wp_list_pluck( $comments, 'comment_ID' ) ); + + $this->assertStringNotContainsString( '{"noteId":2}', $post->post_content, 'Old noteId 2 should have been remapped.' ); + $this->assertStringNotContainsString( '{"noteId":3}', $post->post_content, 'Old noteId 3 should have been remapped.' ); + + foreach ( $new_comment_ids as $new_id ) { + $this->assertStringContainsString( '{"noteId":' . $new_id . '}', $post->post_content, "New noteId $new_id should be present in block content." ); + } + } + /** * Provides a mocked HTTP response when the importer downloads attachments. * diff --git a/src/class-wp-import.php b/src/class-wp-import.php index 6b179432..9d01f4b8 100644 --- a/src/class-wp-import.php +++ b/src/class-wp-import.php @@ -33,6 +33,7 @@ class WP_Import extends WP_Importer { public $author_mapping = array(); public $processed_terms = array(); public $processed_posts = array(); + public $processed_comments = array(); public $post_orphans = array(); public $processed_menu_items = array(); public $menu_item_orphans = array(); @@ -818,6 +819,7 @@ public function process_posts() { if ( ! empty( $post['comments'] ) ) { $this->process_post_comments( $post['comments'], (bool) $post_exists, $comment_post_id, $post ); unset( $post['comments'] ); + $this->update_block_note_ids( $post_id ); } if ( ! isset( $post['postmeta'] ) ) { @@ -1095,6 +1097,10 @@ protected function process_post_comments( $comments, $post_exists, $comment_post do_action( 'wp_import_insert_comment', $inserted_comment_id, $comment, $comment_post_id, $post ); $this->process_post_comment_metas( $inserted_comment_id, $comment['commentmeta'] ); $inserted_comments[ $key ] = $inserted_comment_id; + // Store comment ID mapping for note-type comments to update noteId references in blocks. + if ( isset( $comment['comment_type'] ) && 'note' === $comment['comment_type'] ) { + $this->processed_comments[ $key ] = $inserted_comment_id; + } ++$num_comments; } } @@ -1139,6 +1145,98 @@ protected function process_post_comment_metas( $comment_id, $commentmeta ) { } } + /** + * Remaps noteId references in block metadata after note-type comments are imported. + * + * @param int $post_id ID of the post whose block content should be updated. + * @return void + */ + protected function update_block_note_ids( int $post_id = 0 ): void { + if ( empty( $this->processed_comments ) ) { + return; + } + + $post = get_post( $post_id ); + if ( ! $post ) { + return; + } + + if ( ! str_contains( $post->post_content, '"noteId"' ) ) { + return; + } + + // @todo Replace with WP_HTML_Tag_Processor or WP_Block_Processor once minimum version support is 6.2 or 6.9 respectively. + $parser = new WP_Block_Parser(); + $parser->document = $post->post_content; + $parser->offset = 0; + $end = strlen( $post->post_content ); + $replacements = array(); + + while ( $parser->offset < $end ) { + $next_token = $parser->next_token(); + list( $token_type, $block_name, $attrs, $start_offset, $token_length ) = $next_token; + + if ( 'no-more-tokens' === $token_type ) { + break; + } + + $parser->offset = $start_offset + $token_length; + + if ( 'block-opener' !== $token_type && 'void-block' !== $token_type ) { + continue; + } + + $old_note_id = $attrs['metadata']['noteId'] ?? null; + + if ( + ! ( is_string( $old_note_id ) || is_int( $old_note_id ) ) || + ! isset( $this->processed_comments[ $old_note_id ] ) + ) { + continue; + } + + $attribute_string = substr( $post->post_content, $start_offset, $token_length ); + $attribute_json_start = strcspn( $attribute_string, '{' ); + $attribute_json_end = strrpos( $attribute_string, '}' ); + + if ( false === $attribute_json_end || $attribute_json_start >= $attribute_json_end ) { + continue; + } + + $json_start = $start_offset + $attribute_json_start; + $json_length = $attribute_json_end - $attribute_json_start + 1; + + $attrs['metadata']['noteId'] = $this->processed_comments[ $old_note_id ]; + $replacements[] = array( $json_start, $json_length, serialize_block_attributes( $attrs ) ); + } + + if ( empty( $replacements ) ) { + return; + } + + $post_content = $post->post_content; + $updated_content = ''; + $was_at = 0; + + foreach ( $replacements as $replacement ) { + list( $offset, $length, $new_json ) = $replacement; + + $pre_length = $offset - $was_at; + $updated_content .= substr( $post_content, $was_at, $pre_length ) . $new_json; + $was_at = $offset + $length; + } + + $updated_content .= substr( $post_content, $was_at ); + + wp_update_post( + // Cast to object to ensure wp_update_post() will add the required slashes. + (object) array( + 'ID' => $post_id, + 'post_content' => $updated_content, + ) + ); + } + /** * Process a single comment meta entry for an imported comment. *