From 0890e0c9d157c0af109fb2ac34b45ecc4e68bf99 Mon Sep 17 00:00:00 2001 From: Joseph Scott Date: Mon, 14 Sep 2026 14:03:23 -0600 Subject: [PATCH 01/17] Fatal error in wp_insert_term() when the term name is not a string https://core.trac.wordpress.org/ticket/66108 --- src/wp-includes/class-wp-xmlrpc-server.php | 4 ++ src/wp-includes/taxonomy.php | 3 +- tests/phpunit/tests/term/wpInsertTerm.php | 56 ++++++++++++++++++++++ tests/phpunit/tests/xmlrpc/wp/newPost.php | 37 ++++++++++++++ 4 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index dcb2636d7a726..d4b073bc001e8 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -1666,6 +1666,10 @@ protected function _insert_post( $user, $content_struct ) { $term_names = $post_data['terms_names'][ $taxonomy ]; foreach ( $term_names as $term_name ) { + if ( ! is_string( $term_name ) ) { + return new IXR_Error( 403, __( 'Invalid term name.' ) ); + } + if ( in_array( $term_name, $ambiguous_terms, true ) ) { return new IXR_Error( 401, __( 'Ambiguous term name used in a hierarchical taxonomy. Please use term ID instead.' ) ); } diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index 6dbc336395f74..0242b3a98a92c 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -2501,6 +2501,7 @@ function wp_get_object_terms( $object_ids, $taxonomies, $args = array() ) { * @global wpdb $wpdb WordPress database abstraction object. * * @since 2.3.0 + * @since 7.2.0 A non-scalar `$term` now returns a WP_Error instead of causing a fatal error. * * @param string $term The term name to add. * @param string $taxonomy The taxonomy to which to add the term. @@ -2558,7 +2559,7 @@ function wp_insert_term( $term, $taxonomy, $args = array() ) { return new WP_Error( 'invalid_term_id', __( 'Invalid term ID.' ) ); } - if ( '' === trim( $term ) ) { + if ( ! is_scalar( $term ) || '' === trim( $term ) ) { return new WP_Error( 'empty_term_name', __( 'A name is required for this term.' ) ); } diff --git a/tests/phpunit/tests/term/wpInsertTerm.php b/tests/phpunit/tests/term/wpInsertTerm.php index 39754652e3f46..31c7a60d92979 100644 --- a/tests/phpunit/tests/term/wpInsertTerm.php +++ b/tests/phpunit/tests/term/wpInsertTerm.php @@ -76,6 +76,62 @@ public function test_wp_insert_term_term_trims_to_empty_string() { $this->assertSame( 'empty_term_name', $found->get_error_code() ); } + /** + * Tests that a non-scalar term name returns a WP_Error instead of causing a fatal error. + * + * @ticket 66108 + * + * @dataProvider data_wp_insert_term_non_scalar_term_name + * + * @param mixed $term The term name to insert. + */ + public function test_wp_insert_term_non_scalar_term_name( $term ): void { + $found = wp_insert_term( $term, 'post_tag' ); + + $this->assertWPError( $found ); + $this->assertSame( 'empty_term_name', $found->get_error_code() ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_wp_insert_term_non_scalar_term_name(): array { + return array( + 'array' => array( array( 'foo', 'bar' ) ), + 'object' => array( new stdClass() ), + ); + } + + /** + * Tests that a numeric term name is inserted and stored as its string form. + * + * @ticket 66108 + * + * @dataProvider data_wp_insert_term_numeric_term_name + * + * @param int|float $term The term name to insert. + */ + public function test_wp_insert_term_numeric_term_name( $term ): void { + $found = wp_insert_term( $term, 'post_tag' ); + + $this->assertNotWPError( $found ); + $this->assertSame( (string) $term, get_term( $found['term_id'], 'post_tag' )->name ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_wp_insert_term_numeric_term_name(): array { + return array( + 'integer' => array( 2024 ), + 'float' => array( 1.5 ), + ); + } + public function test_wp_insert_term_parent_does_not_exist() { $found = wp_insert_term( 'foo', diff --git a/tests/phpunit/tests/xmlrpc/wp/newPost.php b/tests/phpunit/tests/xmlrpc/wp/newPost.php index c6386a1f23d5b..73fa61a4d26c9 100644 --- a/tests/phpunit/tests/xmlrpc/wp/newPost.php +++ b/tests/phpunit/tests/xmlrpc/wp/newPost.php @@ -345,6 +345,43 @@ public function test_terms_names() { $this->assertSame( 401, $result2->code ); } + /** + * Tests that a non-string term name in `terms_names` returns an error instead of causing a fatal error. + * + * @ticket 66108 + * + * @dataProvider data_terms_names_non_string_term_name + * + * @param mixed $term_name The term name to send. + */ + public function test_terms_names_non_string_term_name( $term_name ): void { + $this->make_user_by_role( 'editor' ); + + $post = array( + 'post_title' => 'Test', + 'terms_names' => array( + 'post_tag' => array( $term_name ), + ), + ); + $result = $this->myxmlrpcserver->wp_newPost( array( 1, 'editor', 'editor', $post ) ); + + $this->assertIXRError( $result ); + $this->assertSame( 403, $result->code ); + $this->assertSame( 'Invalid term name.', $result->message ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_terms_names_non_string_term_name(): array { + return array( + 'array' => array( array( 'foo', 'bar' ) ), + 'integer' => array( 123 ), + ); + } + /** * @ticket 28601 */ From 99cded94e3e0f72861c613dacb24e60c5b3e7809 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 29 Oct 2026 18:07:45 -0700 Subject: [PATCH 02/17] Fix PHPStan errors in tests --- tests/phpunit/tests/term/wpInsertTerm.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/phpunit/tests/term/wpInsertTerm.php b/tests/phpunit/tests/term/wpInsertTerm.php index deb46b79d2a9b..869227aba06a5 100644 --- a/tests/phpunit/tests/term/wpInsertTerm.php +++ b/tests/phpunit/tests/term/wpInsertTerm.php @@ -93,10 +93,10 @@ public function test_wp_insert_term_term_trims_to_empty_string() { * * @dataProvider data_wp_insert_term_non_scalar_term_name * - * @param mixed $term The term name to insert. + * @param mixed $name The term name to insert. */ - public function test_wp_insert_term_non_scalar_term_name( $term ): void { - $found = wp_insert_term( $term, 'post_tag' ); + public function test_wp_insert_term_non_scalar_term_name( $name ): void { + $found = wp_insert_term( $name, 'post_tag' ); // @phpstan-ignore argument.type (Intentionally passing scalars.) $this->assertWPError( $found ); $this->assertSame( 'empty_term_name', $found->get_error_code() ); @@ -121,19 +121,21 @@ public function data_wp_insert_term_non_scalar_term_name(): array { * * @dataProvider data_wp_insert_term_numeric_term_name * - * @param int|float $term The term name to insert. + * @param int|float $name The term name to insert. */ - public function test_wp_insert_term_numeric_term_name( $term ): void { - $found = wp_insert_term( $term, 'post_tag' ); + public function test_wp_insert_term_numeric_term_name( $name ): void { + $found = wp_insert_term( $name, 'post_tag' ); // @phpstan-ignore argument.type (Intentionally passing scalars.) $this->assertNotWPError( $found ); - $this->assertSame( (string) $term, get_term( $found['term_id'], 'post_tag' )->name ); + $term = get_term( $found['term_id'], 'post_tag' ); + $this->assertInstanceOf( WP_Term::class, $term ); + $this->assertSame( (string) $name, $term->name ); } /** * Data provider. * - * @return array + * @return array */ public function data_wp_insert_term_numeric_term_name(): array { return array( From 10a1d92d7388e10c287339544e176258a565e63a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 29 Oct 2026 18:09:29 -0700 Subject: [PATCH 03/17] Add see references --- tests/phpunit/tests/term/wpInsertTerm.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/term/wpInsertTerm.php b/tests/phpunit/tests/term/wpInsertTerm.php index 869227aba06a5..439a5893d3549 100644 --- a/tests/phpunit/tests/term/wpInsertTerm.php +++ b/tests/phpunit/tests/term/wpInsertTerm.php @@ -103,7 +103,7 @@ public function test_wp_insert_term_non_scalar_term_name( $name ): void { } /** - * Data provider. + * Data provider for {@see self::test_wp_insert_term_non_scalar_term_name()} * * @return array */ @@ -133,7 +133,7 @@ public function test_wp_insert_term_numeric_term_name( $name ): void { } /** - * Data provider. + * Data provider for {@see self::test_wp_insert_term_numeric_term_name()}. * * @return array */ From a21ad670aae4e8372c2df5838b53af6df8355794 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 29 Oct 2026 18:10:03 -0700 Subject: [PATCH 04/17] S p a c e i t o u t --- tests/phpunit/tests/term/wpInsertTerm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/term/wpInsertTerm.php b/tests/phpunit/tests/term/wpInsertTerm.php index 439a5893d3549..365e03c2e3f44 100644 --- a/tests/phpunit/tests/term/wpInsertTerm.php +++ b/tests/phpunit/tests/term/wpInsertTerm.php @@ -105,7 +105,7 @@ public function test_wp_insert_term_non_scalar_term_name( $name ): void { /** * Data provider for {@see self::test_wp_insert_term_non_scalar_term_name()} * - * @return array + * @return array */ public function data_wp_insert_term_non_scalar_term_name(): array { return array( From 402aaaa8a2592b8c3418a91df5dfd59bd04e3ffa Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 29 Oct 2026 18:10:41 -0700 Subject: [PATCH 05/17] Narrow data provider keys --- tests/phpunit/tests/term/wpInsertTerm.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/term/wpInsertTerm.php b/tests/phpunit/tests/term/wpInsertTerm.php index 365e03c2e3f44..7cf917e72e38f 100644 --- a/tests/phpunit/tests/term/wpInsertTerm.php +++ b/tests/phpunit/tests/term/wpInsertTerm.php @@ -105,7 +105,7 @@ public function test_wp_insert_term_non_scalar_term_name( $name ): void { /** * Data provider for {@see self::test_wp_insert_term_non_scalar_term_name()} * - * @return array + * @return array */ public function data_wp_insert_term_non_scalar_term_name(): array { return array( @@ -135,7 +135,7 @@ public function test_wp_insert_term_numeric_term_name( $name ): void { /** * Data provider for {@see self::test_wp_insert_term_numeric_term_name()}. * - * @return array + * @return array */ public function data_wp_insert_term_numeric_term_name(): array { return array( From f216122f954bc638bae724d8e0ec491a761480e5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 29 Oct 2026 18:24:36 -0700 Subject: [PATCH 06/17] Accept numeric term names in XML-RPC terms_names The is_string() guard rejected term names sent as or , such as a tag named 2024, which previously worked via get_term_by() and wp_insert_term(). Cast ints and floats to strings instead, and reject only other types such as arrays and booleans. Replace the 'integer' invalid case with 'boolean', and add a test asserting numeric names are assigned using their string form. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/class-wp-xmlrpc-server.php | 5 ++- tests/phpunit/tests/xmlrpc/wp/newPost.php | 46 +++++++++++++++++++--- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index 2702358da783b..874993dc7c033 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -1710,7 +1710,10 @@ protected function _insert_post( $user, $content_struct ) { $term_names = $post_data['terms_names'][ $taxonomy ]; foreach ( $term_names as $term_name ) { - if ( ! is_string( $term_name ) ) { + // Numeric names may arrive as or values. + if ( is_int( $term_name ) || is_float( $term_name ) ) { + $term_name = (string) $term_name; + } elseif ( ! is_string( $term_name ) ) { return new IXR_Error( 403, __( 'Invalid term name.' ) ); } diff --git a/tests/phpunit/tests/xmlrpc/wp/newPost.php b/tests/phpunit/tests/xmlrpc/wp/newPost.php index 73fa61a4d26c9..903bef29eae82 100644 --- a/tests/phpunit/tests/xmlrpc/wp/newPost.php +++ b/tests/phpunit/tests/xmlrpc/wp/newPost.php @@ -346,15 +346,15 @@ public function test_terms_names() { } /** - * Tests that a non-string term name in `terms_names` returns an error instead of causing a fatal error. + * Tests that an invalid term name in `terms_names` returns an error instead of causing a fatal error. * * @ticket 66108 * - * @dataProvider data_terms_names_non_string_term_name + * @dataProvider data_terms_names_invalid_term_name * * @param mixed $term_name The term name to send. */ - public function test_terms_names_non_string_term_name( $term_name ): void { + public function test_terms_names_invalid_term_name( $term_name ): void { $this->make_user_by_role( 'editor' ); $post = array( @@ -375,10 +375,46 @@ public function test_terms_names_non_string_term_name( $term_name ): void { * * @return array */ - public function data_terms_names_non_string_term_name(): array { + public function data_terms_names_invalid_term_name(): array { return array( 'array' => array( array( 'foo', 'bar' ) ), - 'integer' => array( 123 ), + 'boolean' => array( true ), + ); + } + + /** + * Tests that a numeric term name in `terms_names` is assigned using its string form. + * + * @ticket 66108 + * + * @dataProvider data_terms_names_numeric_term_name + * + * @param int|float $term_name The term name to send. + */ + public function test_terms_names_numeric_term_name( $term_name ): void { + $this->make_user_by_role( 'editor' ); + + $post = array( + 'post_title' => 'Test', + 'terms_names' => array( + 'post_tag' => array( $term_name ), + ), + ); + $result = $this->myxmlrpcserver->wp_newPost( array( 1, 'editor', 'editor', $post ) ); + + $this->assertNotIXRError( $result ); + $this->assertSame( array( (string) $term_name ), wp_get_post_tags( (int) $result, array( 'fields' => 'names' ) ) ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_terms_names_numeric_term_name(): array { + return array( + 'integer' => array( 2024 ), + 'float' => array( 1.5 ), ); } From 5f65187c6b5bc7bbbbd6f05d87951b62fab8eec1 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 29 Oct 2026 18:26:59 -0700 Subject: [PATCH 07/17] Detect ambiguous numeric term names in XML-RPC terms_names The ambiguous names list comes from array_keys(), which turns numeric names like "2024" into integer keys. Since term names are now always strings by the time they are checked, the strict in_array() never matched them, so a hierarchical taxonomy with two terms named "2024" silently resolved to whichever term get_term_by() returned first. Cast the keys back to strings. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/class-wp-xmlrpc-server.php | 3 +- tests/phpunit/tests/xmlrpc/wp/newPost.php | 39 ++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index 874993dc7c033..425c5c24d779e 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -1705,7 +1705,8 @@ protected function _insert_post( $user, $content_struct ) { // Filter out non-ambiguous term names. $ambiguous_tax_term_counts = array_filter( $tax_term_names_count, array( $this, '_is_greater_than_one' ) ); - $ambiguous_terms = array_keys( $ambiguous_tax_term_counts ); + // Cast back to strings, since array keys turn numeric names into integers. + $ambiguous_terms = array_map( 'strval', array_keys( $ambiguous_tax_term_counts ) ); } $term_names = $post_data['terms_names'][ $taxonomy ]; diff --git a/tests/phpunit/tests/xmlrpc/wp/newPost.php b/tests/phpunit/tests/xmlrpc/wp/newPost.php index 903bef29eae82..20406cefe6bb6 100644 --- a/tests/phpunit/tests/xmlrpc/wp/newPost.php +++ b/tests/phpunit/tests/xmlrpc/wp/newPost.php @@ -418,6 +418,45 @@ public function data_terms_names_numeric_term_name(): array { ); } + /** + * Tests that an ambiguous numeric term name in a hierarchical taxonomy returns an error. + * + * @ticket 66108 + * + * @dataProvider data_terms_names_ambiguous_numeric_term_name + * + * @param string|int $term_name The term name to send. + */ + public function test_terms_names_ambiguous_numeric_term_name( $term_name ): void { + $this->make_user_by_role( 'editor' ); + + $parent_cat = wp_create_category( '2024' ); + wp_create_category( '2024', $parent_cat ); + + $post = array( + 'post_title' => 'Test', + 'terms_names' => array( + 'category' => array( $term_name ), + ), + ); + $result = $this->myxmlrpcserver->wp_newPost( array( 1, 'editor', 'editor', $post ) ); + + $this->assertIXRError( $result ); + $this->assertSame( 401, $result->code ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_terms_names_ambiguous_numeric_term_name(): array { + return array( + 'string' => array( '2024' ), + 'integer' => array( 2024 ), + ); + } + /** * @ticket 28601 */ From 4188078e66a4814fe7a3927284ad2308aa4f0ec0 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 29 Oct 2026 18:29:17 -0700 Subject: [PATCH 08/17] Reject boolean term names in wp_insert_term() The is_scalar() guard let booleans through, so true was inserted or matched as a term named "1". Accept only strings, ints, and floats. Rename the test to reflect that it covers invalid types rather than just non-scalars, add true, false, and null cases, and correct the ignore comment, which said scalars were being passed. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/taxonomy.php | 4 ++-- tests/phpunit/tests/term/wpInsertTerm.php | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index 50bfce081f10f..d02ad240d49f1 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -2545,7 +2545,7 @@ function wp_get_object_terms( $object_ids, $taxonomies, $args = array() ) { * @global wpdb $wpdb WordPress database abstraction object. * * @since 2.3.0 - * @since 7.2.0 A non-scalar `$term` now returns a WP_Error instead of causing a fatal error. + * @since 7.2.0 A `$term` that is not a string or number now returns a WP_Error instead of causing a fatal error. * * @param string $term The term name to add. * @param string $taxonomy The taxonomy to which to add the term. @@ -2603,7 +2603,7 @@ function wp_insert_term( $term, $taxonomy, $args = array() ) { return new WP_Error( 'invalid_term_id', __( 'Invalid term ID.' ) ); } - if ( ! is_scalar( $term ) || '' === trim( $term ) ) { + if ( ! ( is_string( $term ) || is_int( $term ) || is_float( $term ) ) || '' === trim( (string) $term ) ) { return new WP_Error( 'empty_term_name', __( 'A name is required for this term.' ) ); } diff --git a/tests/phpunit/tests/term/wpInsertTerm.php b/tests/phpunit/tests/term/wpInsertTerm.php index 7cf917e72e38f..7bdb2756c9701 100644 --- a/tests/phpunit/tests/term/wpInsertTerm.php +++ b/tests/phpunit/tests/term/wpInsertTerm.php @@ -87,30 +87,33 @@ public function test_wp_insert_term_term_trims_to_empty_string() { } /** - * Tests that a non-scalar term name returns a WP_Error instead of causing a fatal error. + * Tests that a term name that is not a string or number returns a WP_Error instead of causing a fatal error. * * @ticket 66108 * - * @dataProvider data_wp_insert_term_non_scalar_term_name + * @dataProvider data_wp_insert_term_invalid_term_name_type * * @param mixed $name The term name to insert. */ - public function test_wp_insert_term_non_scalar_term_name( $name ): void { - $found = wp_insert_term( $name, 'post_tag' ); // @phpstan-ignore argument.type (Intentionally passing scalars.) + public function test_wp_insert_term_invalid_term_name_type( $name ): void { + $found = wp_insert_term( $name, 'post_tag' ); // @phpstan-ignore argument.type (Intentionally passing invalid types.) $this->assertWPError( $found ); $this->assertSame( 'empty_term_name', $found->get_error_code() ); } /** - * Data provider for {@see self::test_wp_insert_term_non_scalar_term_name()} + * Data provider for {@see self::test_wp_insert_term_invalid_term_name_type()}. * * @return array */ - public function data_wp_insert_term_non_scalar_term_name(): array { + public function data_wp_insert_term_invalid_term_name_type(): array { return array( 'array' => array( array( 'foo', 'bar' ) ), 'object' => array( new stdClass() ), + 'true' => array( true ), + 'false' => array( false ), + 'null' => array( null ), ); } From 1f8bcc42d61d0f80aaa46767a865f0b204f12013 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 29 Oct 2026 18:29:50 -0700 Subject: [PATCH 09/17] Document numeric term names in wp_insert_term() The function accepts ints and floats and stores them as strings, which the tests now assert, but the docblock declared only string. Widen the $term type to string|int|float so the contract is explicit, which also makes the PHPStan ignore in the numeric test unnecessary. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/taxonomy.php | 6 +++--- tests/phpunit/tests/term/wpInsertTerm.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index d02ad240d49f1..31b86f4664b06 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -2547,9 +2547,9 @@ function wp_get_object_terms( $object_ids, $taxonomies, $args = array() ) { * @since 2.3.0 * @since 7.2.0 A `$term` that is not a string or number now returns a WP_Error instead of causing a fatal error. * - * @param string $term The term name to add. - * @param string $taxonomy The taxonomy to which to add the term. - * @param array|string $args { + * @param string|int|float $term The term name to add. A number is stored as its string form. + * @param string $taxonomy The taxonomy to which to add the term. + * @param array|string $args { * Optional. Array or query string of arguments for inserting a term. * * @type string $alias_of Slug of the term to make this term an alias of. diff --git a/tests/phpunit/tests/term/wpInsertTerm.php b/tests/phpunit/tests/term/wpInsertTerm.php index 7bdb2756c9701..3943bddf0c563 100644 --- a/tests/phpunit/tests/term/wpInsertTerm.php +++ b/tests/phpunit/tests/term/wpInsertTerm.php @@ -127,7 +127,7 @@ public function data_wp_insert_term_invalid_term_name_type(): array { * @param int|float $name The term name to insert. */ public function test_wp_insert_term_numeric_term_name( $name ): void { - $found = wp_insert_term( $name, 'post_tag' ); // @phpstan-ignore argument.type (Intentionally passing scalars.) + $found = wp_insert_term( $name, 'post_tag' ); $this->assertNotWPError( $found ); $term = get_term( $found['term_id'], 'post_tag' ); From 19ef32bc06e70e5dd92661c4ceafa989ab3c9ce0 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 29 Oct 2026 18:32:35 -0700 Subject: [PATCH 10/17] Return invalid_term_name for wrong-type term names A term name of the wrong type, such as an array, an object, or true, returned empty_term_name ("A name is required for this term."), which misdescribes the problem: a name was given, just not a usable one. Return the existing invalid_term_name code instead, so callers can tell a missing name from a malformed one. Null and false still return empty_term_name, as they did before this change, since they reduced to an empty string under trim(). Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/taxonomy.php | 9 ++++++++- tests/phpunit/tests/term/wpInsertTerm.php | 19 ++++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index 31b86f4664b06..caafcc5a3f030 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -2603,7 +2603,14 @@ function wp_insert_term( $term, $taxonomy, $args = array() ) { return new WP_Error( 'invalid_term_id', __( 'Invalid term ID.' ) ); } - if ( ! ( is_string( $term ) || is_int( $term ) || is_float( $term ) ) || '' === trim( (string) $term ) ) { + // Null and false are treated as a missing name rather than an invalid one, as they were before. + if ( null === $term || false === $term ) { + $term = ''; + } elseif ( ! ( is_string( $term ) || is_int( $term ) || is_float( $term ) ) ) { + return new WP_Error( 'invalid_term_name', __( 'Invalid term name.' ) ); + } + + if ( '' === trim( (string) $term ) ) { return new WP_Error( 'empty_term_name', __( 'A name is required for this term.' ) ); } diff --git a/tests/phpunit/tests/term/wpInsertTerm.php b/tests/phpunit/tests/term/wpInsertTerm.php index 3943bddf0c563..90b17c26168f5 100644 --- a/tests/phpunit/tests/term/wpInsertTerm.php +++ b/tests/phpunit/tests/term/wpInsertTerm.php @@ -93,27 +93,28 @@ public function test_wp_insert_term_term_trims_to_empty_string() { * * @dataProvider data_wp_insert_term_invalid_term_name_type * - * @param mixed $name The term name to insert. + * @param mixed $name The term name to insert. + * @param non-empty-string $expected_error_code The expected error code. */ - public function test_wp_insert_term_invalid_term_name_type( $name ): void { + public function test_wp_insert_term_invalid_term_name_type( $name, string $expected_error_code ): void { $found = wp_insert_term( $name, 'post_tag' ); // @phpstan-ignore argument.type (Intentionally passing invalid types.) $this->assertWPError( $found ); - $this->assertSame( 'empty_term_name', $found->get_error_code() ); + $this->assertSame( $expected_error_code, $found->get_error_code() ); } /** * Data provider for {@see self::test_wp_insert_term_invalid_term_name_type()}. * - * @return array + * @return array */ public function data_wp_insert_term_invalid_term_name_type(): array { return array( - 'array' => array( array( 'foo', 'bar' ) ), - 'object' => array( new stdClass() ), - 'true' => array( true ), - 'false' => array( false ), - 'null' => array( null ), + 'array' => array( array( 'foo', 'bar' ), 'invalid_term_name' ), + 'object' => array( new stdClass(), 'invalid_term_name' ), + 'true' => array( true, 'invalid_term_name' ), + 'false' => array( false, 'empty_term_name' ), + 'null' => array( null, 'empty_term_name' ), ); } From 51ab927ec16edc83fddbad4240c1321258458ba9 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 29 Oct 2026 18:33:22 -0700 Subject: [PATCH 11/17] Align newPost data provider docblocks with conventions Reference the test each provider feeds with {@see}, and narrow the key type to truthy-string, as done for the wp_insert_term() providers. Co-Authored-By: Claude Opus 5.5 --- tests/phpunit/tests/xmlrpc/wp/newPost.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/phpunit/tests/xmlrpc/wp/newPost.php b/tests/phpunit/tests/xmlrpc/wp/newPost.php index 20406cefe6bb6..ede0438aa3554 100644 --- a/tests/phpunit/tests/xmlrpc/wp/newPost.php +++ b/tests/phpunit/tests/xmlrpc/wp/newPost.php @@ -371,9 +371,9 @@ public function test_terms_names_invalid_term_name( $term_name ): void { } /** - * Data provider. + * Data provider for {@see self::test_terms_names_invalid_term_name()}. * - * @return array + * @return array */ public function data_terms_names_invalid_term_name(): array { return array( @@ -407,9 +407,9 @@ public function test_terms_names_numeric_term_name( $term_name ): void { } /** - * Data provider. + * Data provider for {@see self::test_terms_names_numeric_term_name()}. * - * @return array + * @return array */ public function data_terms_names_numeric_term_name(): array { return array( @@ -446,9 +446,9 @@ public function test_terms_names_ambiguous_numeric_term_name( $term_name ): void } /** - * Data provider. + * Data provider for {@see self::test_terms_names_ambiguous_numeric_term_name()}. * - * @return array + * @return array */ public function data_terms_names_ambiguous_numeric_term_name(): array { return array( From f406733f424c3339bb6adfb57b80108b59ba6e8a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 29 Oct 2026 18:37:05 -0700 Subject: [PATCH 12/17] Use 401 for invalid XML-RPC term names Every other validation error in the terms_names block, such as the ambiguous term name error, uses fault code 401. Use the same code for an invalid term name, so clients see consistent codes for this input. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/class-wp-xmlrpc-server.php | 2 +- tests/phpunit/tests/xmlrpc/wp/newPost.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index 425c5c24d779e..9e5f5278dfe82 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -1715,7 +1715,7 @@ protected function _insert_post( $user, $content_struct ) { if ( is_int( $term_name ) || is_float( $term_name ) ) { $term_name = (string) $term_name; } elseif ( ! is_string( $term_name ) ) { - return new IXR_Error( 403, __( 'Invalid term name.' ) ); + return new IXR_Error( 401, __( 'Invalid term name.' ) ); } if ( in_array( $term_name, $ambiguous_terms, true ) ) { diff --git a/tests/phpunit/tests/xmlrpc/wp/newPost.php b/tests/phpunit/tests/xmlrpc/wp/newPost.php index ede0438aa3554..8c9b0743a9051 100644 --- a/tests/phpunit/tests/xmlrpc/wp/newPost.php +++ b/tests/phpunit/tests/xmlrpc/wp/newPost.php @@ -366,7 +366,7 @@ public function test_terms_names_invalid_term_name( $term_name ): void { $result = $this->myxmlrpcserver->wp_newPost( array( 1, 'editor', 'editor', $post ) ); $this->assertIXRError( $result ); - $this->assertSame( 403, $result->code ); + $this->assertSame( 401, $result->code ); $this->assertSame( 'Invalid term name.', $result->message ); } From cb770c2ec75c4b0a79c8e2c23cc068309a085536 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 29 Oct 2026 18:38:01 -0700 Subject: [PATCH 13/17] Reject non-array terms and terms_names values in XML-RPC A per-taxonomy value sent as a single string or int, rather than a list, reached foreach unvalidated. That emitted a "foreach() argument must be of type array|object" warning, and the terms were silently ignored instead of an error being returned. Return the existing "Invalid term ID." (403) or "Invalid term name." (401) error instead, matching the codes already used for invalid items in each list. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/class-wp-xmlrpc-server.php | 10 +++++- tests/phpunit/tests/xmlrpc/wp/newPost.php | 41 ++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index 9e5f5278dfe82..c4de7c487e7b4 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -1658,7 +1658,11 @@ protected function _insert_post( $user, $content_struct ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to assign a term to one of the given taxonomies.' ) ); } - $term_ids = $post_data['terms'][ $taxonomy ]; + $term_ids = $post_data['terms'][ $taxonomy ]; + if ( ! is_array( $term_ids ) ) { + return new IXR_Error( 403, __( 'Invalid term ID.' ) ); + } + $terms[ $taxonomy ] = array(); foreach ( $term_ids as $term_id ) { $term = get_term_by( 'id', $term_id, $taxonomy ); @@ -1710,6 +1714,10 @@ protected function _insert_post( $user, $content_struct ) { } $term_names = $post_data['terms_names'][ $taxonomy ]; + if ( ! is_array( $term_names ) ) { + return new IXR_Error( 401, __( 'Invalid term name.' ) ); + } + foreach ( $term_names as $term_name ) { // Numeric names may arrive as or values. if ( is_int( $term_name ) || is_float( $term_name ) ) { diff --git a/tests/phpunit/tests/xmlrpc/wp/newPost.php b/tests/phpunit/tests/xmlrpc/wp/newPost.php index 8c9b0743a9051..bf4463df84bcd 100644 --- a/tests/phpunit/tests/xmlrpc/wp/newPost.php +++ b/tests/phpunit/tests/xmlrpc/wp/newPost.php @@ -382,6 +382,47 @@ public function data_terms_names_invalid_term_name(): array { ); } + /** + * Tests that a `terms` or `terms_names` value that is not an array returns an error. + * + * @ticket 66108 + * + * @dataProvider data_terms_not_array + * + * @param 'terms'|'terms_names' $field The post field to send. + * @param mixed $value The value to send for the taxonomy. + * @param int $expected_code The expected fault code. + * @param non-empty-string $expected_message The expected fault message. + */ + public function test_terms_not_array( string $field, $value, int $expected_code, string $expected_message ): void { + $this->make_user_by_role( 'editor' ); + + $post = array( + 'post_title' => 'Test', + $field => array( + 'post_tag' => $value, + ), + ); + $result = $this->myxmlrpcserver->wp_newPost( array( 1, 'editor', 'editor', $post ) ); + + $this->assertIXRError( $result ); + $this->assertSame( $expected_code, $result->code ); + $this->assertSame( $expected_message, $result->message ); + } + + /** + * Data provider for {@see self::test_terms_not_array()}. + * + * @return array + */ + public function data_terms_not_array(): array { + return array( + 'terms string' => array( 'terms', '1', 403, 'Invalid term ID.' ), + 'terms integer' => array( 'terms', 1, 403, 'Invalid term ID.' ), + 'terms_names string' => array( 'terms_names', 'foo', 401, 'Invalid term name.' ), + ); + } + /** * Tests that a numeric term name in `terms_names` is assigned using its string form. * From 88f20e13329693332d3515288a2a62bae4cea0b2 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 29 Oct 2026 19:20:28 -0700 Subject: [PATCH 14/17] Preserve argument types through wp_xmlrpc_server::escape() escape() takes its argument by reference with a string|array type, so after every `$this->escape( $args )` call PHPStan widened `$args` to string|array, and each `$args[N]` read became mixed. That discarded the argument shapes derived from the method docblocks and produced hundreds of errors across the class. Declare escape() with a template type for both the parameter and its by-reference output, so callers keep their argument shapes. This drops the file's level 10 error count from 659 to 448. PHPStan cannot prove that the by-reference loop preserves the shape, so that one error is ignored with a justification. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/class-wp-xmlrpc-server.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index c4de7c487e7b4..ec79327e8a52b 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -374,6 +374,10 @@ public function login_pass_ok( * @param string|array $data Escape single string or array of strings. * @return string|null Returns with string if passed, alters by-reference * when array is passed. + * + * @phpstan-template T of string|array + * @phpstan-param T $data + * @phpstan-param-out T $data */ public function escape( &$data ) { if ( ! is_array( $data ) ) { @@ -387,6 +391,8 @@ public function escape( &$data ) { $v = wp_slash( $v ); } } + + // @phpstan-ignore paramOut.type (The loop only slashes string values in place, so the shape of T is preserved.) return null; } From 7ac3e7ebf4ff3ed24d2228fbd8b04b2dec71f24e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 29 Oct 2026 19:20:55 -0700 Subject: [PATCH 15/17] Reject invalid term names in wp.newTerm and wp.editTerm Both methods passed the name straight to trim(), so an array name caused a fatal TypeError before wp_insert_term() or wp_update_term() was ever reached, and true was trimmed to "1". Return a 403 "Invalid term name." error unless the name is a string or number, matching the code these methods already use for an empty name. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/class-wp-xmlrpc-server.php | 8 ++++ tests/phpunit/tests/xmlrpc/wp/editTerm.php | 44 ++++++++++++++++++++++ tests/phpunit/tests/xmlrpc/wp/newTerm.php | 40 ++++++++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index ec79327e8a52b..25f57a5714dd7 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -2220,6 +2220,10 @@ public function wp_newTerm( $args ) { // Hold the data of the term. $term_data = array(); + if ( isset( $content_struct['name'] ) && ! ( is_string( $content_struct['name'] ) || is_int( $content_struct['name'] ) || is_float( $content_struct['name'] ) ) ) { + return new IXR_Error( 403, __( 'Invalid term name.' ) ); + } + $term_data['name'] = trim( $content_struct['name'] ); if ( empty( $term_data['name'] ) ) { return new IXR_Error( 403, __( 'The term name cannot be empty.' ) ); @@ -2338,6 +2342,10 @@ public function wp_editTerm( $args ) { } if ( isset( $content_struct['name'] ) ) { + if ( ! ( is_string( $content_struct['name'] ) || is_int( $content_struct['name'] ) || is_float( $content_struct['name'] ) ) ) { + return new IXR_Error( 403, __( 'Invalid term name.' ) ); + } + $term_data['name'] = trim( $content_struct['name'] ); if ( empty( $term_data['name'] ) ) { diff --git a/tests/phpunit/tests/xmlrpc/wp/editTerm.php b/tests/phpunit/tests/xmlrpc/wp/editTerm.php index 9b367681f3e95..a6a8e3e166027 100644 --- a/tests/phpunit/tests/xmlrpc/wp/editTerm.php +++ b/tests/phpunit/tests/xmlrpc/wp/editTerm.php @@ -4,6 +4,9 @@ * @group xmlrpc */ class Tests_XMLRPC_wp_editTerm extends WP_XMLRPC_UnitTestCase { + /** + * @var int + */ protected static $parent_term; protected static $child_term; protected static $post_tag; @@ -114,6 +117,47 @@ public function test_empty_term() { $this->assertSame( __( 'Empty Term.' ), $result->message ); } + /** + * Tests that a term name that is not a string or number returns an error instead of causing a fatal error. + * + * @ticket 66108 + * + * @dataProvider data_invalid_term_name + * + * @param mixed $name The term name to send. + */ + public function test_invalid_term_name( $name ): void { + $this->make_user_by_role( 'editor' ); + + $result = $this->myxmlrpcserver->wp_editTerm( + array( + 1, + 'editor', + 'editor', + self::$parent_term, + array( + 'taxonomy' => 'category', + 'name' => $name, + ), + ) + ); + $this->assertIXRError( $result ); + $this->assertSame( 403, $result->code ); + $this->assertSame( 'Invalid term name.', $result->message ); + } + + /** + * Data provider for {@see self::test_invalid_term_name()}. + * + * @return array + */ + public function data_invalid_term_name(): array { + return array( + 'array' => array( array( 'foo' ) ), + 'boolean' => array( true ), + ); + } + public function test_empty_term_name() { $this->make_user_by_role( 'editor' ); diff --git a/tests/phpunit/tests/xmlrpc/wp/newTerm.php b/tests/phpunit/tests/xmlrpc/wp/newTerm.php index fdd9a4803e7b4..a9a44a0384603 100644 --- a/tests/phpunit/tests/xmlrpc/wp/newTerm.php +++ b/tests/phpunit/tests/xmlrpc/wp/newTerm.php @@ -48,6 +48,46 @@ public function test_incapable_user() { $this->assertSame( __( 'Sorry, you are not allowed to create terms in this taxonomy.' ), $result->message ); } + /** + * Tests that a term name that is not a string or number returns an error instead of causing a fatal error. + * + * @ticket 66108 + * + * @dataProvider data_invalid_term_name + * + * @param mixed $name The term name to send. + */ + public function test_invalid_term_name( $name ): void { + $this->make_user_by_role( 'editor' ); + + $result = $this->myxmlrpcserver->wp_newTerm( + array( + 1, + 'editor', + 'editor', + array( + 'taxonomy' => 'category', + 'name' => $name, + ), + ) + ); + $this->assertIXRError( $result ); + $this->assertSame( 403, $result->code ); + $this->assertSame( 'Invalid term name.', $result->message ); + } + + /** + * Data provider for {@see self::test_invalid_term_name()}. + * + * @return array + */ + public function data_invalid_term_name(): array { + return array( + 'array' => array( array( 'foo' ) ), + 'boolean' => array( true ), + ); + } + public function test_empty_term() { $this->make_user_by_role( 'editor' ); From 7f728ca2d2859b5b181f12583a9ec35deb197846 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 29 Oct 2026 19:21:32 -0700 Subject: [PATCH 16/17] Reject invalid category names in wp.newCategory The category name was passed to wp_insert_category(), whose trim() caused a fatal TypeError for an array name. Return a 403 "Invalid term name." error unless the name is a string or number, matching the term methods. There were no tests for wp.newCategory, so add a test class covering both a valid name and invalid name types. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/class-wp-xmlrpc-server.php | 4 ++ tests/phpunit/tests/xmlrpc/wp/newCategory.php | 57 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 tests/phpunit/tests/xmlrpc/wp/newCategory.php diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index 25f57a5714dd7..9416427810c46 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -3571,6 +3571,10 @@ public function wp_newCategory( $args ) { return new IXR_Error( 401, __( 'Sorry, you are not allowed to add a category.' ) ); } + if ( isset( $category['name'] ) && ! ( is_string( $category['name'] ) || is_int( $category['name'] ) || is_float( $category['name'] ) ) ) { + return new IXR_Error( 403, __( 'Invalid term name.' ) ); + } + /* * If no slug was provided, make it empty * so that WordPress will generate one. diff --git a/tests/phpunit/tests/xmlrpc/wp/newCategory.php b/tests/phpunit/tests/xmlrpc/wp/newCategory.php new file mode 100644 index 0000000000000..bc76e5571df91 --- /dev/null +++ b/tests/phpunit/tests/xmlrpc/wp/newCategory.php @@ -0,0 +1,57 @@ +make_user_by_role( 'editor' ); + + $result = $this->myxmlrpcserver->wp_newCategory( array( 1, 'editor', 'editor', array( 'name' => 'Foo' ) ) ); + + $this->assertNotIXRError( $result ); + $this->assertIsInt( $result ); + $category = get_term( $result, 'category' ); + $this->assertInstanceOf( WP_Term::class, $category ); + $this->assertSame( 'Foo', $category->name ); + } + + /** + * Tests that a category name that is not a string or number returns an error instead of causing a fatal error. + * + * @ticket 66108 + * + * @dataProvider data_invalid_category_name + * + * @param mixed $name The category name to send. + */ + public function test_invalid_category_name( $name ): void { + $this->make_user_by_role( 'editor' ); + + $result = $this->myxmlrpcserver->wp_newCategory( array( 1, 'editor', 'editor', array( 'name' => $name ) ) ); + + $this->assertIXRError( $result ); + $this->assertSame( 403, $result->code ); + $this->assertSame( 'Invalid term name.', $result->message ); + } + + /** + * Data provider for {@see self::test_invalid_category_name()}. + * + * @return array + */ + public function data_invalid_category_name(): array { + return array( + 'array' => array( array( 'foo' ) ), + 'boolean' => array( true ), + ); + } +} From 7a115d9424868f3315773ca9084941f249d5f2d5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 29 Oct 2026 19:23:45 -0700 Subject: [PATCH 17/17] Reject invalid term types in wp_set_object_terms() A term that was not a string or number, such as a nested array in a metaWeblog.newPost or metaWeblog.editPost mt_keywords list, reached trim() and caused a fatal TypeError. Validate every term up front and return an invalid_term_name error before any terms or relationships are changed, so a bad item cannot leave the object half updated. Null and false are still skipped, as before. Cast to string before the existing trim() so skipping null no longer emits a PHP 8.1 deprecation notice. The metaWeblog methods ignore the result of setting tags, so the post is still saved, without the requested tags, rather than crashing. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/taxonomy.php | 10 +++- tests/phpunit/tests/term/wpSetObjectTerms.php | 52 +++++++++++++++++++ tests/phpunit/tests/xmlrpc/mw/editPost.php | 20 +++++++ tests/phpunit/tests/xmlrpc/mw/newPost.php | 18 +++++++ 4 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index caafcc5a3f030..cc1202765fe8a 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -2965,6 +2965,7 @@ function wp_insert_term( $term, $taxonomy, $args = array() ) { * exists under. * * @since 2.3.0 + * @since 7.2.0 A term that is not a string or number now returns a WP_Error instead of causing a fatal error. * * @global wpdb $wpdb WordPress database abstraction object. * @@ -2991,6 +2992,13 @@ function wp_set_object_terms( $object_id, $terms, $taxonomy, $append = false ) { $terms = array( $terms ); } + // Validate all terms before making any changes. Null and false are skipped below, as they were before. + foreach ( $terms as $term ) { + if ( ! ( is_string( $term ) || is_int( $term ) || is_float( $term ) || null === $term || false === $term ) ) { + return new WP_Error( 'invalid_term_name', __( 'Invalid term name.' ) ); + } + } + if ( ! $append ) { $old_tt_ids = wp_get_object_terms( $object_id, @@ -3009,7 +3017,7 @@ function wp_set_object_terms( $object_id, $terms, $taxonomy, $append = false ) { $new_tt_ids = array(); foreach ( (array) $terms as $term ) { - if ( '' === trim( $term ) ) { + if ( '' === trim( (string) $term ) ) { continue; } diff --git a/tests/phpunit/tests/term/wpSetObjectTerms.php b/tests/phpunit/tests/term/wpSetObjectTerms.php index 3734b00828ba1..487578597da09 100644 --- a/tests/phpunit/tests/term/wpSetObjectTerms.php +++ b/tests/phpunit/tests/term/wpSetObjectTerms.php @@ -7,6 +7,10 @@ */ class Tests_Term_WpSetObjectTerms extends WP_UnitTestCase { protected static $taxonomy = 'category'; + + /** + * @var int[] + */ protected static $post_ids = array(); protected static $term_ids = array(); @@ -15,6 +19,54 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { self::$term_ids = $factory->term->create_many( 5, array( 'taxonomy' => self::$taxonomy ) ); } + /** + * Tests that a term that is not a string or number returns a WP_Error without changing existing terms. + * + * @ticket 66108 + * + * @dataProvider data_invalid_term_type + * + * @param mixed $term The invalid term to set alongside a valid one. + */ + public function test_invalid_term_type( $term ): void { + $post_id = self::$post_ids[0]; + wp_set_object_terms( $post_id, array( 'existing' ), 'post_tag' ); + + $result = wp_set_object_terms( $post_id, array( 'new', $term ), 'post_tag' ); + + $this->assertWPError( $result ); + $this->assertSame( 'invalid_term_name', $result->get_error_code() ); + $this->assertSame( array( 'existing' ), wp_get_object_terms( $post_id, 'post_tag', array( 'fields' => 'names' ) ) ); + $this->assertNull( term_exists( 'new', 'post_tag' ) ); + } + + /** + * Data provider for {@see self::test_invalid_term_type()}. + * + * @return array + */ + public function data_invalid_term_type(): array { + return array( + 'array' => array( array( 'foo' ) ), + 'object' => array( new stdClass() ), + 'true' => array( true ), + ); + } + + /** + * Tests that null and false terms are skipped rather than rejected. + * + * @ticket 66108 + */ + public function test_null_and_false_terms_are_skipped(): void { + $post_id = self::$post_ids[0]; + + $result = wp_set_object_terms( $post_id, array( 'foo', null, false ), 'post_tag' ); + + $this->assertNotWPError( $result ); + $this->assertSame( array( 'foo' ), wp_get_object_terms( $post_id, 'post_tag', array( 'fields' => 'names' ) ) ); + } + /** * @ticket 26570 */ diff --git a/tests/phpunit/tests/xmlrpc/mw/editPost.php b/tests/phpunit/tests/xmlrpc/mw/editPost.php index 23515233325ff..3cd4101212c6b 100644 --- a/tests/phpunit/tests/xmlrpc/mw/editPost.php +++ b/tests/phpunit/tests/xmlrpc/mw/editPost.php @@ -30,6 +30,26 @@ public function test_edit_own_post() { $this->assertSame( $new_title, $out->post_title ); } + /** + * Tests that a keyword that is not a string or number does not cause a fatal error. + * + * @ticket 66108 + */ + public function test_invalid_keyword(): void { + $author_id = $this->make_user_by_role( 'author' ); + $post_id = self::factory()->post->create( array( 'post_author' => $author_id ) ); + wp_set_post_tags( $post_id, array( 'existing' ) ); + + $post = array( + 'title' => 'Test', + 'mt_keywords' => array( 'foo', array( 'bar' ) ), + ); + $result = $this->myxmlrpcserver->mw_editPost( array( $post_id, 'author', 'author', $post ) ); + + $this->assertNotIXRError( $result ); + $this->assertSame( array( 'existing' ), wp_get_post_tags( $post_id, array( 'fields' => 'names' ) ) ); + } + public function test_capable_edit_others_post() { $this->make_user_by_role( 'editor' ); $contributor_id = $this->make_user_by_role( 'contributor' ); diff --git a/tests/phpunit/tests/xmlrpc/mw/newPost.php b/tests/phpunit/tests/xmlrpc/mw/newPost.php index e7c4af0cee01f..50dbd476b5f36 100644 --- a/tests/phpunit/tests/xmlrpc/mw/newPost.php +++ b/tests/phpunit/tests/xmlrpc/mw/newPost.php @@ -21,6 +21,24 @@ public function test_incapable_user() { $this->assertSame( 401, $result->code ); } + /** + * Tests that a keyword that is not a string or number does not cause a fatal error. + * + * @ticket 66108 + */ + public function test_invalid_keyword(): void { + $this->make_user_by_role( 'author' ); + + $post = array( + 'title' => 'Test', + 'mt_keywords' => array( 'foo', array( 'bar' ) ), + ); + $result = $this->myxmlrpcserver->mw_newPost( array( 1, 'author', 'author', $post ) ); + + $this->assertNotIXRError( $result ); + $this->assertSame( array(), wp_get_post_tags( (int) $result, array( 'fields' => 'names' ) ) ); + } + public function test_no_content() { $this->make_user_by_role( 'author' );