Make WordPress Core

Changeset 60788

Timestamp:
09/21/2025 05:25:49 AM (3 weeks ago)
Author:
westonruter
Message:

Posts, Post Types: Improve wp_count_posts() query performance for users who cannot read_private_posts.

The query is refactored to use two subqueries which can leverage DB indexes.

Props rcorrales, snehapatil02, sirlouen, sajjad67, pbearne, johnbillion, westonruter.
Fixes #61097.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/post.php

    r60724 r60788  
    34013401    }
    34023402
    3403     $query = "SELECT post_status, COUNT( * ) AS num_posts FROM {$wpdb->posts} WHERE post_type = %s";
    3404 
    3405     if ( 'readable' === $perm && is_user_logged_in() ) {
    3406         $post_type_object = get_post_type_object( $type );
    3407         if ( ! current_user_can( $post_type_object->cap->read_private_posts ) ) {
    3408             $query .= $wpdb->prepare(
    3409                 " AND (post_status != 'private' OR ( post_author = %d AND post_status = 'private' ))",
    3410                 get_current_user_id()
    3411             );
    3412         }
    3413     }
    3414 
    3415     $query .= ' GROUP BY post_status';
    3416 
    3417     $results = (array) $wpdb->get_results( $wpdb->prepare( $query, $type ), ARRAY_A );
     3403    if (
     3404        'readable' === $perm &&
     3405        is_user_logged_in() &&
     3406        ! current_user_can( get_post_type_object( $type )->cap->read_private_posts )
     3407    ) {
     3408        // Optimized query uses subqueries which can leverage DB indexes for better performance. See #61097.
     3409        $query = $wpdb->prepare(
     3410            "
     3411            SELECT post_status, COUNT(*) AS num_posts
     3412            FROM (
     3413                SELECT post_status
     3414                FROM {$wpdb->posts}
     3415                WHERE post_type = %s AND post_status != 'private'
     3416                UNION ALL
     3417                SELECT post_status
     3418                FROM {$wpdb->posts}
     3419                WHERE post_type = %s AND post_status = 'private' AND post_author = %d
     3420            ) AS filtered_posts
     3421            ",
     3422            $type,
     3423            $type,
     3424            get_current_user_id()
     3425        );
     3426    } else {
     3427        $query = $wpdb->prepare(
     3428            "
     3429            SELECT post_status, COUNT(*) AS num_posts
     3430            FROM {$wpdb->posts}
     3431            WHERE post_type = %s
     3432            ",
     3433            $type
     3434        );
     3435    }
     3436
     3437    $query  .= ' GROUP BY post_status';
     3438    $results = (array) $wpdb->get_results( $query, ARRAY_A );
    34183439    $counts  = array_fill_keys( get_post_stati(), 0 );
    34193440
  • trunk/tests/phpunit/tests/post.php

    r60251 r60788  
    77 */
    88class Tests_Post extends WP_UnitTestCase {
    9     protected static $editor_id;
    109    protected static $grammarian_id;
    1110
     11    protected static $user_ids = array(
     12        'administrator' => null,
     13        'editor'        => null,
     14        'author'        => null,
     15        'contributor'   => null,
     16    );
     17
    1218    private $post_ids = array();
    1319
    1420    public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
    15         self::$editor_id = $factory->user->create( array( 'role' => 'editor' ) );
     21
     22        self::$user_ids = array(
     23            'administrator' => $factory->user->create(
     24                array(
     25                    'role' => 'administrator',
     26                )
     27            ),
     28            'editor'        => $factory->user->create(
     29                array(
     30                    'role' => 'editor',
     31                )
     32            ),
     33            'author'        => $factory->user->create(
     34                array(
     35                    'role' => 'author',
     36                )
     37            ),
     38            'contributor'   => $factory->user->create(
     39                array(
     40                    'role' => 'contributor',
     41                )
     42            ),
     43        );
    1644
    1745        add_role(
     
    143171    /**
    144172     * @ticket 24803
     173     * @covers ::wp_count_posts
    145174     */
    146175    public function test_wp_count_posts() {
     
    162191    }
    163192
     193    /**
     194     * Ensure `wp_count_posts()` in 'readable' context excludes private posts
     195     * authored by other users when the current user lacks the capability to
     196     * read private posts.
     197     *
     198     * @ticket 61097
     199     * @covers ::wp_count_posts
     200     */
     201    public function test_wp_count_posts_readable_excludes_unreadable_private_posts() {
     202        $post_type = rand_str( 20 );
     203        register_post_type( $post_type );
     204
     205        $admin_user_id = self::$user_ids['administrator'];
     206
     207        self::factory()->post->create_many(
     208            5,
     209            array(
     210                'post_type'   => $post_type,
     211                'post_status' => 'publish',
     212                'post_author' => $admin_user_id,
     213            )
     214        );
     215
     216        self::factory()->post->create_many(
     217            3,
     218            array(
     219                'post_type'   => $post_type,
     220                'post_status' => 'private',
     221                'post_author' => $admin_user_id,
     222            )
     223        );
     224
     225        $current_user_id = self::$user_ids['author'];
     226        wp_set_current_user( $current_user_id );
     227
     228        $count = wp_count_posts( $post_type, 'readable' );
     229        $this->assertEquals( 5, $count->publish );
     230        _unregister_post_type( $post_type );
     231    }
     232
     233    /**
     234     * @covers ::wp_count_posts
     235     */
    164236    public function test_wp_count_posts_filtered() {
    165237        $post_type = rand_str( 20 );
     
    187259    }
    188260
     261    /**
     262     * @covers ::wp_count_posts
     263     */
    189264    public function test_wp_count_posts_insert_invalidation() {
    190265        $post_ids       = self::factory()->post->create_many( 3 );
     
    207282    }
    208283
     284    /**
     285     * @covers ::wp_count_posts
     286     */
    209287    public function test_wp_count_posts_trash_invalidation() {
    210288        $post_ids       = self::factory()->post->create_many( 3 );
     
    227305    /**
    228306     * @ticket 49685
     307     * @covers ::wp_count_posts
    229308     */
    230309    public function test_wp_count_posts_status_changes_visible() {
     
    253332        wp_set_object_terms( $post, 'foo', $tax );
    254333
    255         wp_set_current_user( self::$editor_id );
     334        wp_set_current_user( self::$user_ids['editor'] );
    256335
    257336        $wp_tag_cloud = wp_tag_cloud(
     
    306385        );
    307386
    308         wp_set_current_user( self::$editor_id );
     387        wp_set_current_user( self::$user_ids['editor'] );
    309388
    310389        edit_post( $data );
Note: See TracChangeset for help on using the changeset viewer.