diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index b3738c24ec9df..8447fdd7907ae 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -2162,6 +2162,8 @@ function wp_insert_comment( $commentdata ) { 'user_id' ); + $compacted = _wp_encode_emoji_in_fields( $compacted, $wpdb->comments, array( 'comment_author', 'comment_content' ) ); + if ( ! $wpdb->insert( $wpdb->comments, $compacted ) ) { return false; } @@ -2759,6 +2761,8 @@ function wp_update_comment( $commentarr, $wp_error = false ) { $data = wp_array_slice_assoc( $data, $keys ); + $data = _wp_encode_emoji_in_fields( $data, $wpdb->comments, array( 'comment_author', 'comment_content' ) ); + $result = $wpdb->update( $wpdb->comments, $data, array( 'comment_ID' => $comment_id ) ); if ( false === $result ) { diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 74a28109b6536..58737946b4e21 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -6116,6 +6116,42 @@ function wp_encode_emoji( $content ) { return $content; } +/** + * Encodes emoji in the given fields of a data array whose corresponding + * database columns cannot natively store 4-byte UTF-8 characters. + * + * Used by wp_insert_post(), wp_insert_comment(), and wp_update_comment() so + * that emoji (and other 4-byte characters) are stored as HTML entities on + * `utf8`/`utf8mb3` columns instead of causing truncated or failed writes. + * + * @access private + * @since 7.1.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param array $data Array of field name => value pairs to check, keyed the + * same way as the corresponding database columns. + * @param string $table Name of the database table the fields belong to. + * @param array $fields Names of the fields within $data that may contain emoji. + * @return array $data with emoji encoded in the given fields, where needed. + */ +function _wp_encode_emoji_in_fields( $data, $table, $fields ) { + global $wpdb; + + foreach ( $fields as $field ) { + if ( isset( $data[ $field ] ) ) { + $charset = $wpdb->get_col_charset( $table, $field ); + + // The 'utf8' character set is a deprecated alias of 'utf8mb3'. See . + if ( 'utf8' === $charset || 'utf8mb3' === $charset ) { + $data[ $field ] = wp_encode_emoji( $data[ $field ] ); + } + } + } + + return $data; +} + /** * Converts emoji to a static img element. * diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 3813176140bb4..a2b3a7f9afad1 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -4917,18 +4917,7 @@ function wp_insert_post( $postarr, $wp_error = false, $fire_after_hooks = true ) 'guid' ); - $emoji_fields = array( 'post_title', 'post_content', 'post_excerpt' ); - - foreach ( $emoji_fields as $emoji_field ) { - if ( isset( $data[ $emoji_field ] ) ) { - $charset = $wpdb->get_col_charset( $wpdb->posts, $emoji_field ); - - // The 'utf8' character set is a deprecated alias of 'utf8mb3'. See . - if ( 'utf8' === $charset || 'utf8mb3' === $charset ) { - $data[ $emoji_field ] = wp_encode_emoji( $data[ $emoji_field ] ); - } - } - } + $data = _wp_encode_emoji_in_fields( $data, $wpdb->posts, array( 'post_title', 'post_content', 'post_excerpt' ) ); if ( 'attachment' === $post_type ) { /** diff --git a/tests/phpunit/tests/comment.php b/tests/phpunit/tests/comment.php index 8c7e8d684bc59..1d927a1101040 100644 --- a/tests/phpunit/tests/comment.php +++ b/tests/phpunit/tests/comment.php @@ -2045,4 +2045,112 @@ public function test_get_comment_should_only_treat_numeric_values_as_comment_ids $this->assertNull( get_comment( $comment_id . 'abc' ), 'Expected a malformed numeric string not to be cast to a comment ID.' ); $this->assertNull( get_comment( true ), 'Expected true not to be cast to comment ID 1.' ); // @phpstan-ignore argument.type (Intentionally passing an invalid value.) } + + /** + * `wp_insert_post()` already encodes emoji as HTML entities before saving to a + * utf8/utf8mb3 column (see #31242). This forces the same code path for comments + * via the `pre_get_col_charset` filter, so it runs deterministically regardless + * of the test database's actual charset. + * + * @ticket 65700 + * + * @covers ::wp_insert_comment + * @covers ::_wp_encode_emoji_in_fields + */ + public function test_wp_insert_comment_encodes_emoji_for_utf8mb3_columns(): void { + add_filter( 'pre_get_col_charset', array( $this, 'filter_pre_get_col_charset_utf8mb3_for_comment_fields' ), 10, 3 ); + + $comment_id = wp_insert_comment( + array( + 'comment_post_ID' => self::$post_id, + 'comment_author' => "foo\xf0\x9f\x98\x88bar", + 'comment_content' => "foo\xf0\x9f\x98\x8ebaz", + ) + ); + + remove_filter( 'pre_get_col_charset', array( $this, 'filter_pre_get_col_charset_utf8mb3_for_comment_fields' ), 10 ); + + $this->assertIsInt( $comment_id, 'Expected wp_insert_comment() to return the new comment ID.' ); + + $comment = get_comment( $comment_id ); + + $this->assertSame( 'foo😈bar', $comment->comment_author ); + $this->assertSame( 'foo😎baz', $comment->comment_content ); + } + + /** + * Control for test_wp_insert_comment_encodes_emoji_for_utf8mb3_columns(): on a + * utf8mb4 column, emoji should be stored as-is rather than as HTML entities. + * + * @ticket 65700 + * + * @covers ::wp_insert_comment + */ + public function test_wp_insert_comment_does_not_encode_emoji_for_utf8mb4_columns(): void { + add_filter( 'pre_get_col_charset', array( $this, 'filter_pre_get_col_charset_utf8mb4_for_comment_fields' ), 10, 3 ); + + $comment_id = wp_insert_comment( + array( + 'comment_post_ID' => self::$post_id, + 'comment_author' => "foo\xf0\x9f\x98\x88bar", + 'comment_content' => "foo\xf0\x9f\x98\x8ebaz", + ) + ); + + remove_filter( 'pre_get_col_charset', array( $this, 'filter_pre_get_col_charset_utf8mb4_for_comment_fields' ), 10 ); + + $comment = get_comment( $comment_id ); + + $this->assertSame( "foo\xf0\x9f\x98\x88bar", $comment->comment_author ); + $this->assertSame( "foo\xf0\x9f\x98\x8ebaz", $comment->comment_content ); + } + + /** + * @ticket 65700 + * + * @covers ::wp_update_comment + * @covers ::_wp_encode_emoji_in_fields + */ + public function test_wp_update_comment_encodes_emoji_for_utf8mb3_columns(): void { + $comment_id = self::factory()->comment->create( array( 'comment_post_ID' => self::$post_id ) ); + + add_filter( 'pre_get_col_charset', array( $this, 'filter_pre_get_col_charset_utf8mb3_for_comment_fields' ), 10, 3 ); + + $result = wp_update_comment( + array( + 'comment_ID' => $comment_id, + 'comment_author' => "foo\xf0\x9f\x98\x88bar", + 'comment_content' => "foo\xf0\x9f\x98\x8ebaz", + ) + ); + + remove_filter( 'pre_get_col_charset', array( $this, 'filter_pre_get_col_charset_utf8mb3_for_comment_fields' ), 10 ); + + $this->assertSame( 1, $result ); + + $comment = get_comment( $comment_id ); + + $this->assertSame( 'foo😈bar', $comment->comment_author ); + $this->assertSame( 'foo😎baz', $comment->comment_content ); + } + + public function filter_pre_get_col_charset_utf8mb3_for_comment_fields( $charset, $table, $column ) { + global $wpdb; + + if ( $wpdb->comments === $table && in_array( $column, array( 'comment_author', 'comment_content' ), true ) ) { + return 'utf8mb3'; + } + + return $charset; + } + + public function filter_pre_get_col_charset_utf8mb4_for_comment_fields( $charset, $table, $column ) { + global $wpdb; + + if ( $wpdb->comments === $table && in_array( $column, array( 'comment_author', 'comment_content' ), true ) ) { + return 'utf8mb4'; + } + + return $charset; + } } diff --git a/tests/phpunit/tests/formatting/emoji.php b/tests/phpunit/tests/formatting/emoji.php index d885c49fd7289..81c2d5cdd1b14 100644 --- a/tests/phpunit/tests/formatting/emoji.php +++ b/tests/phpunit/tests/formatting/emoji.php @@ -376,4 +376,95 @@ public function data_wp_staticize_emoji() { public function test_wp_staticize_emoji( $emoji, $expected ) { $this->assertSame( $expected, wp_staticize_emoji( $emoji ) ); } + + /** + * @ticket 65700 + * + * @covers ::_wp_encode_emoji_in_fields + */ + public function test_wp_encode_emoji_in_fields_only_touches_requested_utf8mb3_fields(): void { + $data = array( + 'legacy_field' => '🙂', + 'modern_field' => '🙂', + 'ignored_field' => '🙂', + ); + + add_filter( 'pre_get_col_charset', array( $this, 'filter_pre_get_col_charset_for_encode_emoji_in_fields_test' ), 10, 3 ); + + $result = _wp_encode_emoji_in_fields( $data, 'some_table', array( 'legacy_field', 'modern_field' ) ); + + remove_filter( 'pre_get_col_charset', array( $this, 'filter_pre_get_col_charset_for_encode_emoji_in_fields_test' ), 10 ); + + $this->assertSame( + '🙂', + $result['legacy_field'], + 'Expected emoji to be encoded for a field on a utf8/utf8mb3 column.' + ); + $this->assertSame( + '🙂', + $result['modern_field'], + 'Expected emoji to be left alone for a field on a utf8mb4 column.' + ); + $this->assertSame( + '🙂', + $result['ignored_field'], + 'Expected a field not passed in $fields to be left alone entirely.' + ); + } + + /** + * @ticket 65700 + * + * @covers ::_wp_encode_emoji_in_fields + */ + public function test_wp_encode_emoji_in_fields_recognizes_the_utf8_alias(): void { + $data = array( 'legacy_field' => '🙂' ); + + add_filter( 'pre_get_col_charset', array( $this, 'filter_pre_get_col_charset_utf8_alias_for_encode_emoji_in_fields_test' ) ); + + $result = _wp_encode_emoji_in_fields( $data, 'some_table', array( 'legacy_field' ) ); + + remove_filter( 'pre_get_col_charset', array( $this, 'filter_pre_get_col_charset_utf8_alias_for_encode_emoji_in_fields_test' ) ); + + $this->assertSame( + '🙂', + $result['legacy_field'], + 'Expected emoji to be encoded for a field reporting the deprecated utf8 charset alias.' + ); + } + + /** + * @ticket 65700 + * + * @covers ::_wp_encode_emoji_in_fields + */ + public function test_wp_encode_emoji_in_fields_ignores_fields_missing_from_data(): void { + $data = array( 'present_field' => '🙂' ); + + // Short-circuits wpdb::get_col_charset() so this doesn't hit a real, + // nonexistent 'some_table' table in the test database. + add_filter( 'pre_get_col_charset', array( $this, 'filter_pre_get_col_charset_utf8mb4_for_encode_emoji_in_fields_test' ) ); + + $result = _wp_encode_emoji_in_fields( $data, 'some_table', array( 'present_field', 'absent_field' ) ); + + remove_filter( 'pre_get_col_charset', array( $this, 'filter_pre_get_col_charset_utf8mb4_for_encode_emoji_in_fields_test' ) ); + + $this->assertArrayNotHasKey( + 'absent_field', + $result, + 'Expected a field missing from $data to not be added by _wp_encode_emoji_in_fields().' + ); + } + + public function filter_pre_get_col_charset_for_encode_emoji_in_fields_test( $charset, $table, $column ) { + return 'legacy_field' === $column ? 'utf8mb3' : 'utf8mb4'; + } + + public function filter_pre_get_col_charset_utf8_alias_for_encode_emoji_in_fields_test() { + return 'utf8'; + } + + public function filter_pre_get_col_charset_utf8mb4_for_encode_emoji_in_fields_test() { + return 'utf8mb4'; + } } diff --git a/tests/phpunit/tests/post.php b/tests/phpunit/tests/post.php index e609fa0d3003d..1d2834fd243ab 100644 --- a/tests/phpunit/tests/post.php +++ b/tests/phpunit/tests/post.php @@ -395,6 +395,46 @@ public function test_utf8mb3_post_saves_with_emoji() { } } + /** + * Forces the utf8mb3 code path via the `pre_get_col_charset` filter, rather + * than relying on the test database actually using that character set, so + * this runs deterministically regardless of the environment's DB charset. + * + * @ticket 65700 + * + * @covers ::wp_insert_post + * @covers ::_wp_encode_emoji_in_fields + */ + public function test_wp_insert_post_encodes_emoji_for_utf8mb3_columns_via_filter(): void { + add_filter( 'pre_get_col_charset', array( $this, 'filter_pre_get_col_charset_utf8mb3_for_post_fields' ), 10, 3 ); + + $post_id = self::factory()->post->create( + array( + 'post_title' => "foo\xf0\x9f\x98\x88bar", + 'post_content' => "foo\xf0\x9f\x98\x8ebaz", + 'post_excerpt' => "foo\xf0\x9f\x98\x90bat", + ) + ); + + remove_filter( 'pre_get_col_charset', array( $this, 'filter_pre_get_col_charset_utf8mb3_for_post_fields' ), 10 ); + + $post = get_post( $post_id ); + + $this->assertSame( 'foo😈bar', $post->post_title ); + $this->assertSame( 'foo😎baz', $post->post_content ); + $this->assertSame( 'foo😐bat', $post->post_excerpt ); + } + + public function filter_pre_get_col_charset_utf8mb3_for_post_fields( $charset, $table, $column ) { + global $wpdb; + + if ( $wpdb->posts === $table && in_array( $column, array( 'post_title', 'post_content', 'post_excerpt' ), true ) ) { + return 'utf8mb3'; + } + + return $charset; + } + /** * If a sticky post is updated via `wp_update_post()` by a user * without the `publish_posts` capability, it should stay sticky.