Make WordPress Core

Opened 5 weeks ago

Last modified 5 weeks ago

#65805 new defect (bug)

wp_set_object_terms() casts numeric term IDs to strings via array_unique(), causing false matching by term name

Reported by: badwolfgr Owned by:
Priority: normal Milestone: Awaiting Review
Component: Taxonomy Version: 7.0.2
Severity: normal Keywords: reporter-feedback
Cc: Focuses:

Description

When passing an array of integer term IDs to wp_set_object_terms() where the term IDs represent terms that consist purely of numbers (e.g. term ID 725 for a color named "01"), WordPress incorrectly treats the term ID as a term name.

Steps to reproduce:

  1. Create a term in a taxonomy (e.g. 'pa_color') with the name "01". Assume it is assigned Term ID 725.
  2. Create a product or post.
  3. Pass the integer term ID in an array to wp_set_object_terms( $post_id, [ 725 ], 'pa_color' ).

Expected behavior:
WordPress should assign Term ID 725 (Name: "01") to the post, because passing integers to wp_set_object_terms should be treated as Term IDs.

Actual behavior:
Inside wp_set_object_terms(), the $terms array is passed through array_unique(). Because PHP's array_unique() defaults to SORT_STRING, it silently casts the integer 725 into the string "725".
Then, inside the foreach loop, $term is a string. When passed to term_exists( $term, $taxonomy ), WordPress searches for a term with the Name "725" instead of the ID 725.
If a term named "725" doesn't exist, it incorrectly creates a brand new term named "725".

Proposed Fix:
In wp-includes/taxonomy.php inside wp_set_object_terms(), the array_unique() call should use SORT_REGULAR or strictly preserve integers so numeric Term IDs are not cast to strings before being evaluated by term_exists().

Change History (1)

#1 @debarghyabanerjee
5 weeks ago

  • Keywords reporter-feedback added

Hi @badwolfgr, thanks for the report.

I went looking for that array_unique() call and I can't find it. wp_set_object_terms() doesn't have one. Here's the relevant stretch on 7.0:

<?php
2820: function wp_set_object_terms( $object_id, $terms, $taxonomy, $append = false ) {
...
2853:         if ( '' === trim( $term ) ) {
2857:         $term_info = term_exists( $term, $taxonomy );
2861:             if ( is_int( $term ) ) {   // Skip if a non-existent term ID is passed.

It goes straight from the is_array() check into the loop. taxonomy.php does use array_unique() in four spots (3567, 3699, 3850, 4160), but those are deferred term counts, taxonomy lists, cache priming and object types. None of them ever see $terms.

The other thing is array_unique() wouldn't cast anything even if it were there. SORT_STRING only affects how elements get compared; what comes back keeps its original types. So array_unique( array( 725 ) ) hands you back int 725, and SORT_REGULAR wouldn't change a thing.

You might be thinking of wp_set_post_terms(), which does call it:

<?php
// src/wp-includes/post.php
5735: if ( is_taxonomy_hierarchical( $taxonomy ) ) {
5736:     $terms = array_unique( array_map( 'intval', $terms ) );

but intval() runs first there and the ints come through fine. Core counts on that, actually. If array_unique() stringified its output, the is_int() guard at taxonomy.php:2861 would never fire for hierarchical taxonomiy ID would create a categorycalled 999. There's a test pinning that at tests/phpunit/tests/term/wpSetObjectTerms.php:428.

What I suspect you're actually hitting is a string. term_exists() checks is_int() at line 1637, and anything that isn't a real int gets looked up as a slug and then as a name. The
guard at 2861 only skips ints, hes nothing falls through towp_insert_term() and you get a brand new term. Which is what you're seeing.

These two do completely different things:

<?php
wp_set_object_terms( $post_id, array( 725 ),   'pa_color' ); // assigns term 725
wp_set_object_terms( $post_id, array( '725' ), 'pa_color' ); // looks for slug/name "725",
creates it

Could you var_dump() the array right before the call? print_r() renders 725 and '725' identically so it's easy to miss. Strings sneak in from $wpdb results (every column comes back as a string), $_POST, RESTs, array_keys().

The pa_color detail might not bt_post_terms() only does theintval() pass for hierarchical taxonomies (post.php:5735), and WooCommerce's pa_* attributes
aren't hierarchical, so nothing.

To unblock yourself:

<?php
$term_ids = array_map( 'intval', (array) $term_ids );
wp_set_object_terms( $post_id, $term_ids, 'pa_color' );
Last edited 5 weeks ago by debarghyabanerjee (previous) (diff)
Note: See TracTickets for help on using tickets.