Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions src/wp-includes/comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 ) {
Expand Down
36 changes: 36 additions & 0 deletions src/wp-includes/formatting.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://dev.mysql.com/doc/refman/8.4/en/charset-unicode-utf8.html>.
if ( 'utf8' === $charset || 'utf8mb3' === $charset ) {
$data[ $field ] = wp_encode_emoji( $data[ $field ] );
}
}
}

return $data;
}

/**
* Converts emoji to a static img element.
*
Expand Down
13 changes: 1 addition & 12 deletions src/wp-includes/post.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://dev.mysql.com/doc/refman/8.4/en/charset-unicode-utf8.html>.
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 ) {
/**
Expand Down
108 changes: 108 additions & 0 deletions tests/phpunit/tests/comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -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&#x1f608;bar', $comment->comment_author );
$this->assertSame( 'foo&#x1f60e;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&#x1f608;bar', $comment->comment_author );
$this->assertSame( 'foo&#x1f60e;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;
}
}
91 changes: 91 additions & 0 deletions tests/phpunit/tests/formatting/emoji.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
'&#x1f642;',
$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(
'&#x1f642;',
$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';
}
}
40 changes: 40 additions & 0 deletions tests/phpunit/tests/post.php
Original file line number Diff line number Diff line change
Expand Up @@ -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&#x1f608;bar', $post->post_title );
$this->assertSame( 'foo&#x1f60e;baz', $post->post_content );
$this->assertSame( 'foo&#x1f610;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.
Expand Down
Loading