[wp-trac] [WordPress Trac] #65055: _pad_term_counts() uses string-concatenated SQL without prepared statement

WordPress Trac noreply at wordpress.org
Tue Apr 14 10:00:34 UTC 2026


#65055: _pad_term_counts() uses string-concatenated SQL without prepared statement
--------------------------+------------------------------
 Reporter:  rajeshcp      |       Owner:  rajeshcp
     Type:  defect (bug)  |      Status:  assigned
 Priority:  normal        |   Milestone:  Awaiting Review
Component:  Database      |     Version:  trunk
 Severity:  major         |  Resolution:
 Keywords:  has-patch     |     Focuses:
--------------------------+------------------------------

Comment (by liaison):

 Replying to [comment:5 abcd95]:
 > Replying to [comment:4 liaison]:
 >
 > Thanks @liaison! agreed on the alignment with _update_post_term_count().
 The updated patch incorporating post_type_exists() filtering looks solid.

 Test Report:

 I verified the fix using a zero-dependency test script that loads the core
 wp-includes/taxonomy.php while mocking the database environment.

 Before Patch:
 The logic uses direct string interpolation and includes unregistered post
 types in the query.

 Method: get_results
 Query: ... AND post_type IN ('post', 'ghost_type') ...
 ❌ FAILED: 'ghost_type' present in raw SQL string.
 After Patch:
 The logic now correctly filters post types via post_type_exists() and uses
 wpdb::prepare() for the final query.

 Method: prepare
 Arguments: 123, post
 ✅ SUCCESS: 'ghost_type' filtered from prepare arguments.

 test-65055-sql-filter.php
 {{{#!php
 <?php
 <?php
 /**
  * Trac #65055: Zero-Dependency Test for Core wp-includes/taxonomy.php
  * Supports both patched (prepare) and unpatched (direct query) versions.
  */

 define( 'ABSPATH', __DIR__ . '/' );
 define( 'WPINC', 'wp-includes' );

 /**
  * 1. Robust Mock_WPDB
  * Handles table name properties and captures both prepare/get_results.
  */
 class Mock_WPDB {
     public $queries = [];
     public $prefix = 'wp_';

     // Core table names used in _pad_term_counts
     public $term_relationships = 'wp_term_relationships';
     public $posts = 'wp_posts';

     public function prepare( $query, ...$args ) {
         // Flatten arguments if they were passed as an array
         $flat_args = ( is_array( $args[0] ) && count( $args ) === 1 ) ?
 $args[0] : $args;
         $this->queries[] = [
             'method' => 'prepare',
             'query'  => $query,
             'args'   => $flat_args
         ];
         return $query;
     }

     public function get_results( $query ) {
         // If query was not recorded by prepare(), it's a direct string
 query
         $already_recorded = false;
         foreach ( $this->queries as $q ) {
             if ( $q['query'] === $query ) {
                 $already_recorded = true;
                 break;
             }
         }

         if ( ! $already_recorded ) {
             $this->queries[] = [
                 'method' => 'get_results',
                 'query'  => $query,
                 'args'   => []
             ];
         }

         // Return empty result to satisfy the function
         return array();
     }
 }

 $GLOBALS['wpdb'] = new Mock_WPDB();

 /**
  * 2. Mock Global Functions (Stubbing)
  */
 if ( ! function_exists( 'post_type_exists' ) ) {
     function post_type_exists( $post_type ) {
         return in_array( $post_type, array( 'post' ), true );
     }
 }

 if ( ! function_exists( 'wp_installing' ) ) {
     function wp_installing() { return false; }
 }

 if ( ! function_exists( 'esc_sql' ) ) {
     function esc_sql( $data ) { return $data; }
 }

 if ( ! function_exists( 'get_option' ) ) {
     function get_option( $option, $default = false ) {
         // Force get_term_hierarchy to believe Term 123 has a child 999
         if ( strpos( $option, '_children' ) !== false ) {
             return array( 123 => array( 999 ) );
         }
         return array();
     }
 }

 /**
  * 3. Load actual Core file
  */
 if ( file_exists( ABSPATH . WPINC . '/taxonomy.php' ) ) {
     require_once ABSPATH . WPINC . '/taxonomy.php';
 } else {
     die( "❌ Error: wp-includes/taxonomy.php not found. Check your
 ABSPATH.\n" );
 }

 /**
  * 4. Setup Global State
  */
 global $wp_taxonomies;
 $test_tax = new stdClass();
 $test_tax->name         = 'test_tax';
 $test_tax->object_type  = array( 'post', 'ghost_type' ); // Mixed
 valid/invalid
 $test_tax->hierarchical = true;
 $wp_taxonomies['test_tax'] = $test_tax;

 /**
  * 5. Execute Test
  */
 echo "--- Starting Core _pad_term_counts() Test ---\n";

 $term = new stdClass();
 $term->term_id          = 123;
 $term->term_taxonomy_id = 123;
 $term->parent           = 0;
 $term->count            = 0;

 $terms_array = array( $term );
 _pad_term_counts( $terms_array, 'test_tax' );
 /**
  * 6. Final Analysis
  */
 $last_execution = end( $GLOBALS['wpdb']->queries );

 if ( ! $last_execution ) {
     die( "❌ FAILED: No database activity detected.\n" );
 }

 echo "\n[Execution Result]\n";
 echo "Method: " . $last_execution['method'] . "\n";

 if ( $last_execution['method'] === 'prepare' ) {
     $args = $last_execution['args'];
     echo "Arguments: " . implode( ', ', $args ) . "\n";

     if ( in_array( 'ghost_type', $args, true ) ) {
         echo "❌ FAILED: 'ghost_type' remains in prepare arguments.\n";
     } else {
         echo "✅ SUCCESS: 'ghost_type' filtered from prepare
 arguments.\n";
     }
 } else {
     echo "Query: " . $last_execution['query'] . "\n";

     if ( strpos( $last_execution['query'], 'ghost_type' ) !== false ) {
         echo "❌ FAILED: 'ghost_type' present in raw SQL string.\n";
     } else {
         echo "✅ SUCCESS: 'ghost_type' not found in SQL string.\n";
     }
 }

 echo "\n--- Test Completed ---\n";



 }}}

-- 
Ticket URL: <https://core.trac.wordpress.org/ticket/65055#comment:6>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform


More information about the wp-trac mailing list