From f7ced48ad89f5a6381e824dbe66a8923b99824e6 Mon Sep 17 00:00:00 2001 From: Boone Gorges Date: Thu, 17 Dec 2020 16:15:38 +0000 Subject: [PATCH] Query: Respect post-type specific capabilities when querying for multiple post types. After this change, the relevant `read_private_posts` capability is checked for each queried post type. This ensures that private posts appear in search and archive queries for users who have the ability to view those posts. Props leogermani. Fixes #13509, #48968, #48556. git-svn-id: https://develop.svn.wordpress.org/trunk@49830 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-query.php | 114 +++++++++++----- tests/phpunit/tests/query/postStatus.php | 164 ++++++++++++++++++++++- 2 files changed, 240 insertions(+), 38 deletions(-) diff --git a/src/wp-includes/class-wp-query.php b/src/wp-includes/class-wp-query.php index 2f0dd53558..561aa747a7 100644 --- a/src/wp-includes/class-wp-query.php +++ b/src/wp-includes/class-wp-query.php @@ -2420,26 +2420,28 @@ class WP_Query { $where .= $wpdb->prepare( " AND {$wpdb->posts}.ping_status = %s ", $q['ping_status'] ); } + $has_valid_post_types = true; if ( 'any' === $post_type ) { $in_search_post_types = get_post_types( array( 'exclude_from_search' => false ) ); if ( empty( $in_search_post_types ) ) { - $where .= ' AND 1=0 '; + $post_type_where = ' AND 1=0 '; + $has_valid_post_types = true; } else { - $where .= " AND {$wpdb->posts}.post_type IN ('" . implode( "', '", array_map( 'esc_sql', $in_search_post_types ) ) . "')"; + $post_type_where = " AND {$wpdb->posts}.post_type IN ('" . implode( "', '", array_map( 'esc_sql', $in_search_post_types ) ) . "')"; } } elseif ( ! empty( $post_type ) && is_array( $post_type ) ) { - $where .= " AND {$wpdb->posts}.post_type IN ('" . implode( "', '", esc_sql( $post_type ) ) . "')"; + $post_type_where = " AND {$wpdb->posts}.post_type IN ('" . implode( "', '", esc_sql( $post_type ) ) . "')"; } elseif ( ! empty( $post_type ) ) { - $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_type = %s", $post_type ); + $post_type_where = $wpdb->prepare( " AND {$wpdb->posts}.post_type = %s", $post_type ); $post_type_object = get_post_type_object( $post_type ); } elseif ( $this->is_attachment ) { - $where .= " AND {$wpdb->posts}.post_type = 'attachment'"; + $post_type_where = " AND {$wpdb->posts}.post_type = 'attachment'"; $post_type_object = get_post_type_object( 'attachment' ); } elseif ( $this->is_page ) { - $where .= " AND {$wpdb->posts}.post_type = 'page'"; + $post_type_where = " AND {$wpdb->posts}.post_type = 'page'"; $post_type_object = get_post_type_object( 'page' ); } else { - $where .= " AND {$wpdb->posts}.post_type = 'post'"; + $post_type_where = " AND {$wpdb->posts}.post_type = 'post'"; $post_type_object = get_post_type_object( 'post' ); } @@ -2457,7 +2459,13 @@ class WP_Query { $user_id = get_current_user_id(); $q_status = array(); - if ( ! empty( $q['post_status'] ) ) { + + if ( ! $has_valid_post_types ) { + // When there are no public post types, there's no need to assemble the post_status clause. + $where .= $post_type_where; + } elseif ( ! empty( $q['post_status'] ) ) { + $where .= $post_type_where; + $statuswheres = array(); $q_status = $q['post_status']; if ( ! is_array( $q_status ) ) { @@ -2516,40 +2524,74 @@ class WP_Query { if ( ! empty( $where_status ) ) { $where .= " AND ($where_status)"; } + } elseif ( ! $this->is_singular ) { - $where .= " AND ({$wpdb->posts}.post_status = 'publish'"; - - // Add public states. - $public_states = get_post_stati( array( 'public' => true ) ); - foreach ( (array) $public_states as $state ) { - if ( 'publish' === $state ) { // Publish is hard-coded above. - continue; - } - $where .= " OR {$wpdb->posts}.post_status = '$state'"; + if ( 'any' === $post_type ) { + $queried_post_types = get_post_types( array( 'exclude_from_search' => false ) ); + } elseif ( is_array( $post_type ) ) { + $queried_post_types = $post_type; + } elseif ( ! empty( $post_type ) ) { + $queried_post_types = array( $post_type ); + } else { + $queried_post_types = array( 'post' ); } - if ( $this->is_admin ) { - // Add protected states that should show in the admin all list. - $admin_all_states = get_post_stati( - array( - 'protected' => true, - 'show_in_admin_all_list' => true, - ) - ); - foreach ( (array) $admin_all_states as $state ) { - $where .= " OR {$wpdb->posts}.post_status = '$state'"; + if ( ! empty( $queried_post_types ) ) { + $status_type_clauses = array(); + + // Assemble a post_status clause for each post type. + foreach ( $queried_post_types as $queried_post_type ) { + $queried_post_type_object = get_post_type_object( $queried_post_type ); + if ( ! $queried_post_type_object instanceof \WP_Post_Type ) { + continue; + } + + $type_where = '(' . $wpdb->prepare( "{$wpdb->posts}.post_type = %s AND (", $queried_post_type ); + + // Public statuses. + $public_statuses = get_post_stati( array( 'public' => true ) ); + $status_clauses = []; + foreach ( (array) $public_statuses as $public_status ) { + $status_clauses[] = "{$wpdb->posts}.post_status = '$public_status'"; + } + $type_where .= implode( ' OR ', $status_clauses ); + + // Add protected states that should show in the admin all list. + if ( $this->is_admin ) { + $admin_all_statuses = get_post_stati( + array( + 'protected' => true, + 'show_in_admin_all_list' => true, + ) + ); + foreach ( (array) $admin_all_statuses as $admin_all_status ) { + $type_where .= " OR {$wpdb->posts}.post_status = '$admin_all_status'"; + } + } + + // Add private states that are visible to current user. + if ( is_user_logged_in() ) { + $read_private_cap = $queried_post_type_object->cap->read_private_posts; + $private_statuses = get_post_stati( array( 'private' => true ) ); + foreach ( (array) $private_statuses as $private_status ) { + $type_where .= current_user_can( $read_private_cap ) ? " OR {$wpdb->posts}.post_status = '$private_status'" : " OR ({$wpdb->posts}.post_author = $user_id AND {$wpdb->posts}.post_status = '$private_status')"; + } + } + + $type_where .= '))'; + + $status_type_clauses[] = $type_where; } + + if ( ! empty( $status_type_clauses ) ) { + $where .= ' AND (' . implode( ' OR ', $status_type_clauses ) . ')'; + } + } else { + $where .= ' AND 1=0 '; } - if ( is_user_logged_in() ) { - // Add private states that are limited to viewing by the author of a post or someone who has caps to read private states. - $private_states = get_post_stati( array( 'private' => true ) ); - foreach ( (array) $private_states as $state ) { - $where .= current_user_can( $read_private_cap ) ? " OR {$wpdb->posts}.post_status = '$state'" : " OR {$wpdb->posts}.post_author = $user_id AND {$wpdb->posts}.post_status = '$state'"; - } - } - - $where .= ')'; + } else { + $where .= $post_type_where; } /* diff --git a/tests/phpunit/tests/query/postStatus.php b/tests/phpunit/tests/query/postStatus.php index 7d4a236b50..244bfdc9e8 100644 --- a/tests/phpunit/tests/query/postStatus.php +++ b/tests/phpunit/tests/query/postStatus.php @@ -6,14 +6,16 @@ class Tests_Query_PostStatus extends WP_UnitTestCase { public static $editor_user_id; public static $author_user_id; + public static $subscriber_user_id; public static $editor_private_post; public static $author_private_post; public static $editor_privatefoo_post; public static $author_privatefoo_post; public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { - self::$editor_user_id = $factory->user->create( array( 'role' => 'editor' ) ); - self::$author_user_id = $factory->user->create( array( 'role' => 'author' ) ); + self::$editor_user_id = $factory->user->create( array( 'role' => 'editor' ) ); + self::$author_user_id = $factory->user->create( array( 'role' => 'author' ) ); + self::$subscriber_user_id = $factory->user->create( array( 'role' => 'subscriber' ) ); self::$editor_private_post = $factory->post->create( array( @@ -457,4 +459,162 @@ class Tests_Query_PostStatus extends WP_UnitTestCase { $this->assertContains( $p1, wp_list_pluck( $q->posts, 'ID' ) ); } + + /** + * @ticket 48556 + * @ticket 13509 + */ + public function test_non_singular_queries_using_post_type_any_should_respect_post_type_read_private_posts_cap() { + register_post_type( + 'wptests_pt1', + array( + 'exclude_from_search' => false, + 'capabilities' => [ + 'read_private_posts' => 'read_private_pt1s', + ], + ) + ); + + register_post_type( + 'wptests_pt2', + array( + 'exclude_from_search' => false, + ) + ); + + $post_ids = array(); + + $post_ids['wptests_pt1_p1'] = $this->factory->post->create( + array( + 'post_type' => 'wptests_pt1', + 'post_status' => 'private', + 'post_author' => self::$editor_user_id, + ) + ); + + $post_ids['wptests_pt1_p2'] = $this->factory->post->create( + array( + 'post_type' => 'wptests_pt1', + 'post_status' => 'publish', + 'post_author' => self::$editor_user_id, + ) + ); + + $post_ids['wptests_pt2_p1'] = $this->factory->post->create( + array( + 'post_type' => 'wptests_pt2', + 'post_status' => 'private', + 'post_author' => self::$editor_user_id, + ) + ); + + $post_ids['wptests_pt2_p2'] = $this->factory->post->create( + array( + 'post_type' => 'wptests_pt2', + 'post_status' => 'publish', + 'post_author' => self::$editor_user_id, + ) + ); + + wp_set_current_user( 0 ); + + $q = new WP_Query( + array( + 'post_type' => 'any', + ) + ); + + $this->assertSameSets( array( $post_ids['wptests_pt1_p2'], $post_ids['wptests_pt2_p2'] ), wp_list_pluck( $q->posts, 'ID' ) ); + + wp_set_current_user( self::$subscriber_user_id ); + $GLOBALS['current_user']->add_cap( 'read_private_pt1s' ); + + $q = new WP_Query( + array( + 'post_type' => 'any', + ) + ); + + $this->assertSameSets( array( $post_ids['wptests_pt1_p1'], $post_ids['wptests_pt1_p2'], $post_ids['wptests_pt2_p2'] ), wp_list_pluck( $q->posts, 'ID' ) ); + } + + /** + * @ticket 48556 + * @ticket 13509 + */ + public function test_non_singular_queries_using_multiple_post_type_should_respect_post_type_read_private_posts_cap() { + wp_set_current_user( 0 ); + + register_post_type( + 'wptests_pt1', + array( + 'exclude_from_search' => false, + 'capabilities' => [ + 'read_private_posts' => 'read_private_pt1s', + ], + ) + ); + + register_post_type( + 'wptests_pt2', + array( + 'exclude_from_search' => false, + ) + ); + + $post_ids = array(); + + $post_ids['wptests_pt1_p1'] = $this->factory->post->create( + array( + 'post_type' => 'wptests_pt1', + 'post_status' => 'private', + 'post_author' => self::$editor_user_id, + ) + ); + + $post_ids['wptests_pt1_p2'] = $this->factory->post->create( + array( + 'post_type' => 'wptests_pt1', + 'post_status' => 'publish', + 'post_author' => self::$editor_user_id, + ) + ); + + $post_ids['wptests_pt2_p1'] = $this->factory->post->create( + array( + 'post_type' => 'wptests_pt2', + 'post_status' => 'private', + 'post_author' => self::$editor_user_id, + ) + ); + + $post_ids['wptests_pt2_p2'] = $this->factory->post->create( + array( + 'post_type' => 'wptests_pt2', + 'post_status' => 'publish', + 'post_author' => self::$editor_user_id, + ) + ); + + $q = new WP_Query( + array( + 'post_type' => 'any', + ) + ); + + $this->assertSameSets( array( $post_ids['wptests_pt1_p2'], $post_ids['wptests_pt2_p2'] ), wp_list_pluck( $q->posts, 'ID' ) ); + + $u = $this->factory->user->create(); + + wp_set_current_user( self::$subscriber_user_id ); + $GLOBALS['current_user']->add_cap( 'read_private_pt1s' ); + + $q = new WP_Query( + array( + 'post_type' => [ 'wptests_pt1', 'wptests_pt2' ], + ) + ); + + $this->assertSameSets( array( $post_ids['wptests_pt1_p1'], $post_ids['wptests_pt1_p2'], $post_ids['wptests_pt2_p2'] ), wp_list_pluck( $q->posts, 'ID' ) ); + } }